GH-3708 (step 1): add EndpointMode.NativeAck as a default-closed, opt-in mode - #4032
Merged
Conversation
Step 1 of the build order on #3708: land the enum member and its transport opt-in gate ahead of any behavior, so the mode is never briefly default-open, and sweep every switch that would otherwise treat a fourth member as garbage. No transport opts in yet, so nothing can set this mode and no behavior changes. * NativeAck is gated on its own `supportsNativeAck` predicate rather than through `supportsMode()`. The latter is default-open -- the base returns true and three overrides are written as negations or a blanket true (TcpEndpoint's "mode != Inline", SignalRTransport's "mode != Durable", HttpEndpoint's "return true") -- so routing this member through it would have every un-audited transport silently accept a mode whose settlement model it cannot express. A separate default-false predicate cannot be leaked by an existing override. * `EndpointCollection.buildSendingAgent` gets a NativeAck arm. Mode governs both directions, so an endpoint that listens with native acks and is also used for replies landed on that method's fallthrough -- which threw InvalidOperationException with no message at all. It now maps to BufferedSendingAgent (NativeAck means "no outbox" on the sending side) and the fallthrough names the mode and the endpoint. * `LocalQueue.BuildAgent` rejects it with a real message rather than the bare fallthrough, the same treatment GH-4022 gave Inline: a local queue has no broker delivery to settle against. * `GlobalPartitionedMessageTopology.Mode()` rejects it -- partitioned slots bridge into a companion local queue, which has nothing to ack. * `ShouldEnforceBackPressure()` returns false: broker prefetch is what bounds this mode, so a BackPressureAgent is redundant, same as Inline. * `RabbitMqQueue.PreFetchCount` sizes the unacked window to cover every lane that can be busy at once -- partition slots when group-partitioned, otherwise MaxDegreeOfParallelism -- doubled so a lane never starves. `Endpoint.ModeIgnoresParallelism` needed no change: NativeAck does read MaxDegreeOfParallelism, so GH-3712's diagnostics stay correct as written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 23, 2026
Merged
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.
First step of the build order in #3708 (comment). Plumbing only.
No behavior changes.
EndpointMode.NativeAcknow exists, but no transport opts in, soEndpoint.Modecannot be set to it and nothing reaches the new code paths. The point of landing this separately is that the member is never briefly default-open, and that the switch sweep is reviewable on its own rather than buried in the receiver PR.The gate, and why it isn't
supportsModeNativeAckis gated on its own predicate:consulted by the
Modesetter independently ofsupportsMode(). That is deliberate.supportsMode()is default-open, and it is not only the base that returnstrue:Endpoint.supportsMode(base)=> trueTcpEndpoint.cs:36mode != EndpointMode.InlineSignalRTransport.cs:182mode != EndpointMode.DurableHttpEndpoint.cs:86=> trueNatsEndpoint(_ => false),GrpcEndpoint, control endpointsRouting the member through
supportsMode()would therefore have had a dozen transports silently accept a mode whose settlement model they cannot express — and would have required auditing every one of them. A default-false predicate cannot be leaked by an override that predates it. A transport opts in when it is ready; RabbitMQ does so in a later step.Switch sweep
C# does not require exhaustive
switchstatements, so none of these would have produced a compile error. Each was found by grep, not by the compiler.EndpointCollection.buildSendingAgent:448— gets aNativeAckarm.Modeis a single property governing both directions, so an endpoint that listens with native acks and is also used for replies landed on this method's fallthrough:throw new InvalidOperationException()with no message at all. It now maps toBufferedSendingAgent(the mode describes incoming settlement and says nothing about sending; "no outbox" is exactly Buffered), and the fallthrough now names the mode and the endpoint.LocalQueue.BuildAgent— rejects it with a real message rather than the bare fallthrough. This is the same treatment Local queue with ProcessInline() is accepted, then throws a message-less NotSupportedException from LocalQueue.BuildAgent() #4022 / GH-4022: reject ProcessInline() on a local queue instead of throwing at the first send #4027 just gaveInline; without it, a new enum member re-creates precisely that bug.GlobalPartitionedMessageTopology.Mode()— rejects it. Partitioned slots bridge into a companion local queue viaGlobalPartitionedReceiverBridge, and a local queue has no broker delivery to settle.PartitionProcessingByGroupId()directly on a native-ack listener is the supported shape.Endpoint.ShouldEnforceBackPressure()— false for this mode. Nothing is acked until the handler succeeds, so the broker's prefetch window is the back pressure and an in-processBackPressureAgentis redundant, same asInline.ListeningAgent.buildReceiverAsync— explicit arm throwing aNotSupportedExceptionthat names the issue, replacing what would otherwise be a bareArgumentOutOfRangeException. Unreachable today;NativeAckReceiverreplaces it in step 3.RabbitMqQueue.PreFetchCount— sizes the unacked window to cover every lane that can be busy at once (partition slots when group-partitioned, elseMaxDegreeOfParallelism), doubled so a lane never starves waiting on the next delivery.ServerlessEndpointsMustBeInlinePolicy— comment only. Coercing toInlineis correct for Serverless (no long-running process to hold an execution block), but it silently drops partitioning; that deserves to be said out loud once a transport can actually opt in, which is why it is noted rather than half-implemented here.Endpoint.ModeIgnoresParallelismneeded no change —NativeAckdoes readMaxDegreeOfParallelism, so GH-3712's diagnostics remain correct as written. There is a test pinning that.Tests
src/Testing/CoreTests/Configuration/native_ack_mode_gate.cs— 8 tests: default-closed for every endpoint type; an opted-in endpoint accepts it;TcpEndpointspecifically does not leak it through its negation-shaped override; local queues never accept it; back pressure off; parallelism still reported in diagnostics; global partitioned topologies reject it; and the GH-3712 listener validator leavesNativeAckalone, since partitioning plus real parallelism is the entire point of the mode.Verification
dotnet build wolverine.slnx -c Release -f net9.0— clean, 0 warnings, 0 errorsNot in this PR
NativeAckReceiver(step 3), #4011'sEnqueueDirectlyAsyncbranch (step 4), the RabbitMQ opt-in (step 5), andProcessInParallelWithNativeAcks()plus the GH-3712 doc/validator-message debt (step 6).🤖 Generated with Claude Code