Fix live regression: NativeAck endpoints get an unregistered BatchedSender - #4070
Merged
Merged
Conversation
…tchedSender Live regression on main, introduced by #4061. Sending anything to a NativeAck endpoint on Redis fails outright with: System.InvalidOperationException: This sender has not been registered. Mechanism, end to end: * #4061 remapped EndpointMode.NativeAck from BufferedSendingAgent to InlineSendingAgent, to close the interceptor loss window. * InlineSendingAgent is `ISendingAgent, IDisposable` -- deliberately NOT an ISenderCallback -- so the registration in EndpointCollection.CreateSendingAgent (`sender is ISenderRequiresCallback && agent is ISenderCallback`) is skipped. * Every transport that uses BatchedSender chooses it by asking `Mode == EndpointMode.Inline`. NativeAck is not Inline, so those transports still build a BatchedSender. * BatchedSender throws on every send, ping and failure path when `_callback` is null. Redis adopted NativeAck in #4056, so this is broken on main today. RabbitMQ and Pulsar use their own senders rather than BatchedSender, which is exactly why CI stayed green: the only NativeAck integration tests that exist run on those two transports. The fix is one property rather than nine copies of a boolean, because the failure is silent and total, and because the next mode that sends inline should not require finding all nine sites again: public bool SendsInline => Mode is EndpointMode.Inline or EndpointMode.NativeAck; Applied to the sender gates in SNS, SQS, Azure Service Bus (topic and queue), Kafka, MQTT and Redis. GCP Pub/Sub is deliberately untouched: it has not adopted NativeAck (supportsNativeAck is false there), so it cannot reach this today, and that file is being actively edited under #4065/#4066. The Pub/Sub gate should move to SendsInline whenever #4052 adopts the mode. Red-baselined: with the Redis gate reverted to `Mode == EndpointMode.Inline`, three of the six Redis native_ack_mode tests fail with the exact "This sender has not been registered." exception; with SendsInline, 6/6 pass. Verified: pinned dotnet build wolverine.slnx -c Release -f net9.0 clean, CoreTests 2581 / 0 failed, Redis native_ack_mode 6/6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Aug 24, 2026
One conflict, and a welcome one: this branch and #4070 independently fixed the same GH-4061 sender-registration regression on the ASB gates. This branch used `Mode is EndpointMode.Inline or EndpointMode.NativeAck` inline at both call sites; #4070 introduced `Endpoint.SendsInline` and applied it across all seven transports. Kept main's central form. The explanation this branch carried in a comment at each site now lives in the xml-docs on SendsInline itself, which was the point of centralising it -- the per-transport version is exactly what invites the bug again the next time a mode is added. Verified after resolution: dotnet build wolverine.slnx -c Release -f net9.0 clean.
jeremydmiller
added a commit
that referenced
this pull request
Aug 24, 2026
#4070 fixed this regression independently and reached the same design -- an Endpoint.SendsInline predicate replacing the `Mode == EndpointMode.Inline` literal. It merged first, so everything this branch had in common with it is dropped in favor of main's version: * Endpoint.SendsInline -- main's declaration kept, mine removed. Note this auto-merged into TWO declarations of the same property (we added it in different places, so there was no textual conflict for git to report) and would not have compiled. * The Redis, SQS, SNS, Kafka and MQTT sender gates -- main's taken verbatim. * Azure Service Bus -- #4070 fixed it too, so the earlier "leave ASB to #4064" note is moot. What remains here is the delta #4070 does not cover: * The bootstrap guard in EndpointCollection.CreateSendingAgent. Main still has the silent `&&`, so a callback-requiring sender under an inline agent is still skipped quietly rather than refused. That is the part that made this expensive to diagnose: the throw lands on a block worker thread and surfaces only as a ~30s timeout far from the endpoint at fault. * Pub/Sub and HTTP, the two sender gates #4070 left on the literal. Inert today -- neither sets supportsNativeAck -- but they are the last two. * Five regression tests. #4070 shipped none, so nothing currently pins this behavior. Co-Authored-By: Claude Opus 5 <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.
Live regression on
main, introduced by #4061 (mine). Sending anything to a NativeAck endpoint on Redis fails outright:Mechanism
EndpointMode.NativeAckfromBufferedSendingAgenttoInlineSendingAgent, to close the global-partitioned interceptor's loss window.InlineSendingAgentisISendingAgent, IDisposable— not anISenderCallback. So the registration inEndpointCollection.CreateSendingAgentis skipped:BatchedSenderselects it by askingMode == EndpointMode.Inline. NativeAck is not Inline, so those transports still build one.BatchedSenderthrows whenever_callbackis null — on send, on ping, and on the failure path.Redis adopted NativeAck in #4056, so this is broken on
mainright now.Why no PR's CI caught it
RabbitMQ and Pulsar use their own senders rather than
BatchedSender, and they are the only transports with NativeAck integration tests. Redis has such tests, from #4056 — but #4061 branched frommainbefore #4056 merged, so #4061's CI ran against a tree that did not contain them.Both PRs were green. The combination is red. This is the classic stale-base gap, and it is worth knowing that nothing in the current workflow would have caught it: neither branch was wrong on its own.
The fix
One property rather than nine copies of a boolean:
Applied to the sender gates in SNS, SQS, Azure Service Bus (topic and queue), Kafka, MQTT and Redis.
It is centralised deliberately. The failure mode is silent and total — an endpoint that looks configured and cannot send a single message — and the per-transport form invites exactly this bug again the next time a mode is added. The xml-docs on
SendsInlinestate the coupling toISenderCallbackexplicitly, so the next person changing a sending-agent mapping can see why the property exists.GCP Pub/Sub is deliberately untouched. It has not adopted NativeAck (
supportsNativeAckis false), so it cannot reach this today, and that file is being actively edited under #4065/#4066. Its gate should move toSendsInlinewhen #4052 adopts the mode.Verification
Mode == EndpointMode.Inline, three of sixWolverine.Redis.Tests.native_ack_modetests fail with the exact"This sender has not been registered."exception. WithSendsInline, 6/6 pass.dotnet build wolverine.slnx -c Release -f net9.0— clean, 0 warnings, 0 errorsWolverine.Redis.Tests.native_ack_mode— 6/6Credit
Found by the agent working #4049 while investigating what looked like emulator contention. A 10-second assertion failure is not a timeout, so it traced the cause instead of writing it off — which is the only reason this was caught before release rather than by a user.
🤖 Generated with Claude Code