Skip to content

Include transport scheme in listener agent URIs to avoid cross-transport collision (GH-3027)#3028

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-3027-listener-agent-uri-collision
Jun 4, 2026
Merged

Include transport scheme in listener agent URIs to avoid cross-transport collision (GH-3027)#3028
jeremydmiller merged 1 commit into
mainfrom
fix-3027-listener-agent-uri-collision

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3027.

Problem

A single host listening on the same logical queue name across multiple transports — e.g. a "critterwatch" queue on Rabbit + SQS + Azure Service Bus, each with ListenOnlyAtLeader() — crashed on startup:

System.ArgumentException: An item with the same key has already been added.
Key: wolverine-leader-listener://critterwatch/
   at Wolverine.Runtime.Agents.LeaderPinnedListenerFamily..ctor(IWolverineRuntime runtime)

The same shape hit the non-leader-pinned ExclusiveListenerFamily path under cluster partitioning with multiple UseShardedXxxQueues("critterwatch", n) calls.

Root cause

LeaderPinnedListenerAgent and ExclusiveListenerAgent composed their agent URI from only the family scheme + Endpoint.EndpointName, with no transport identifier:

Uri = new Uri($"{SchemeName}://{_endpoint.EndpointName}");

Two endpoints sharing an EndpointName across different transports produced identical agent URIs, and the family ctors collide on .ToDictionary(e => e.Uri).

Fix

Fold the endpoint's transport scheme into the agent URI in both agents:

Uri = new Uri($"{SchemeName}://{_endpoint.Uri.Scheme}/{_endpoint.EndpointName}");

Produces distinct keys — wolverine-leader-listener://rabbitmq/critterwatch, .../sqs/critterwatch, .../azure-service-bus/critterwatch.

The composition is self-consistent: each family keys its dictionary off agent.Uri and resolves BuildAgentAsync / EvaluateAssignmentsAsync off the same value, so registration and lookup stay aligned. Single-transport hosts behave identically (the URI just gains a transport segment). Grepped for wolverine-leader-listener / wolverine-listener literal usages — no external code parses these agent URIs by string convention (the only wolverine-listener literal elsewhere is the unrelated health-check name).

Tests

New listener_agent_uri_includes_transport_scheme (CoreTests) — verifies both agents carry the transport scheme and that three same-named endpoints across transports no longer collide in the .ToDictionary(e => e.Uri) that the family ctors use. All three cases fail without the fix, pass with it.

  • Full wolverine.slnx Release build: 0 warnings, 0 errors.
  • CoreTests Runtime.Agents suite: 92 passing.

Note: no version bump on this branch. The companion fix #3025 (PR pending) carries the bump to 6.4.4; both should land before the next NuGet so 6.4.4 ships with both fixes.

🤖 Generated with Claude Code

…ort collision (GH-3027)

A single host listening on the same logical queue name across multiple transports — e.g. a
"critterwatch" queue on both Rabbit and SQS, each with ListenOnlyAtLeader() (or the sharded /
exclusive path under cluster partitioning) — crashed on startup with "An item with the same key
has already been added. Key: wolverine-leader-listener://critterwatch/".

The leader-pinned and exclusive listener agent URIs were composed from only the family scheme +
Endpoint.EndpointName, with no transport identifier, so two endpoints sharing an EndpointName
across different transports produced identical agent URIs and collided in the family ctor's
.ToDictionary(e => e.Uri).

Both LeaderPinnedListenerAgent and ExclusiveListenerAgent now fold the endpoint's transport scheme
into the agent URI (wolverine-leader-listener://rabbitmq/critterwatch, .../sqs/critterwatch, ...).
The composition is self-consistent — the family keys its dictionary and resolves
BuildAgentAsync/EvaluateAssignmentsAsync off the same agent.Uri — and single-transport hosts behave
identically (the URI just gains a transport segment). No external code parses these agent URIs by
string convention (the only "wolverine-listener" literal elsewhere is the unrelated health-check
name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit a88bd3c into main Jun 4, 2026
24 of 25 checks passed
This was referenced Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant