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

feat(sqs): broker-per-tenant multi-tenancy for AWS SQS (#3304)#3316
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
knotekbr pushed a commit to knotekbr/wolverine that referenced this pull request Jul 6, 2026
Bring the AWS SNS transport up to multi-broker parity with SQS/Kafka/etc.
SNS previously had neither feature.

Named broker:
- Add public AmazonSnsTransport(string protocol) ctor; parameterless delegates to it.
- Build the topic URI scheme from parent.Protocol (was hard-coded "sns"), so a
  named broker's topics carry the broker name as their Uri scheme and match
  findEndpointByUri.
- AddNamedAmazonSnsBroker(BrokerName[, Action<SnsConfig>]),
  ToSnsTopicOnNamedBroker, UseAmazonSnsTransportLocallyAsNamedBroker.
- Because TransportCollection keys transports by Protocol, a named SNS broker and
  a same-named SQS broker cannot coexist. The named SNS's paired SQS (used only
  for the SNS-side client + SNS->SQS subscription provisioning) is therefore a
  standalone, unregistered AmazonSqsTransport seeded from the SNS connection in
  ConnectAsync so it targets the same account/region.

Broker-per-tenant (publish side only; SNS is publish-only):
- New AmazonSnsTenant: child AmazonSnsTransport with its own SnsConfig /
  CredentialSource / SNS client and a paired SQS client for subscription
  provisioning; Compile seeds from the parent then applies tenant overrides.
- AmazonSnsTransport gains a Tenants cache, per-tenant client build in
  ConnectAsync, and IAsyncDisposable to dispose tenant clients.
- AmazonSnsTopic.CreateSender wraps the default in a TenantedSender and registers
  a per-tenant InlineSnsSender bound to each tenant's SNS client via
  BuildTenantSibling. Both default and tenant senders are fire-and-forget
  InlineSnsSender (never BatchedSender) per JasperFxGH-2361. BuildListenerAsync still
  throws NotSupportedException.
- AddTenant(tenantId, Action<SnsConfig>) + credentials overload, and
  TenantIdBehavior(TenantedIdBehavior) on the configuration.

Full per-tenant CONSUMPTION composes with the SQS broker-per-tenant support
(PR JasperFx#3316): each SNS tenant provisions its subscription/queue-policy through its
own paired SQS client that lines up on the same account/region.

Tests: unit wiring coverage (no broker) for tenant registration/compile/sender
resolution and the named-broker Uri scheme; LocalStack integration for named
publish -> subscribed SQS queue -> handler, and per-tenant publish routing by
region partition. Docs: named-broker + multi-tenancy sections in sns.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 9, 2026
This was referenced Jul 13, 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