Repository navigation
Provision BindFrom bindings for queues that opt in - #10335
Merged
PascalSenn merged 3 commits intoSep 5, 2026
Merged
Conversation
With a transport configured deny-by-default, a queue that opts in to provisioning and declares its binding via BindFrom never got that binding created on the broker. ConfigureQueueTopology carried the queue's AutoProvision onto the queue topology descriptor but not onto the binding it derives, the per-resource gate then fell back to the transport default, and the derived binding configuration was unreachable from user code. The queue existed, the consumer attached, and nothing was delivered, with no error raised anywhere. The same hole existed in the Azure Service Bus and Postgres transports for the subscriptions their BindFrom derives. The derived binding or subscription now resolves its provisioning as: an explicitly declared resource keeps its own setting, then the binding's own opt-in from the new BindFrom overload, then the queue's setting, then the transport default. The source exchange or topic is deliberately not inherited: it is owned elsewhere, and a deny-by-default host must not start declaring foreign resources. A binding provisioned against a missing exchange fails loudly at startup, which is the correct failure mode. The BindFrom overload takes a required bool. Omitting the argument already means inheriting, so the overload exists only to decide, and a default would choose for the caller; for RabbitMQ the opt-out was otherwise inexpressible, since DeclareBinding always creates a new binding rather than returning the derived one. Behavior changes, both consequences of the inheritance: - A deny-by-default transport now provisions the BindFrom bindings of queues that opt in. This is the reported defect. - A queue that opts out under a permissive transport now drags its BindFrom bindings out of provisioning with it. A host that relied on the transport default provisioning a binding for an externally managed queue can restore that with BindFrom(source, autoProvision: true). Breaking change: the Azure Service Bus and Postgres queue descriptor configurations store their BindFrom sources as configuration objects instead of bare Uris, so the derived subscription can carry a per-binding setting. The RabbitMQ example now declares the consumer's own binding with BindFrom on its queue and keeps DeclareBinding for the dead-letter binding, which has no local consumer. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
This was referenced Oct 25, 2026
Merged
This was referenced Nov 1, 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.
Problem
With a transport configured deny-by-default (
transport.AutoProvision(false)), a queue that opts in and declares its binding viaBindFromnever gets that binding created on the broker:The queue exists, the consumer attaches, the exchange exists, but the exchange-to-queue binding is absent, so nothing is ever delivered. No startup error, no runtime error, nothing logged.
BindFromwas effectively unusable for any consumer running the transport deny-by-default, forcing every binding into the explicit three-callDeclareBindingform.Root cause
ConfigureQueueTopologycarries the queue'sAutoProvisiononto the queue topology descriptor viaApplyQueueConfiguration, but not onto the binding it derives fromBindFrom. The per-resource gatebinding.AutoProvision ?? transportDefaultthen falls back tofalse, and the derived binding configuration was unreachable from user code, so there was no way to compensate.The Azure Service Bus and Postgres transports have the identical hole in the subscriptions their
BindFromderives, so the fix covers all three.Fix
The derived binding or subscription resolves its provisioning as:
DeclareSubscriptionis get-or-add, so a laterBindFrommust not override it),BindFromoverload,At each derivation site this is one expression:
The source exchange or topic is deliberately not inherited. It is owned elsewhere, and a deny-by-default host must not start declaring foreign resources; the explicit form this replaces (
DeclareBinding(...).AutoProvision(true)) also only opts in the binding. A binding provisioned against a missing exchange fails loudly at startup, which is the correct failure mode. Tests pin the exchange and topic staying unset.New overload
The
boolis required: omitting the argument already means inherit, so the overload exists only to decide, and a default would choose for the caller. For RabbitMQ the per-binding opt-out was otherwise inexpressible, sinceDeclareBindingalways creates a new binding rather than returning the derived one.Behavior changes
Both are consequences of the inheritance:
BindFrombindings of queues that opt in. This is the reported defect.BindFrombindings out of provisioning with it. A host that relied on the transport default provisioning a binding for an externally managed queue can restore that withBindFrom(source, autoProvision: true). Tests pin this direction too.💥 Breaking change — reviewer decision requested
AzureServiceBusQueueDescriptorConfiguration.SourceTopicsandPostgresQueueDescriptorConfiguration.SourceBindingschange fromList<Uri>to lists of configuration objects (Source+AutoProvision), mirroring the record RabbitMQ already had, so the derived subscription can carry a per-binding setting. The new interface methods on the three queue descriptors are additive but formally breaking for external implementors of those interfaces.What breaks what. The fix itself (the inheritance) is break-free on every transport; it only reads existing members. RabbitMQ's overload needed no config change, since its source-binding record already existed. The type change on the two sibling configurations is the price of offering the per-binding overload symmetrically.
BindFrom(...)call sites compile unchanged everywhere; only code that reads or writes those two config properties directly is affected, and migration is mechanical (.Add(uri)becomes.Add(new(...) { Source = uri }), reads becomex.Source).Measured blast radius. The Azure Service Bus transport landed on 2026-08-19 and its package has a few hundred downloads, so this surface is two weeks old. The Postgres
SourceBindingsproperty dates to June with moderate package adoption, but the affected member is one a consumer only touches when writing conventions or tooling against the descriptor configuration.Why this PR keeps it. The bare
List<Uri>was under-modeled from the start: RabbitMQ already needed a record for its routing key, and Azure Service Bus subscriptions have rules and filters in their plausible future, which would force this exact type change later, with more adopters and a compatibility promise in place. Pre-GA is the cheapest this break will ever be.The decision point. The packages ship as
16.6.2with no preview suffix. If 16.x is considered under a compatibility promise, the fallback is to keep the inheritance fix everywhere (break-free), keep the overload only on RabbitMQ, where the per-binding opt-out is otherwise inexpressible becauseDeclareBindingalways creates a new binding, and revert the record and overload on Azure Service Bus and Postgres. Their per-binding opt-out remains expressible through an explicitDeclareSubscription(...).AutoProvision(false), which the non-stomp tests in this PR pin either way. Happy to apply that reduction if versioning policy calls for it.Tests
17 new tests across the three transports covering the full matrix: inheritance of the opt-in, inheritance of the opt-out, unset queue leaving the transport default in charge, the overload overriding the queue in both directions, the explicit declaration surviving a later
BindFrom(ASB and Postgres, where the derived resource is get-or-add), and the source exchange or topic never inheriting.Reverting only the three transport descriptor changes fails exactly the three inheritance tests, one per transport, with everything else green.
Suites:
Mocha.Transport.RabbitMQ.Tests1856/1856,Mocha.Transport.AzureServiceBus.Tests1380 (8 pre-existing skips; one unrelated session-receive flake that also fails without this change),Mocha.Transport.Postgres.Tests1416/1416,Mocha.Tests4048/4048.Example
The RabbitMQ transport example now declares the consumer's own binding with
BindFromon its queue chain and keepsDeclareBindingfor the dead-letter binding, which has no local consumer, so the example teaches when each form applies.🤖 Generated with Claude Code