feat(sns): named-broker and broker-per-tenant support (GH-3305)#3317
Merged
Conversation
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 GH-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 #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>
jeremydmiller
marked this pull request as ready for review
July 6, 2026 11:03
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 #3305
Brings the AWS SNS transport (
Wolverine.AmazonSns) up to multi-broker parity with SQS/Kafka/RabbitMQ/Azure Service Bus. SNS previously had neither feature. This PR adds both.Named broker
public AmazonSnsTransport(string protocol)ctor (required byTransportCollection.GetOrCreate<T>(BrokerName)); the parameterless ctor delegates to it.AmazonSnsTopicbuilt itsUrifrom a hard-coded"sns"literal, so a named broker's topics carried schemesnswhileProtocolwas the name →findEndpointByUrimismatch. The scheme is now built fromparent.Protocol(mirroringAmazonSqsQueue).AddNamedAmazonSnsBroker(BrokerName[, Action<AmazonSimpleNotificationServiceConfig>]),ToSnsTopicOnNamedBroker(BrokerName, string),UseAmazonSnsTransportLocallyAsNamedBroker(BrokerName, int port = 4566).SNS↔SQS pairing caveat (design note)
TransportCollectionkeys transports byProtocol, so a named SNS broker and a same-named SQS broker cannot coexist under oneBrokerName(they'd evict each other) — the named SNS owns that key. Since the SNS-side SQS is used only internally (the SNS client + SNS→SQS subscription/queue-policy provisioning), a named SNS broker pairs with a standalone, unregisteredAmazonSqsTransportthatConnectAsyncseeds from the SNS connection so it targets the same account/region. A user who also wants to listen on the subscribed queue does so through the default (or a differently-named) SQS broker. The default (unnamed) broker still pairs with the shared default SQS transport as before.Broker-per-tenant (publish side)
SNS is publish-only (
BuildListenerAsyncthrowsNotSupportedException; consumption happens by subscribing an SQS queue to the topic). So broker-per-tenant here needs only aTenantedSender— never aCompoundListener.AmazonSnsTenant: a childAmazonSnsTransportwith its ownSnsConfig/CredentialSource/ SNS client and its own paired SQS client for subscription provisioning.Compile(parent, runtime)seeds credentials + connection from the parent, applies the tenant's overrides, aligns the paired SQS client to the tenant's final SNS account/region, then builds both clients.AmazonSnsTransport:Tenantscache, per-tenant client build inConnectAsync, andIAsyncDisposableto dispose tenant clients.AmazonSnsTopic.CreateSender: when tenants exist and the endpoint is tenant-aware, wrap the default in aTenantedSenderand register a per-tenantInlineSnsSenderbound (viaBuildTenantSibling) to each tenant's SNS client. Both the tenant senders and the default fallback are fire-and-forgetInlineSnsSender(neverBatchedSender), per Message is stuck in outbox with multiple tenants and durable messaging #2361 —TenantedSenderdoes not forwardRegisterCallback, so a batched sender under it would silently drop every message.AddTenant(string, Action<AmazonSimpleNotificationServiceConfig>)+ credentials overload, andTenantIdBehavior(TenantedIdBehavior).Scope: full per-tenant consumption composes with SQS #3316
This PR implements SNS broker-per-tenant on the send + subscription-provisioning side. Full per-tenant consumption (tenant SNS topic → tenant SQS queue → Wolverine listener) requires SQS tenant listeners, which live in the separate SQS broker-per-tenant PR #3316 (open, not in this base). Each SNS tenant already provisions its subscription/queue-policy through its own paired SQS client that lines up on the same account/region, so the two compose directly.
Tests
Unit (no broker/Docker) —
AmazonSnsPerTenantConfigurationTests(8) + the named-broker Uri-scheme assertion: tenant registration/compile/credential inheritance, paired-SQS alignment, tenant-aware default,BuildTenantSibling(deep-copies subscriptions),TenantIdBehaviordefault/override, and that a named broker's topic Uri scheme is the broker name.Integration (LocalStack, skip-guarded) —
end_to_end_with_named_broker: publish to a named SNS topic → subscribed SQS queue on the default broker → handler.AmazonSnsPerTenantConnectionTests: a tenant message publishes to the tenant region and a default message to the shared region (routing diverges byEnvelope.TenantId), plusTenantedSender+ per-tenant client resolution.LocalStack note
Like the SQS PR, tenant separation is simulated via signing region (
AuthenticationRegion) on the single LocalStack endpoint, which partitions SNS/SQS per region — LocalStack cannot enforce separate accounts. The two routing tests each provision an observation in only one region and assert the positive: LocalStack community SNS delivers unreliably when a topic of the same name exists in both regions at once, so the pair (tenant→west, default→east) proves divergence without a flaky cross-region negative. The sender wiring is additionally proven broker-free in the unit tests.Verification
dotnet build wolverine.slnx -c Release→ 0 errors, 0 warnings.🤖 Generated with Claude Code