Skip to content

feat(nats): expose JetStream DeliverPolicy on transport + listener#2711

Merged
jeremydmiller merged 1 commit intomainfrom
nats-jetstream-deliver-policy
May 8, 2026
Merged

feat(nats): expose JetStream DeliverPolicy on transport + listener#2711
jeremydmiller merged 1 commit intomainfrom
nats-jetstream-deliver-policy

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes the gap in the user question on Wolverine NATS — there was no way to make an auto-provisioned JetStream consumer use DeliverPolicy.New (or anything other than the NATS-server default of All) without pre-creating the consumer outside Wolverine. This adds the knob at both the transport-wide and per-listener level.

Why

JetStreamSubscriber.StartAsync builds the consumer config with AckPolicy.Explicit / MaxDeliver / AckWait hardcoded and DeliverPolicy left unset — so every auto-provisioned consumer falls through to NATS's DeliverPolicy.All and replays every message currently in the stream on first connect. That's rarely what a host wants when standing up a new listener against a long-running stream.

What changed

A nullable ConsumerConfigDeliverPolicy? at two levels:

  • Transport-wideJetStreamDefaults.DeliverPolicy. Configure once via UseJetStream(d => d.DeliverPolicy = …) and every auto-provisioned consumer inherits it.
  • Per-listenerNatsListenerConfiguration.DeliverFrom(...). Wins over the transport-wide default for a single endpoint.

Resolution lives on NatsEndpoint.EffectiveDeliverPolicy (endpoint > transport > null), computed at access time so configuration ordering is irrelevant. JetStreamSubscriber.StartAsync reads it and only writes to ConsumerConfig.DeliverPolicy when non-null — hosts that don't opt in see no behaviour change and the NATS-server default is preserved.

// Transport-wide default
opts.UseNats("nats://localhost:4222")
    .UseJetStream(js =>
    {
        js.DeliverPolicy = ConsumerConfigDeliverPolicy.New; // every consumer
                                                            // starts at "now"
    });

// Per-listener override (wins over the transport default)
opts.ListenToNatsSubject("orders.received")
    .UseJetStream("ORDERS")
    .DeliverFrom(ConsumerConfigDeliverPolicy.New);

The override applies only to consumers Wolverine itself auto-provisions. Pre-created consumers referenced by name through UseJetStream(streamName, consumerName) keep their existing config — that path already does GetConsumerAsync first and only falls through to CreateOrUpdateConsumerAsync on miss, so there's no behaviour change there either.

The endpoint's DeliverPolicy is also propagated when the endpoint clones itself for tenanted setups in CreateSender, so per-listener overrides flow through to per-tenant endpoints.

Test plan

  • NatsDeliverPolicyTests 11/11 on net9.0 — covers default-null, EffectiveDeliverPolicy resolution at every precedence point, listener-configuration round-trip, and parameterized cases for All / Last / New / LastPerSubject.
  • Existing NATS unit tests still pass (NatsEndpointUriTests, broker_role_tests).
  • No behaviour change when neither knob is set — ConsumerConfig.DeliverPolicy is left untouched and the NATS server default applies, exactly as before.
  • CI to confirm full integration suite (NatsTransportComplianceTests, BasicTests, etc.) — those need a NATS container which I don't have locally.

Docs

New Consumer Deliver Policy subsection under JetStream Configuration in docs/guide/messaging/transports/nats.md, with a code sample for both knobs and a table of the relevant ConsumerConfigDeliverPolicy values (including the OptStartSeq / OptStartTime caveat for ByStartSequence / ByStartTime, which require pre-creating the consumer since those supplemental properties have no listener-configuration surface here). vitepress build clean (17.29s).

🤖 Generated with Claude Code

…stener

Until now, when Wolverine auto-provisioned a JetStream consumer for a
listener it built the ConsumerConfig with `AckPolicy.Explicit` /
`MaxDeliver` / `AckWait` hardcoded and `DeliverPolicy` left unset —
which falls through to the NATS server default of `DeliverPolicy.All`.
For a new listener attached to a long-running stream that means every
message currently in the stream is replayed on first connect, which
isn't typically what hosts want. The only workaround was to pre-create
the consumer outside Wolverine and reference it by name.

Adds a `ConsumerConfigDeliverPolicy?` knob at two levels:

* **Transport-wide** via `JetStreamDefaults.DeliverPolicy`. Set it once
  on `UseJetStream(d => d.DeliverPolicy = ...)` and every auto-
  provisioned consumer under the transport inherits it.

* **Per-listener** via the new
  `NatsListenerConfiguration.DeliverFrom(ConsumerConfigDeliverPolicy)`
  method. Wins over the transport-wide default for a single endpoint.

Resolution lives on `NatsEndpoint.EffectiveDeliverPolicy` and is
computed at access time so override mutations performed during host
bootstrap are picked up regardless of ordering between transport and
listener configuration calls. `JetStreamSubscriber.StartAsync` reads
that property and applies it to the `ConsumerConfig` only when
non-null — when both endpoint and transport leave it null Wolverine
writes nothing and the NATS server default is preserved (no behaviour
change for hosts that don't opt in).

The override only applies to consumers Wolverine itself
auto-provisions. Pre-created consumers referenced by name through
`UseJetStream(streamName, consumerName)` keep their existing config
— matches the existing reuse-by-name behaviour in
`JetStreamSubscriber` (it `GetConsumerAsync` first, falls through to
`CreateOrUpdateConsumerAsync` only on miss).

The endpoint's `DeliverPolicy` is also propagated when the endpoint
clones itself for tenanted setups in `CreateSender`, so per-listener
overrides flow through to per-tenant endpoints.

Tests: 11/11 in new `NatsDeliverPolicyTests` covering:
* default null on endpoint + transport
* `EffectiveDeliverPolicy` null when neither is set
* fallback to transport when endpoint is null
* endpoint override wins over transport default
* `DeliverFrom(...)` round-trips through the configuration callback
* parameterized round-trip across `All` / `Last` / `New` / `LastPerSubject`

Docs: new "Consumer Deliver Policy" subsection under the JetStream
Configuration section of `docs/guide/messaging/transports/nats.md`,
including the `OptStartSeq` / `OptStartTime` caveat and a table of
the relevant `ConsumerConfigDeliverPolicy` values.
`vitepress build` clean.
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