Skip to content

GH-3708 (step 1): add EndpointMode.NativeAck as a default-closed, opt-in mode - #4032

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3708/native-ack-mode-gate
Aug 23, 2026
Merged

GH-3708 (step 1): add EndpointMode.NativeAck as a default-closed, opt-in mode#4032
jeremydmiller merged 1 commit into
mainfrom
gh-3708/native-ack-mode-gate

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

First step of the build order in #3708 (comment). Plumbing only.

No behavior changes. EndpointMode.NativeAck now exists, but no transport opts in, so Endpoint.Mode cannot 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 supportsMode

NativeAck is gated on its own predicate:

protected virtual bool supportsNativeAck => false;

consulted by the Mode setter independently of supportsMode(). That is deliberate. supportsMode() is default-open, and it is not only the base that returns true:

Site Current Would a 4th member pass?
Endpoint.supportsMode (base) => true yes — ASB, SQS, SNS, Pub/Sub, Kafka, MQTT, Redis, Pulsar, Stub, LocalQueue
TcpEndpoint.cs:36 mode != EndpointMode.Inline yes
SignalRTransport.cs:182 mode != EndpointMode.Durable yes
HttpEndpoint.cs:86 => true yes
RDBMS queues, NatsEndpoint (_ => false), GrpcEndpoint, control endpoints positive lists no ✔

Routing 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 switch statements, so none of these would have produced a compile error. Each was found by grep, not by the compiler.

  • EndpointCollection.buildSendingAgent:448 — gets a NativeAck arm. Mode is 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 to BufferedSendingAgent (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 gave Inline; without it, a new enum member re-creates precisely that bug.
  • GlobalPartitionedMessageTopology.Mode() — rejects it. Partitioned slots bridge into a companion local queue via GlobalPartitionedReceiverBridge, 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-process BackPressureAgent is redundant, same as Inline.
  • ListeningAgent.buildReceiverAsync — explicit arm throwing a NotSupportedException that names the issue, replacing what would otherwise be a bare ArgumentOutOfRangeException. Unreachable today; NativeAckReceiver replaces 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, else MaxDegreeOfParallelism), doubled so a lane never starves waiting on the next delivery.
  • ServerlessEndpointsMustBeInlinePolicy — comment only. Coercing to Inline is 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.ModeIgnoresParallelism needed no change — NativeAck does read MaxDegreeOfParallelism, 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; TcpEndpoint specifically 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 leaves NativeAck alone, 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 errors
  • CoreTests — 2529 total, 0 failed, 2 skipped

Not in this PR

NativeAckReceiver (step 3), #4011's EnqueueDirectlyAsync branch (step 4), the RabbitMQ opt-in (step 5), and ProcessInParallelWithNativeAcks() plus the GH-3712 doc/validator-message debt (step 6).

🤖 Generated with Claude Code

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>
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