Settlement
What ack, drop, retry and retry_after become on
NATS: an acknowledgement the JetStream server acts on, and copies the runtime publishes on Core.
The diagram as text
- Handler: ack(); retry(); retry_after(d); drop(); no outcome before ack_wait
- Sent: +ACK; -NAK; -NAK {"delay": …}; +TERM; nothing; On Core NATS every outcome reports AckError::Unsupported: the server settles nothing
- Server: finished, never sent again; sent again now, delivered + 1; held for d, then sent again, delivered + 1; ended, never sent again; sent again after ack_wait, delivered + 1
- ack() → +ACK
- +ACK → finished, never sent again
- retry() → -NAK
- -NAK → sent again now, delivered + 1
- retry_after(d) → -NAK {"delay": …}
- -NAK {"delay": …} → held for d, then sent again, delivered + 1
- drop() → +TERM
- +TERM → ended, never sent again
- no outcome before ack_wait → nothing
- nothing → sent again after ack_wait, delivered + 1
A JetStream delivery settles on the server. The answer goes to the delivery's acknowledgement
subject, and the consumer acts on it as Consumers draws. A retry_after puts
the delay into the negative acknowledgement itself. The server holds the message and delivers it
again on the same consumer, with the same stream sequence and a delivered count one higher.
Nothing is published, and no copy is made.
A Core delivery has nothing to settle. ack, retry and drop report AckError::Unsupported,
which the runtime logs. The server keeps nothing to deliver again, and the runtime brings a message
back only as a copy it publishes. That is
at-most-once delivery.
| Outcome | Core NATS |
|---|---|
ack, drop | AckError::Unsupported |
retry() | AckError::Unsupported; under a declared cap, a copy published at once |
retry_after(d) | a copy published after d |
| no outcome | nothing |
Retries
The diagram as text
- Below the cap: JetStream: -NAK with the delay, the server holds it; Core subject: a copy published to the subject; Core pattern: a copy to the subject the mount site names
- At the cap: Dead-letter subject: the delivery published as it arrived; No dead letter: +TERM on JetStream, nothing more on Core
- A delivery JetStream counts its deliveries; Core carries a header → The handler returns retry_after(delay), or retry() under a declaration: handled
- The handler returns retry_after(delay), or retry() under a declaration → max_attempts: is this attempt below the cap?: a retry
- max_attempts: is this attempt below the cap? → Below the cap: below
- max_attempts: is this attempt below the cap? → At the cap: at the cap
- A Core copy waits for its delay inside the process: a stop before the timer fires loses it
The cap is declared at the mount site with
max_attempts, and the first delivery counts
as one. On JetStream the count is the server's own delivered, so a delivery that ran past
ack_wait counts too. On Core the runtime counts the copies it published, in the copy's
x-ruststream-retry-count header.
Below the cap, each model brings the message back its own way. JetStream redelivers it from the
stream. On Core the runtime publishes a copy to the address the descriptor reports. A
CoreSubject reports its own subject. A
CoreWildcard matches many subjects and
publishes to none, so its registration names the subject
a copy goes to, or the app refuses to start.
At the cap, the delivery goes to the dead-letter subject
as it arrived, payload and headers unchanged. With no dead-letter subject declared, a JetStream
delivery is terminated with +TERM, and a Core one is let go.
The crate declares nothing to the server: max_deliver stays unset, because JetStream has no
place to send a message that ran out of deliveries. The runtime's path covers both models.