natsmsg: scope the DLQ copy's dedupe key, and strip the TTL and schedule directives

main

Commit

nodo3w ago

Two defects in the capture copy, both found reviewing the header filter that landed in 3129b67.

THE DEDUPE KEY WAS A SILENT-LOSS PATH. That commit deliberately carried the original's Nats-Msg-Id onto the copy, reasoning that it dedupes the re-capture after a failed Ack. The dedupe it actually buys is not that narrow: JetStream matches on the ID alone, STREAM-wide across every subject in the DLQ. Two DIFFERENT originals whose producer derived the same ID — an entity key rather than an event key is enough — collapse onto one record inside the duplicate window. The second publish returns a SUCCESSFUL PubAck with Duplicate set, this code reports the capture as done, and the caller acks an original whose copy was never stored. A message gone with no DLQ record: the exact outcome the capture path exists to make impossible, reached through the code that implements it.

The copy now carries its own key, derived from the original's stream and stream sequence. That keeps the wanted dedupe — a re-capture of the SAME message presents the same origin identity and collapses onto its existing record — while making a cross-message collision impossible, since no two originals share a (stream, sequence). The publisher's key is not discarded, just relocated to Nats-Dlq-Origin-Msg-Id, because a replay tool restoring the message upstream needs it. Without metadata there is no identity to build a key from, so the copy goes out with none: a re-capture then leaves two records, which is the right way to be wrong here.

THE STRIPPED SET WAS INCOMPLETE. Nats-TTL was missed: carried onto a DLQ that allows per-message TTL it makes the captured record expire on the SOURCE stream's retention terms, and onto one that does not it fails the capture outright — either way the original is acked against a copy that is not durable on the DLQ's own terms. Auditing the rest of the current header set turned up more: the Nats-Schedule family (expression, target, source, TTL, time zone) would turn the captured record into a scheduled publish, and Nats-Schedule-Target would deliver it somewhere else entirely. All are stripped now.

Left alone deliberately: the server-set republish and direct-get headers (Nats-Stream, Nats-Sequence and friends). They are provenance, not directives, and a message that was itself republished carries real information in them.

The fake publisher now dedupes the way the broker does — on the ID alone, stream-wide, success-with-Duplicate and storing nothing — because without that it cannot tell deduping one message twice from collapsing two. Against the previous behaviour the new test fails with one stored record for two distinct messages.

Two assertions from 3129b67 pinned the old behaviour and moved with it.

Refs ENT-1601, ENT-1535, ENT-1492.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_018qXhU99D2Sxaa32yZWwYRP Entire-Checkpoint: b4c990fd0ecf

Checkpoints

Reconcile jsconsumer Retry Schedule After Broker Semantics

Claude CodeOpus 5.[1m]
View session
Checkpoint 1