Repository navigation
Reserve proxyless endpoint ports process-wide to avoid cross-app port collisions - #21533
Conversation
ProxylessEndpointPortAllocator is a per-application singleton that probes a port (bind + release) and hands it out; the workload binds it later. When multiple AppHosts run in the same process (e.g. Aspire.Hosting.Testing tests running in parallel), two allocators could hand out the same target port, so one app's dashboard/proxy ends up pointing at another app's workload. Track allocated ports in a process-wide reservation set that is released when the allocator is disposed. Fixes microsoft#20009 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 21533Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 21533" |
There was a problem hiding this comment.
🟡 Changes recommended
The test helper checks only IPv4 loopback while production probes both IPv4 and IPv6 wildcard addresses, so an IPv6-only listener can cause a false regression failure.
1 open issue
What changed in this PR
This PR addresses cross-application port collisions behind the flaky dashboard login test in #20009 by sharing port reservations across AppHosts in the same process.
Changes:
- Reserve ports before probing, releasing unsuccessful probes immediately and successful allocations when the owning allocator is disposed.
- Add regression tests for cross-application uniqueness and reservation release.
- Isolate reservations in executor tests whose allocators are not disposed.
| File | Description |
|---|---|
tests/Aspire.Hosting.Tests/Dcp/ProxylessEndpointPortAllocatorTests.cs |
Adds shared-reservation and disposal regression tests. |
tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs |
Uses isolated reservations to avoid process-wide test leaks. |
src/Aspire.Hosting/Dcp/ProxylessEndpointPortAllocator.cs |
Adds synchronized process-wide reservations and disposal cleanup. |
Review effort: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI rerun note for head
On the previous head ( 🤖 Posted by AspireAgent (AI Agent) |
Replace the allocator's per-instance visited array with the shared ProxylessEndpointPortReservations, backed by a directly indexed state array for the whole port space instead of a HashSet. Allocated ports stay reserved until the allocator is disposed; ports whose probe failed are marked unavailable process-wide so no allocator re-probes them. Ports excluded by the app model remain per-allocator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI note for head
Windows E2E shards have also failed intermittently on recent 🤖 Posted by AspireAgent (AI Agent) |
Use the process-wide reservation array as the single availability authority. Entries count live reservation references (1-254) with 255 as the permanent-exclusion sentinel. ExcludePort now adds shared references instead of a per-allocator set, deduplicated per allocator, and disposal releases only the references that allocator registered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI note for head
🤖 Posted by AspireAgent (AI Agent) |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
CI note for head
🤖 Posted by AspireAgent (AI Agent) |
e5a5acd
into
microsoft:main

Description
DashboardUrlTests.GetDashboardUrlAsyncAuthenticatesDashboardBrowserintermittently fails withExpected: Found, Actual: RedirectKeepVerbon the/login?t=...request (CI log).Root cause
Over plain HTTP the dashboard only issues
302 Foundfrom its login and redirect middleware. A307can't come from the dashboard. It comes fromTestingAppHost1'smywebapp1, which callsUseHttpsRedirection().Target ports for proxied endpoints that don't specify a
TargetPortcome fromProxylessEndpointPortAllocator. That allocator is a per-application DI singleton. It probes a port by binding it and then releasing it, and the workload binds the port later.Aspire.Hosting.Testing.Testsruns several AppHosts in the same process in parallel, so two applications' allocators can hand out the same target port. In that case:address already in use, oraspire-dashboardforwards to another app'smywebapp1, which returns307.GetDashboardUrlAsynconly waits for the dashboard to be running, so the test then hits the wrong backend.Fix
Port availability is now tracked in one process-wide
ProxylessEndpointPortReservationsarray, indexed directly by port. Every allocator uses it as the single availability authority. Eachbyteentry holds:0: available for dynamic allocation.1-254: the number of live reservation references.255: permanently excluded after a failed probe.How each operation uses it:
0entry before probing (the probe runs outside the shared lock).ExcludePort(fixed, persisted and target ports) adds a reference to the same shared array. Repeated exclusions, or exclusions of ports the allocator already owns, are deduplicated. A permanently excluded port stays permanent.254throws rather than wrapping or saturating.Allocators for different applications in one process can no longer return the same port. Assertions, retries and timeouts are unchanged.
DcpExecutorTestsbuilds allocators that are never disposed, so it now passes an isolated reservation set instead of leaking into the shared one.Evidence
Before (functional repro). I ran the full
Aspire.Hosting.Testing.Testsassembly withASPIRE_PROXYLESS_ENDPOINT_PORT_RANGE=20000-20015to force overlap between parallel AppHosts:This happened 4 times in
aspire-dashboard.GetDashboardUrlAsyncReturnsBaseUrlWhenDashboardAllowsAnonymousAccessfailed with "aspire-dashboard ... failed to start".After. The same run with the fix had 0
address already in useerrors and no dashboard start failures. I only have log evidence here: the full assembly doesn't finish in my sandbox because of unrelated environment issues (no trusted dev cert, no Playwright browsers).Regression test.
AllocatorsForDifferentApplicationsInTheSameProcessDoNotShareAPortuses the production constructor with a single-port range:Assert.Throws() Failure: No exception was thrown, because the second app's allocator received the same port.ProxylessEndpointPortAllocatorTests(13/13) andDcpExecutorTests(310/310) pass.Fixes #20009
Checklist
🤖 Posted by AspireAgent (AI Agent)