Repository navigation
fix(floci): select Service Bus host ports below the ephemeral range - #2241
Conversation
The Service Bus endpoint allocator took OS-assigned ephemeral ports and released them before Docker bound them. DCP allocates project proxy ports from the same range and could receive the same port. On Windows the proxy's loopback listener then shadows Docker's wildcard bind, so AMQP clients reach the proxy instead of the broker; on Linux Docker's bind fails and the broker never starts. Select random free ports in 20000-32767 instead, below every OS ephemeral range. Explicit ports are unchanged. Fixes CommunityToolkit#2240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh | bash -s -- 2241Or
iex "& { $(irm https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1) } 2241" |
There was a problem hiding this comment.
🟡 Changes recommended
Random selection can collide with explicitly configured app-model ports.
1 open finding
What changed in this PR
Updates Floci Service Bus port allocation to avoid DCP ephemeral-port races.
Changes:
- Selects random ports from 20000–32767 and probes availability.
- Tests verify automatically selected ports use that range.
| File | Description |
|---|---|
FlociServiceBusEndpointAllocator.cs |
Implements bounded host-port selection. |
AzureServiceBusResourceTests.cs |
Verifies selected and configured ports. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A port configured explicitly on any endpoint (including one of the two Service Bus endpoints) is bound later, so the availability probe cannot see it. Exclude every configured app-model port from selection, and scan the whole range from a random offset instead of a fixed number of random attempts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docker also binds [::]:<port>, and a process listening on ::1 shadows it for IPv6 localhost connections, as on IPv4 loopback. Probe ::1 too when the OS supports IPv6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // Ports configured anywhere in the app model are bound later, so probing cannot see them. | ||
| var reservedPorts = @event.Model.Resources | ||
| .SelectMany(resource => resource.Annotations.OfType<EndpointAnnotation>()) | ||
| .Select(endpoint => endpoint.Port) |
There was a problem hiding this comment.
This probably needs to filter to endpoint.IsAllocated. endpoint allocation can occur asyncronously, so not everythign is guaranteed to have a port allocated at this point, in which case this method may throw or stall.
Either
- Guard by only reading port if
endpoint.IsAllocated - await endpoint.Property(EndpointProperty.Port).GetValueAsync() to guarantee you get hte port
Probably the former as otherwise you may hang forever.
There was a problem hiding this comment.
Neither read blocks here. EndpointAnnotation.Port is the configured value (a plain property), and AllocatedEndpoint returns null until the snapshot is set, without waiting. So an endpoint that is still being allocated never throws or stalls this handler.
I didn't filter to IsAllocated, though. At BeforeStartEvent almost nothing is allocated yet, so that filter would drop the configured ports. Those are the ones this set must hold, because their sockets don't exist yet and probing can't see them.
ef2af1d adds already-allocated ports to the set as well, so it is now configured ports plus allocated ports. I moved the logic into GetReservedPorts and added GetReservedPortsIncludesConfiguredAndAllocatedPortsOnly: a configured port and an allocated port are both reserved, and a pending dynamic endpoint is skipped.
… Bus ports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Closes #2240
FlociServiceBusEndpointAllocatorusedTcpListener(IPAddress.Any, 0)to get an OS-assigned ephemeral port, then released it before Docker bound it. DCP allocates project proxy ports from the same ephemeral range and can receive that port in the meantime:127.0.0.1listener shadows Docker's wildcard bind. AMQP clients connecting tolocalhost:<port>reach a project proxy instead of the broker.port is already allocated, and the sidecar never starts.The issue includes evidence from DCP
Serviceobjects showingfloci-servicebus-amqpandchat-httpson the same port.Change
When no port is configured,
SelectPortnow picks a random port in 20000-32767. That range is below every OS ephemeral range (Linux starts at 32768; Windows and macOS start at 49152), so DCP never allocates these ports. Ports configured on any app-model endpoint, including the sibling Service Bus endpoint, are excluded without probing. The scan starts at a random offset and covers the whole range, so a free port is always found when one exists; the allocator throws only when none is. A candidate must be free on IPv4 loopback, IPv6 loopback (when the OS supports IPv6) andIPAddress.Any. As before, theAnyprobe listeners are held until every port is selected, so AMQP and AMQPS stay distinct. Because the start offset is random, concurrent isolated AppHosts still get different ports. Explicitly configured ports are unchanged.Tests
WithServiceBusAllocatesEndpointsBeforeConfiguringTheDataPlanenow asserts that selected ports fall within 20000-32767. Explicitly configured ports are still checked for equality.CommunityToolkit.Aspire.Hosting.Floci.Testsrun on Windows: 84/84 passed.PR Checklist
Other information
The same approach was checked in a downstream AppHost by passing ports in this range explicitly through
WithServiceBus(name, amqpPort, amqpTlsPort);aspire start --isolatedthen reached healthy.The k3s allocator uses the same
TcpListener(IPAddress.Any, 0)pattern and may have the same race. It is left out of this PR, which addresses one issue.