Skip to content

feat(mqtt): named-broker and broker-per-tenant support (GH-3307)#3319

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/mqtt-named-and-tenant-3307
Jul 6, 2026
Merged

feat(mqtt): named-broker and broker-per-tenant support (GH-3307)#3319
jeremydmiller merged 1 commit into
mainfrom
feat/mqtt-named-and-tenant-3307

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3307

Brings the MQTT transport up to parity with RabbitMQ / NATS / SQS on the two established multi-broker features. MQTT had neither before; this adds both.

Named broker

  • MqttTransport(string protocol) ctor (+ MqttTransport() : this("mqtt")), reached via TransportCollection.GetOrCreate<T>(BrokerName).
  • MqttTopic/MqttEndpointUri build the endpoint URI from parent.Protocol instead of a hard-coded mqtt:// literal — the key change so a named broker's endpoints get their own URI scheme and don't collide with the default broker.
  • New API: AddNamedMqttBroker(BrokerName, …) (builder + ManagedMqttClientOptions overloads), ListenToMqttTopicOnNamedBroker, ToMqttTopicOnNamedBroker; internal MqttTransport(this WolverineOptions, BrokerName?) accessor.
  • MqttHealthCheck.Protocol now follows the transport's Protocol.

Broker-per-tenant

  • New MqttTenant (own ManagedMqttClientOptions / IManagedMqttClient) and MqttTopicSender (a fire-and-forget ISender bound to a specific client; kept callback-free per Message is stuck in outbox with multiple tenants and durable messaging #2361 since TenantedSender doesn't forward RegisterCallback).
  • MqttTransport gains a per-tenant client cache + TenantedIdBehavior. Each tenant client is created in InitializeAsync with a unique ClientId (<id>-tenant-<tenantId>) — enforced even if the user pre-set one, because MQTT brokers kick a duplicate ClientId — and stopped in DisposeAsync. Listener bookkeeping is now per-managed-client so messages arriving on a tenant connection resolve that tenant's own (tenant-id-stamping) listener.
  • MqttTopic.CreateSender returns a TenantedSender when tenants exist; BuildListenerAsync builds a CompoundListener with one ReceiverWithRules([new TenantIdRule(tenant)]) listener per tenant client.
  • MqttTransportExpression.TenantIdBehavior(...) + AddTenant(...) overloads.
  • Adds Wolverine.MQTT to InternalsVisibleTo for TenantIdRule / ReceiverWithRules.

Tests

No Docker needed — two in-process LocalMqttBroker instances on free ports simulate two brokers.

  • Named broker (named_broker_tests.cs + registration tests): distinct transport per broker, broker-name URI scheme, publish lands on the named broker (raw MQTTnet subscriber) not the default and vice-versa, a consumed message is stamped with the named broker's scheme, and a full request/reply round-trips over the named broker.
  • Broker-per-tenant (mqtt_per_tenant_broker_tests.cs): a DeliveryOptions { TenantId } message lands on the tenant's broker not the shared one, default falls back to the shared broker, inbound tenant message consumed and stamped with TenantId, unique tenant ClientId, and the endpoint resolves a TenantedSender.

All 12 new tests pass; both existing compliance suites (47 tests) pass in isolation. dotnet build wolverine.slnx -c Release is clean (0 warnings, 0 errors).

Caveats (documented)

  • MQTT tenants are always own-connection (no topic-prefix model) — isolation is purely which broker the tenant client points at; retained messages are per-broker.
  • Unique ClientId per tenant is mandatory and enforced automatically.

Docs: adds "Connecting to Multiple MQTT Brokers" and "Broker per Tenant" sections to docs/guide/messaging/transports/mqtt.md.

🤖 Generated with Claude Code

Brings the MQTT transport to parity with RabbitMQ/NATS/SQS on the two
established multi-broker features:

Named broker
- MqttTransport(string protocol) ctor + MqttTransport() : this("mqtt");
  MqttTopic/MqttEndpointUri build the endpoint URI from parent.Protocol so
  a named broker's endpoints get their own URI scheme and never collide
  with the default mqtt:// broker.
- AddNamedMqttBroker + ListenToMqttTopicOnNamedBroker + ToMqttTopicOnNamedBroker;
  internal MqttTransport(this WolverineOptions, BrokerName?) accessor.
- MqttHealthCheck.Protocol now follows the transport's Protocol.

Broker-per-tenant
- New MqttTenant (own ManagedMqttClientOptions / IManagedMqttClient) and
  MqttTopicSender (fire-and-forget ISender bound to a specific client, kept
  callback-free per GH-2361).
- MqttTransport gains a per-tenant client cache + TenantedIdBehavior; each
  tenant client is created in InitializeAsync with a UNIQUE ClientId
  (<id>-tenant-<tenantId>) so the broker never kicks it, and is stopped in
  DisposeAsync. Listener bookkeeping is now per-managed-client so tenant
  connections resolve their own (tenant-id-stamping) listeners.
- MqttTopic.CreateSender returns a TenantedSender when tenants exist;
  BuildListenerAsync builds a CompoundListener with one ReceiverWithRules
  listener per tenant client.
- MqttTransportExpression.TenantIdBehavior + AddTenant overloads.

Adds Wolverine.MQTT to InternalsVisibleTo for TenantIdRule/ReceiverWithRules.

Tests (two LocalMqttBroker instances on free ports, no Docker): named-broker
routing + round-trip + request/reply, and per-tenant routing + inbound
tenant-id stamping + unique client-id + TenantedSender resolution. Docs:
"Connecting to Multiple MQTT Brokers" and "Broker per Tenant" sections.

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 75d491c into main Jul 6, 2026
26 checks passed
This was referenced Jul 9, 2026
This was referenced Jul 14, 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.

MQTT: add named-broker and broker-per-tenant support

1 participant