Skip to content

feat(sqs): broker-per-tenant multi-tenancy for AWS SQS (#3304) - #3316

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/sqs-broker-per-tenant-3304
Jul 6, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
feat/sqs-broker-per-tenant-3304

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3304

Brings AWS SQS up to parity with RabbitMQ / Azure Service Bus / NATS / Kafka on broker-per-tenant multi-tenancy. SQS already had named-broker support; this adds the missing runtime-routed, tenant-per-connection feature: each Wolverine tenant gets its own dedicated SQS connection (distinct AWS account/credentials, region, or ServiceURL) while sharing one queue topology, routed at runtime by Envelope.TenantId.

Approach

Mirrors the Azure Service Bus / Kafka tenant model. Each tenant owns a child AmazonSqsTransport with its own IAmazonSQS client and its own queue/QueueUrl cache — necessary because QueueUrl is cached per endpoint and would otherwise collide across tenants that share a queue name.

  • AmazonSqsTenant (new): Compile(parent, runtime) seeds the child from the parent connection (credential source, region/ServiceURL, AuthenticationRegion, AutoProvision/AutoPurge, DLQ behavior) and then applies the tenant's own Configure overrides (seed-then-override, so a tenant re-points only the axes it sets). Note: AmazonSQSConfig.RegionEndpoint lazily resolves to an ambient default (never null) and is mutually exclusive with ServiceURL, both handled here.
  • AmazonSqsTransport: LightweightCache<string, AmazonSqsTenant> Tenants; ConnectAsync compiles each tenant, builds its client, and (under AutoProvision) provisions the shared queues + DLQs on every tenant connection. Now implements IAsyncDisposable to dispose tenant (and its own) clients.
  • AmazonSqsQueue: CreateSender returns a TenantedSender fanning out to per-tenant InlineSqsSenders; BuildListenerAsync returns a CompoundListener (shared listener + one per-tenant SqsListener wrapped in ReceiverWithRules + TenantIdRule). A BuildTenantSibling helper materializes the tenant's twin queue with copied config on the tenant transport. All guarded on Tenants.Any() && TenancyBehavior == TenantAware, so the non-tenant path is unchanged.

GH-2361 gotcha

Both the tenant senders and the default fallback are fire-and-forget InlineSqsSenders. TenantedSender intentionally does not implement ISenderRequiresCallback / forward RegisterCallback, so a BatchedSender/SqsSenderProtocol under it would silently drop every message. Same model as RabbitMQ / NATS / Kafka.

New public API (AmazonSqsTransportConfiguration)

TenantIdBehavior(TenantedIdBehavior behavior);
AddTenant(string tenantId, Action<AmazonSQSConfig> configure);               // dedicated region/endpoint
AddTenant(string tenantId, AWSCredentials credentials, Action<AmazonSQSConfig>? configure = null); // dedicated account

Tests

  • 6 CI-safe unit tests (AmazonSqsPerTenantConfigurationTests, no broker): tenant registration, parent-seeding/override in Compile, credential inheritance vs. dedicated credentials, DLQ/region inheritance, tenant-aware-by-default, BuildTenantSibling isolation.
  • 5 LocalStack integration tests (AmazonSqsPerTenantConnectionTests, skip-guarded when Docker/LocalStack is down): tenant message routes to the tenant connection and NOT the shared one; default message routes to the shared one; inbound tenant message is consumed and stamped with its TenantId; per-tenant queue provisioning; endpoint resolves a TenantedSender.

Caveat — LocalStack cannot prove separate accounts. LocalStack is a single container that does not enforce AWS account boundaries, so these tests cannot demonstrate account isolation. Tenant isolation is simulated with a distinct signing region (AuthenticationRegion) on the same endpoint — LocalStack partitions SQS queues per region, so the tenant's queue is genuinely a different physical queue — and the assertions verify message routing + Envelope.TenantId stamping, not physical broker separation.

Verification

  • Wolverine.AmazonSqs.Tests: 215 passed, 6 skipped, 1 failed. The single failure (end_to_end_with_conventional_routing_with_prefix, a broker-init timeout with no tenants involved) reproduces identically on a clean tree without this change — a pre-existing LocalStack flake, not a regression.
  • dotnet build wolverine.slnx -c Release: 0 warnings, 0 errors.

Docs

Added a "Multi-Tenancy with a Broker per Tenant" section under transports/sqs with a compiling mdsnippets sample sourced from Samples/Bootstrapping.cs.

🤖 Generated with Claude Code

Amazon SQS already supported named brokers; this adds broker-per-tenant
multi-tenancy so each Wolverine tenant is served by its own dedicated SQS
connection (distinct AWS account/credentials, region, or ServiceURL) while
sharing one queue topology, routed at runtime by Envelope.TenantId.

Mirrors the Azure Service Bus / Kafka tenant model: each tenant owns a child
AmazonSqsTransport with its own IAmazonSQS client and its own queue/QueueUrl
cache (so a shared queue name can't collide across tenants). New AmazonSqsTenant
seeds the child from the parent connection (credentials, region/ServiceURL,
AuthenticationRegion, auto-provision + DLQ behavior) then applies the tenant's
own overrides in Compile(). Outbound wraps the endpoint in the framework
TenantedSender over fire-and-forget InlineSqsSenders (never a BatchedSender,
per GH-2361); inbound builds a CompoundListener with one per-tenant SqsListener
wrapped in ReceiverWithRules + TenantIdRule to stamp Envelope.TenantId.

New API on AmazonSqsTransportConfiguration:
- TenantIdBehavior(TenantedIdBehavior)
- AddTenant(tenantId, Action<AmazonSQSConfig>) — dedicated region/endpoint
- AddTenant(tenantId, AWSCredentials, Action<AmazonSQSConfig>?) — dedicated account

ConnectAsync compiles each tenant, builds its client, and (under AutoProvision)
provisions the shared queue topology + DLQs on every tenant connection. The
transport now implements IAsyncDisposable to dispose tenant (and its own) clients.

Tests: 6 CI-safe unit tests (tenant compile/seed/override + TenantedSender
wiring, no broker) and 5 LocalStack integration tests. LocalStack is a single
endpoint and does NOT enforce AWS account boundaries, so tenant isolation is
simulated with a distinct signing region (AuthenticationRegion) on the same
endpoint — LocalStack partitions SQS per region — proving message routing and
TenantId stamping rather than physical account separation. Integration tests
skip-guard when LocalStack/Docker is unavailable.

Docs: broker-per-tenant section under transports/sqs with a compiling snippet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller marked this pull request as ready for review July 6, 2026 03:06
@jeremydmiller
jeremydmiller merged commit 59bdf24 into main Jul 6, 2026
25 checks passed
This was referenced Jul 9, 2026
This was referenced Jul 28, 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

Development

Successfully merging this pull request may close these issues.

AWS SQS: add broker-per-tenant multi-tenancy (dedicated account/region per tenant)

1 participant