feat(sqs): broker-per-tenant multi-tenancy for AWS SQS (#3304)#3316
Merged
Conversation
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
marked this pull request as ready for review
July 6, 2026 03:06
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
Merged
This was referenced Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 byEnvelope.TenantId.Approach
Mirrors the Azure Service Bus / Kafka tenant model. Each tenant owns a child
AmazonSqsTransportwith its ownIAmazonSQSclient and its own queue/QueueUrlcache — necessary becauseQueueUrlis 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 ownConfigureoverrides (seed-then-override, so a tenant re-points only the axes it sets). Note:AmazonSQSConfig.RegionEndpointlazily resolves to an ambient default (never null) and is mutually exclusive withServiceURL, both handled here.AmazonSqsTransport:LightweightCache<string, AmazonSqsTenant> Tenants;ConnectAsynccompiles each tenant, builds its client, and (underAutoProvision) provisions the shared queues + DLQs on every tenant connection. Now implementsIAsyncDisposableto dispose tenant (and its own) clients.AmazonSqsQueue:CreateSenderreturns aTenantedSenderfanning out to per-tenantInlineSqsSenders;BuildListenerAsyncreturns aCompoundListener(shared listener + one per-tenantSqsListenerwrapped inReceiverWithRules+TenantIdRule). ABuildTenantSiblinghelper materializes the tenant's twin queue with copied config on the tenant transport. All guarded onTenants.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.TenantedSenderintentionally does not implementISenderRequiresCallback/ forwardRegisterCallback, so aBatchedSender/SqsSenderProtocolunder it would silently drop every message. Same model as RabbitMQ / NATS / Kafka.New public API (
AmazonSqsTransportConfiguration)Tests
AmazonSqsPerTenantConfigurationTests, no broker): tenant registration, parent-seeding/override inCompile, credential inheritance vs. dedicated credentials, DLQ/region inheritance, tenant-aware-by-default,BuildTenantSiblingisolation.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 itsTenantId; per-tenant queue provisioning; endpoint resolves aTenantedSender.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.TenantIdstamping, 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/sqswith a compiling mdsnippets sample sourced fromSamples/Bootstrapping.cs.🤖 Generated with Claude Code