Skip to content

fix(floci): let Docker assign Service Bus host ports and discover them from floci-az - #2336

Open
thomhurst wants to merge 3 commits into
CommunityToolkit:mainfrom
thomhurst:floci-servicebus-dynamic-amqp-ports
Open

thomhurst wants to merge 3 commits into
CommunityToolkit:mainfrom
thomhurst:floci-servicebus-dynamic-amqp-ports

Conversation

@thomhurst

@thomhurst thomhurst commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2335

When WithServiceBus() gets no ports, the integration no longer chooses the Service Bus host ports itself. Docker assigns them atomically when floci-az starts the Artemis sidecar, and the integration reads them back from floci-az once the emulator is ready. #2240 / #2241 narrowed the port range but kept the probe-and-release step, so a port could still be taken by another process in the seconds before Docker bound it. Only an atomic bind closes that window.

Change

  • Ports left unset: FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT / ..._AMQP_TLS_PORT are 0, which floci-az already passes to Docker as a dynamic binding. The child's amqp / amqps endpoints stay unallocated until the parent raises ResourceReadyEvent. The allocator then calls GET /devstoreaccount1-servicebus/namespaces/default on the emulator and allocates the endpoints from amqpHostPort / amqpsHostPort on both the localhost network and the container network (host gateway, same port). It then publishes ResourceEndpointsAllocatedEvent and ConnectionStringAvailableEvent and marks the child Running. If floci-az does not report usable ports, the error is logged to the child's resource log, the child is marked FailedToStart, and consumers already resolving its endpoints get the error instead of waiting forever.
  • Ports passed explicitly: behavior is unchanged. They go to floci-az as before and are allocated during BeforeStartEvent, with no probing and no API call. A mixed call (one port given, one not) allocates the given port up front and discovers the other.
  • Child state: the child is Waiting until the parent is ready, then Running, and Exited when the parent stops. Executables and projects can WaitFor(serviceBus).
  • Containers: DCP tunnels a container's host dependencies when it creates the container and needs their addresses at that point, so a container that waits on a child whose ports are not assigned yet would fail with Container tunnel service ... should have valid address. While ports are Docker-assigned, a container's WaitFor(serviceBus) is redirected to the emulator. Its connection string still resolves only after the ports are allocated, so the ordering holds. The container reference helper now reads the endpoint's Port, which waits for allocation, instead of TargetPort, which throws when read before discovery.
  • Removed: the port probing (SelectPort, GetReservedPorts, the 20000-32767 range) and its tests.

Why discovery goes through floci-az and not the container runtime

The other option was reading the sidecar's bindings with docker inspect. That works today, but it needs the Docker CLI on the AppHost's PATH, it does not cover Podman without extra runtime detection, and it depends on floci-az's internal container naming (floci-az-<FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE>-servicebus-<namespace>). floci-az documents its namespace API as the way orchestrators discover the endpoint at runtime, and it knows the published ports first-hand.

Dependency

When floci-az runs in a container, which is how this integration runs it, 0.15.0's namespace API reports only the sidecar's container-internal ports (5672/5671). floci-io/floci-az#592 / floci-io/floci-az#593 add amqpHostPort / amqpsHostPort, the Docker-published host ports in every mode. This PR reads those fields. They ship in floci-az 0.16.0, which main already uses (FlociContainerImageTags.AzureTag). The "Requires floci-az ..." line in the README and XML docs now says 0.16.0.

Breaking change note

FlociAzureServiceBusResource no longer implements IResourceWithParent<FlociAzureContainerResource>. Aspire publishes ConnectionStringAvailableEvent for IResourceWithParent children before it creates the parent container. Resolving this child's connection string waits for its endpoints, which now exist only after the parent has started, so keeping the interface deadlocks startup. The Parent property and the dashboard nesting (the existing WithParentRelationship annotation) are kept. Code that treated the child as IResourceWithParent (for example casting it, or relying on its inherited health checks) is affected. Its readiness now comes from the child's own Running state, which is set only after floci-az is healthy. The API baseline (api/*.cs) is regenerated. Its only change is this interface removal.

Tests

  • WithServiceBusPassesConfiguredPortsAndLetsDockerAssignTheRest: configured ports are allocated during BeforeStartEvent and passed through. Unset ones stay unallocated and are passed as 0.
  • ParentReadyAllocatesDockerAssignedPortsFromTheNamespaceApi (unset, AMQP-only, and AMQPS-only configured): uses a stubbed namespace API to check the request URL, that the host-port fields are used, the allocated ports, the connection string, ConnectionStringAvailableEvent, and the Waiting → Running → Exited states.
  • ParentReadyWithConfiguredPortsDoesNotQueryTheNamespaceApi, ParentReadyMarksTheChildFailedWhenPortsCannotBeDiscovered, ContainersWaitForTheEmulatorWhileServiceBusPortsAreUnknown, ParseNamespacePorts*, ServiceBusIsLinkedToItsParentByRelationship.
  • MultipleEmulatorsUseDistinctChildNamesAndConnectionKeys now discovers ports per emulator.
  • Full CommunityToolkit.Aspire.Hosting.Floci.Tests run on Windows with Docker: 104 tests, 101 passed. The 3 failures are the Docker-backed Service Bus tests listed under Dependency (2 functional tests and the TypeScript AppHost test). AzureServiceBusResourceTests: 31/31 passed.

PR Checklist

  • Created a feature/dev branch in your fork (vs. submitting directly from a commit on main)
  • Based off latest main branch of toolkit
  • PR doesn't include merge commits (always rebase on top of our main, if needed)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Contains NO breaking changes (see the breaking change note above)
  • Every new API (including internal ones) has full XML docs
  • Code follows all style conventions

Other information

A downstream AppHost has run the same approach (dynamic ports, discovery after floci-az is ready, a parent-relationship annotation instead of IResourceWithParent) and has not hit the bind failure since.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh | bash -s -- 2336

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1) } 2336"

…m from floci-az

Probing a free port and releasing it before the Artemis sidecar binds it leaves a window in
which another process can take the port, and Docker cannot recover from an explicit binding.
Ports left unset are now passed to floci-az as 0, so Docker binds them atomically, and the
child's endpoints are allocated from floci-az's namespace API once the emulator is ready.
Explicitly configured ports are unchanged.

Closes CommunityToolkit#2335
Docker-assigned AMQP host ports are discovered through the amqpHostPort /
amqpsHostPort fields that floci-az 0.16.0 added to the namespace API.
The image tag is already 0.16.0 on main.

The baseline drops IResourceWithParent<FlociAzureContainerResource> from
FlociAzureServiceBusResource.
@thomhurst
thomhurst force-pushed the floci-servicebus-dynamic-amqp-ports branch from f7980d6 to a6f8832 Compare November 1, 2026 18:47
@thomhurst
thomhurst marked this pull request as ready for review November 1, 2026 18:47
Copilot AI balanced review requested due to automatic review settings November 1, 2026 18:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The allocator can let containers start despite Service Bus discovery failures, fail with valid non-default namespace configurations, and leave late container-network lookups waiting indefinitely after discovery errors.

3 open issues
What changed in this PR

This PR addresses #2335 by replacing Floci Service Bus port probing with Docker-assigned ports, removing the gap between selecting a port and binding it.

Changes:

  • Discovers dynamic host ports through floci-az’s namespace API while preserving explicitly configured ports.
  • Adds child lifecycle and failure handling, adjusts container waits, and removes IResourceWithParent to avoid startup deadlock.
  • Updates tests, documentation, and the generated API baseline.
File Description
tests/​CommunityToolkit.Aspire.Hosting.Floci.Tests/​AzureServiceBusResourceTests.cs Tests dynamic discovery, configured ports, lifecycle states, failures, and wait redirection.
src/​CommunityToolkit.Aspire.Hosting.Floci/​README.md Documents port discovery, container behavior, and the floci-az 0.16.0 requirement.
src/​CommunityToolkit.Aspire.Hosting.Floci/​FlociServiceBusEndpointAllocator.cs Replaces probing with API discovery and manages child state and endpoint allocation.
src/​CommunityToolkit.Aspire.Hosting.Floci/​FlociHostingExtension.Azure.cs Passes unset ports as zero and defers container port resolution.
src/​CommunityToolkit.Aspire.Hosting.Floci/​FlociAzureServiceBusResource.cs Removes the parent interface while retaining the parent property and relationship.
src/​CommunityToolkit.Aspire.Hosting.Floci/​api/​CommunityToolkit.Aspire.Hosting.Floci.cs Reflects the public interface removal in the generated baseline.

Review effort: Balanced

Comment on lines +153 to +157
container.Annotations.Remove(wait);
container.Annotations.Add(new WaitAnnotation(resource.Parent, wait.WaitType, wait.ExitCode)
{
WaitBehavior = wait.WaitBehavior
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 465be67. Redirected WaitUntilHealthy waits now also add an internal ServiceBusWaitAnnotation. A BeforeResourceStartedEvent handler calls WaitForResourceHealthyAsync on the child with the original wait behavior. Aspire raises that event before DCP creates the container, so the child never becomes a tunnel dependency, and the container fails to start when the child is FailedToStart. Tests cover both cases: WaitFor only, and a configured AMQP port with dynamic AMQPS.

Comment on lines +239 to +240
var uri = new Uri(
$"http://{hostAndPort}/{FlociAzureContainerResource.DefaultAccountName}-servicebus/namespaces/{DefaultNamespace}");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 465be67. Discovery now calls GET /{account}-servicebus/namespaces and picks a namespace in this order: one whose host port matches a configured port (floci-az binds the configured ports to the first topology namespace), then default, then the only namespace running. If none matches, it throws an error that lists the running namespaces. Tests cover a sbemulatorns topology namespace.

Comment on lines +268 to +270
foreach (var snapshot in endpoint.AllAllocatedEndpoints.Where(snapshot => !snapshot.Snapshot.IsValueSet))
{
snapshot.Snapshot.SetException(exception);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 465be67. On failure, FailPendingEndpoints now adds a faulted snapshot for both the localhost and default container networks, or faults the existing unset snapshot. Added ContainerNetworkLookupsAfterAFailedDiscoveryFail, which starts container-network resolution only after the discovery error.

…amespace, fault late lookups

- Keep a child readiness gate for containers whose waits are redirected to
  the parent: enforce it in BeforeResourceStartedEvent so the container fails
  to start when Service Bus fails, without making the child a DCP dependency.
- List floci-az's Service Bus namespaces instead of assuming `default`, so a
  mounted Config.json topology with another namespace name still works.
- On discovery failure, fault both supported network snapshots, including
  ones first requested after the failure.

[AspireExport(ExposeProperties = true)]
public partial class FlociAzureServiceBusResource : Resource, IResourceWithParent<FlociAzureContainerResource>, IResourceWithParent, IResource, IResourceWithConnectionString, IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences, IResourceWithEndpoints
public partial class FlociAzureServiceBusResource : Resource, IResourceWithConnectionString, IResource, IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences, IResourceWithEndpoints

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get picked up by an automated job so it can be revered here.

public class FlociAzureServiceBusResource(
string name,
FlociAzureContainerResource parent) : Resource(name),
IResourceWithParent<FlociAzureContainerResource>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically a breaking API change, since the type signature of the Resoruce type is going to be different.

IMO the change is likely to be minimally impactful on the users - do you agree?

@aaronpowell aaronpowell added this to the 17.0 milestone Nov 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Floci] Service Bus host ports are probed then released, so another process can take them before the Artemis sidecar binds

3 participants