feat(pulsar): named-broker and broker-per-tenant support (GH-3308)#3320
Merged
Conversation
Bring the Pulsar transport up to parity with RabbitMQ/NATS/Kafka on the two multi-broker features it previously lacked. Named broker: - PulsarTransport(string protocol) ctor (parameterless chains to it) so TransportCollection.GetOrCreate<T>(BrokerName) can instantiate a named instance whose Protocol doubles as its endpoints' URI scheme. - PulsarEndpointUri.Topic(topicPath, scheme) builds the endpoint URI from the transport Protocol instead of a hard-coded "pulsar://"; EndpointFor uses it. - AddNamedPulsarBroker + ToPulsarTopicOnNamedBroker + ListenToPulsarTopicOnNamedBroker. - PulsarHealthCheck reports the transport's own Protocol. Broker-per-tenant (RabbitMQ-style: each tenant owns its own transport/client): - New Internal/PulsarTenant with its own PulsarTransport/IPulsarClient, built in the parent's InitializeAsync (Pulsar connects there, not ConnectAsync) and disposed in DisposeAsync. - PulsarConfiguration.AddTenant(tenantId, Action<IPulsarClientBuilder>), AddTenant(tenantId, Uri serviceUrl), TenantIdBehavior(...). - transport.BuildSender / BuildListenerAsync fan out to a TenantedSender and a CompoundListener (per-tenant PulsarListener wrapped in ReceiverWithRules + TenantIdRule) when tenants are registered and the endpoint is TenantAware; the hot-tail PulsarReaderListener branch is preserved per tenant. PulsarSender is fire-and-forget (not ISenderRequiresCallback), so it is safe under TenantedSender (no GH-2361 silent-drop). Native-tenant collision: the Wolverine tenant id selects a whole CLUSTER (service URL + auth), never the native Pulsar tenant segment in persistent://{tenant}/{namespace}/{topic}. Documented in XML docs + guide. Tests: named-broker + tenant registration unit tests (CI-safe), a CI-safe named-broker round-trip integration test on the shared fixture, and a local-only two-cluster tenant connection test ([Trait Category=Integration], excluded from CIPulsar). Docs: "Connecting to Multiple Brokers" and "Multi-Tenancy / Broker per Tenant" sections with the native-tenant callout. 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
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 #3308
Brings the Pulsar transport (
Wolverine.Pulsar) up to parity with RabbitMQ / NATS / Kafka on the two multi-broker features it previously had neither of.Pulsar has its own native tenant segment in the topic hierarchy
persistent://{tenant}/{namespace}/{topic}(thepublicinpersistent://public/default/orders). Wolverine's broker-per-tenant "tenant" is an orthogonalEnvelope.TenantIdrouting concept. This PR keeps the framework-consistentAddTenant(tenantId, …)name, but the per-tenant differentiator is a whole cluster connection (service URL + auth viaIPulsarClientBuilder) — never the native tenant segment. Two Wolverine tenants normally use the same native tenant/namespace on different clusters. This distinction is called out prominently in the XML docs, the guide (a dedicated::: warning), andPulsarTenant's summary.Named broker
PulsarTransport(string protocol)ctor (the parameterless ctor chains to it) soTransportCollection.GetOrCreate<T>(BrokerName)can build a named instance whoseProtocoldoubles as its endpoints' URI scheme.PulsarEndpointUri.Topic(topicPath, scheme)builds the endpoint URI from the transportProtocolinstead of the hard-codedpulsar://;EndpointForuses it.PulsarEndpoint.Parsealready ignores the scheme.AddNamedPulsarBroker(BrokerName, Action<IPulsarClientBuilder>),ToPulsarTopicOnNamedBroker,ListenToPulsarTopicOnNamedBroker(applies the existingPulsarNativeResiliencyPolicy, mirroringUsePulsar).PulsarHealthChecknow reports the transport's ownProtocol.Broker-per-tenant (RabbitMQ-style: each tenant owns its own transport/client)
Internal/PulsarTenant— its ownPulsarTransport/IPulsarClient,Compile(parent)copies DLQ/retry defaults. Because Pulsar connects inInitializeAsync(notConnectAsync), each tenant client is built there and disposed inDisposeAsync.PulsarConfiguration:AddTenant(string, Action<IPulsarClientBuilder>),AddTenant(string, Uri serviceUrl)(convenience),TenantIdBehavior(TenantedIdBehavior).transport.BuildSender/BuildListenerAsync(modeled onRabbitMqTransport) fan out — when tenants are registered and the endpoint isTenantAware— to aTenantedSenderand aCompoundListener(per-tenantPulsarListenerwrapped inReceiverWithRules+TenantIdRule). The hot-tailPulsarReaderListenerbranch is preserved per tenant.PulsarEndpoint.CreateSender/BuildListenerAsyncdelegate to these.GH-2361
PulsarSenderis fire-and-forget — it does not implementISenderRequiresCallback— so it is safe underTenantedSender(no silent-drop regression). No Inline-sender switch was needed (unlike Kafka/SQS whose native senders use callbacks).Tests
CIPulsar):PulsarNamedBrokerTests(distinct transport per name, endpoint scheme == broker name,GetOrCreate<PulsarTransport>(name)),PulsarPerTenantConfigurationTests(AddTenantregisters aPulsarTenant,Compilecopies defaults,TenantAwareendpoint resolves aTenantedSender,TenantIdBehaviorsetter), andPulsarNamedBrokerIntegrationTests— a real round-trip on the shared fixture broker proving the consumed envelope is stamped with the named broker'ssecondary://scheme. 11/11 green.CIPulsar):PulsarPerTenantConnectionTests([Trait("Category","Integration")]) spins up a second Pulsar container as "cluster B" to prove trueTenantIdrouting + inbound stamping. A deterministic single-broker tenant test is not feasible: Pulsar isolates tenants by cluster (unlike NATS's subject-prefix model), and two competing durable subscriptions on one broker collide — which is exactly why broker-per-tenant requires distinct clusters.Build
dotnet build wolverine.slnx -c Release→ 0 warnings, 0 errors.Caveats
AddTenantaction runs against a freshPulsarClient.Builder()and must fully specifyServiceUrl/auth (same "fresh factory" caveat as RabbitMQ).🤖 Generated with Claude Code