Skip to content

Fix live regression: NativeAck endpoints get an unregistered BatchedSender - #4070

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-4061/native-ack-sender-registration
Aug 24, 2026
Merged

Fix live regression: NativeAck endpoints get an unregistered BatchedSender#4070
jeremydmiller merged 1 commit into
mainfrom
gh-4061/native-ack-sender-registration

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Live regression on main, introduced by #4061 (mine). Sending anything to a NativeAck endpoint on Redis fails outright:

System.InvalidOperationException: This sender has not been registered.

Mechanism

  1. GH-3709: send inline from NativeAck endpoints to close the interceptor loss window #4061 remapped EndpointMode.NativeAck from BufferedSendingAgent to InlineSendingAgent, to close the global-partitioned interceptor's loss window.
  2. InlineSendingAgent is ISendingAgent, IDisposablenot an ISenderCallback. So the registration in EndpointCollection.CreateSendingAgent is skipped:
    if (sender is ISenderRequiresCallback senderRequiringCallback && agent is ISenderCallback callbackAgent)
    {
        senderRequiringCallback.RegisterCallback(callbackAgent);
    }
  3. Every transport that uses BatchedSender selects it by asking Mode == EndpointMode.Inline. NativeAck is not Inline, so those transports still build one.
  4. BatchedSender throws whenever _callback is null — on send, on ping, and on the failure path.

Redis adopted NativeAck in #4056, so this is broken on main right 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 from main before #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:

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.

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 SendsInline state the coupling to ISenderCallback explicitly, 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 (supportsNativeAck is false), so it cannot reach this today, and that file is being actively edited under #4065/#4066. Its gate should move to SendsInline when #4052 adopts the mode.

Verification

  • Red-baselined: with the Redis gate reverted to Mode == EndpointMode.Inline, three of six Wolverine.Redis.Tests.native_ack_mode tests fail with the exact "This sender has not been registered." exception. With SendsInline, 6/6 pass.
  • dotnet build wolverine.slnx -c Release -f net9.0 — clean, 0 warnings, 0 errors
  • CoreTests — 2581 total, 0 failed, 2 skipped
  • Wolverine.Redis.Tests.native_ack_mode — 6/6

Credit

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

…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
jeremydmiller merged commit f6f0cf6 into main Aug 24, 2026
39 checks passed
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>
jeremydmiller added a commit that referenced this pull request Aug 24, 2026
GH-4073: refuse the agent/sender mismatch at bootstrap, plus the gates and tests #4070 left open
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.

1 participant