Repository navigation
GH-4059: say so when a publish rule overwrites a listener's EndpointMode - #4582
Merged
Merged
Conversation
EndpointMode is one property governing both directions, so on any transport where publishing and listening resolve to the same Endpoint object -- a RabbitMQ queue, a Redis stream, a Pulsar topic -- a sending-side SendInline() silently overwrote the listening side's mode, and vice versa. Two people hit this independently within an hour while building the Redis Streams and Pulsar transports (#4046, #4047). Endpoint now records ListenerRequestedMode and SubscriberRequestedMode alongside the outcome, so ListenerConfigurationValidator can see both requests and report the collision: which mode each side asked for, which one actually won, and that the answer depends on the order Wolverine applies the two configuration blocks. That recording is why the check lives in the validator rather than the Mode setter. Both sides are delayed configuration, so a setter guard catches one ordering and misses the reverse -- the same reasoning as GH-3712's parallelism clamp. A test runs both orderings for exactly that reason. ## Warning rather than refusal The issue offered warn or throw. "Send inline, receive durably" is a coherent thing to want, and with a single Mode property Wolverine cannot express it -- so throwing would refuse a configuration that is meaningful rather than mistaken, with no remedy but to give one of the two calls up. Separating the two into a distinct SendingMode is the real fix and needs its own migration story. Applies to UseDurableOutbox() and BufferedInMemory() as well; SendInline() is the loudest instance, not the only one. ## The Fatal that already existed now blames the right call The sharper half. When SendInline() reset a listener's mode to Inline and PartitionProcessingByGroupId() was configured, GH-3712's validator refused the host with "use ProcessInParallelWithNativeAcks() instead of ProcessInline()" -- advice pointing at a ProcessInline() call the reader never wrote, and, in the report's own case, telling them to make a change their listener had already made. The message now distinguishes the two causes: when the Inline came from a sending-side call it names SendInline() and the publishing side, and a test asserts it does NOT say "instead of ProcessInline()". When the listener genuinely asked for Inline, the original advice is unchanged. Nine tests, covering both orderings, both non-SendInline sending-side setters, the three cases that must stay quiet (sides that agree, a side that named no mode at all, a listen-only endpoint), and both halves of the partitioning message. The report's literal ProcessInParallelWithNativeAcks() combination is built directly against a native-ack-capable stub, since supportsNativeAck is opt-in per transport and the plain stub throws on the mode. CoreTests 3047 green. Pinned Release build of wolverine.slnx clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
This was referenced Sep 23, 2026
jeremydmiller
added a commit
that referenced
this pull request
Sep 24, 2026
… test suites (#4597) Extracted from @tmorejon's #4506, which found these while building a validator for GH-4059 and is the original work here. The core fix landed separately as #4582 with a WARNING rather than a rejection -- "send inline, receive durably" is a coherent thing to want that a single Endpoint.Mode cannot express -- but these 27 files are real misconfigurations either way, and #4582's warning now reports every one of them. Most are a `.SendInline()` on a publish rule for an endpoint whose listener separately asked for UseDurableInbox() or ProcessInParallelWithNativeAcks(); whichever configuration block Wolverine applied last silently decided the mode for both directions. The Redis dead letter tests are the interesting ones, and the reason this is worth landing rather than leaving as noise: they were RELYING on the bug. Their listener needs Inline, because only an Inline listener takes RedisStreamListener.MoveToErrorsAsync -- a buffered one dead-letters through TryBuildDeadLetterSender instead -- and it was silently getting that from the SendInline() on the publishing side sharing Endpoint.Mode. They now say ProcessInline() outright. Verified: Wolverine.Redis.Tests DeadLetterQueueTests 6 green, which is where these fixes overlap the GH-4559 rewrite of disabled_dead_letter_queue_should_not_create_dead_letter_stream (#4579). Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj Co-authored-by: Jorge L. Torres M <tmorejon.jl@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #4059.
What was wrong
EndpointModeis one property governing both directions, so on any transport where publishing and listening resolve to the sameEndpointobject — a RabbitMQ queue, a Redis stream, a Pulsar topic — this silently discards the listener's mode:Two people hit it independently within an hour while building the Redis Streams and Pulsar transports.
The collision is now reported
EndpointrecordsListenerRequestedModeandSubscriberRequestedModealongside the outcome, soListenerConfigurationValidatorcan see both requests and say which mode each side asked for, which one actually won, and that the answer depends on the order Wolverine applies the two configuration blocks.That recording is the whole reason the check can work. Both sides are delayed configuration, so a guard in the
Modesetter catches one ordering and misses the reverse — the same reasoning as GH-3712's parallelism clamp, and the placement the issue asked for. A test runs both orderings for exactly that reason.Applies to
UseDurableOutbox()andBufferedInMemory()too;SendInline()is the loudest instance, not the only one.Warning rather than refusal — a judgement call worth reviewing
The issue offered "warn or throw". I went with warn, and the reasoning is the part to check:
"Send inline, receive durably" is a coherent thing to want. With a single
Modeproperty Wolverine cannot express it, so throwing would refuse a configuration that is meaningful rather than mistaken, leaving no remedy but to give one of the two calls up. Option 2 in the issue — a distinctSendingMode— removes the collision at the root and needs its own migration story; that is where the refusal belongs.The Fatal that already existed now blames the right call
This is the half with teeth, and it closes the gap the issue names: GH-3712's validator "sees the endpoint after the overwrite and cannot tell 'the user asked for Inline' from 'the user's listener mode was clobbered by a publish rule'."
Before, that case refused the host with:
— advice pointing at a
ProcessInline()call the reader never wrote, and in the report's own configuration, telling them to make a change their listener had already made.It now names
SendInline()and the publishing side when that is what imposed Inline, and a test asserts the message does not say "instead of ProcessInline()" in that case. When the listener genuinely asked for Inline, the original advice is unchanged — also asserted.Tests
Nine, covering:
UseDurableOutbox()over an Inline listener, so the check is notSendInline()-specificPublishAllMessages().To(...)— the common case, and why this is not merely "Mode was assigned twice"), and a listen-only endpointThe report's literal
ProcessInParallelWithNativeAcks()combination is built directly against a native-ack-capable stub endpoint, becausesupportsNativeAckis opt-in per transport and the plain stub throws on that mode — the durable inbox stands in for the fluent-API version of the same case.Verification
CoreTests 3047 green. Pinned full-solution build clean:
dotnet build wolverine.slnx -c Release -f net9.0.🤖 Generated with Claude Code
https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj