Repository navigation
[Floci] Service Bus host ports are probed then released, so another process can take them before the Artemis sidecar binds #2335
Description
Describe the bug
When WithServiceBus() is called without explicit ports, FlociServiceBusEndpointAllocator picks the AMQP and AMQPS host ports itself during BeforeStartEvent. It binds a probe TcpListener on each candidate port, releases the probes, and passes the ports to floci-az through FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT and FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_TLS_PORT. floci-az binds them much later, when it starts the Artemis sidecar with -p <port>:5672 and -p <port>:5671.
Nothing holds the port between the release and Docker's bind. That window covers the rest of AppHost startup, the floci-az image pull, and floci-az's own boot, so it is often several seconds long. Any other process can take the port in that time: another AppHost started concurrently with --isolated, a DCP proxy, a test fixture, or anything else that binds a port. Docker then fails to start the sidecar:
failed to bind host port 0.0.0.0:<port>/tcp: address already in use
floci-az logs the failed namespace start and reports /health as DOWN, so the emulator never becomes healthy and every Service Bus consumer waits forever. Docker retries a port conflict only for dynamic (0) host ports, so an explicit preselected port cannot recover.
#2240 / #2241 moved the selection to a random port in 20000-32767 so DCP's ephemeral proxy ports no longer collide with it. That removed the most common collision, but the allocator still probes and releases, so the time-of-check/time-of-use race remains for everything else that binds ports in that range, including a second AppHost making the same random choice.
Regression
Not a regression. The probe-and-release design has been in place since the allocator was added in 13.6.0.
Steps to reproduce
The race is timing-dependent, so the simplest deterministic repro takes the selected port between allocation and the sidecar's start, as another process would:
var builder = DistributedApplication.CreateBuilder(args);
var azure = builder.AddFlociAzure("floci-az").WithDockerSocket();
var serviceBus = azure.WithServiceBus();
// Simulate another process binding the port after the integration released it.
TcpListener? squatter = null;
builder.Eventing.Subscribe<BeforeResourceStartedEvent>(azure.Resource, (_, _) =>
{
squatter ??= new TcpListener(IPAddress.Any, serviceBus.Resource.AmqpEndpoint.Port);
squatter.Start();
return Task.CompletedTask;
});
builder.Build().Run();- Run the AppHost.
- floci-az logs that it could not start the default Service Bus namespace because Docker could not bind the host port.
floci-azstays Unhealthy, and anything that waits for it never starts.
Without the squatter, the same failure happens intermittently when several AppHosts start at the same time on one machine (for example parallel CI jobs or test fixtures), or when another local service happens to bind the selected port first.
Expected behavior
The Service Bus sidecar starts reliably regardless of what else binds ports on the machine. Only an atomic bind can guarantee that, so the host ports should be assigned by Docker, not chosen in advance by the integration:
- When the caller does not pass
amqpPort/amqpTlsPort, setFLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT=0andFLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_TLS_PORT=0. floci-az already treats0as a dynamic Docker binding. - Leave the child's
amqp/amqpsendpoints unallocated until floci-az is ready, then read the ports Docker published from floci-az's management API (GET /{account}-servicebus/namespaces/default, fieldsamqpHostPort/amqpsHostPort), allocate the endpoints, and raiseResourceEndpointsAllocatedEventandConnectionStringAvailableEventfor the child. - Keep explicitly configured ports exactly as they are today.
Two details matter for this:
- The child currently implements
IResourceWithParent<FlociAzureContainerResource>. Aspire publishesConnectionStringAvailableEventforIResourceWithParentchildren before it creates the parent container, and resolving the connection string waits for the child's endpoints. With ports that only exist after floci-az starts, that is a deadlock. The parent-child link already exists as aWithParentRelationshipannotation, which keeps the dashboard nesting without the early resolution. - When floci-az runs in a container, its namespace API currently reports the sidecar's container-internal ports rather than the published host ports, so discovery through the API needs the new
amqpHostPort/amqpsHostPortfields from [BUG] Service Bus / Event Hubs: namespace API reports the sidecar's container ports, not its published host ports, when floci-az runs in a container floci-io/floci-az#592 / fix(servicebus): report the sidecar's published host ports when floci-az runs in a container floci-io/floci-az#593 and an image tag bump here.
Screenshots
N/A
IDE and version
Other
IDE version
Aspire CLI / Aspire.AppHost.Sdk 13.6.4
Nuget packages
CommunityToolkit.Aspire.Hosting.Floci (current main)
Image: floci/floci-az:0.15.0
Docker Desktop on Windows 11; also Linux CI runners
Additional context
Help us help you
Yes, I'd like to be assigned to work on this item
Activity
fix(floci): let Docker assign Service Bus host ports and discover the…