Skip to content

Skip stale JetStream pull statuses in paced consumer - #25

Open
i-surkov wants to merge 3 commits into
masterfrom
fix/paced-stale-pull-terminus
Open

Skip stale JetStream pull statuses in paced consumer#25
i-surkov wants to merge 3 commits into
masterfrom
fix/paced-stale-pull-terminus

Conversation

@i-surkov

@i-surkov i-surkov commented Jul 31, 2026

Copy link
Copy Markdown

Problem

The engine arms its local window deadline at pull publish; the server starts the same
expiresIn clock at receipt. On an idle consumer the client therefore always times out first,
and the server's routine 408 for the just-expired pull lands at the head of the next window —
indistinguishable from a dead consumer answering instantly, so it trips the 500 ms
EarlyEmptyGuard: a non-draining sleep (delaying anything published meanwhile by up to 500 ms)
plus a redundant second pull, every idle window.

Fix

Match statuses to the pull they answer. jnats publishes every pull request with a distinct reply
subject and the server addresses that pull's statuses to it — jnats's own sync fetch uses
exactly this equality check; the dispatcher path just never had it. The transport now takes the
subject from the internal _pull call that computes it (pullReturningSubject in the
io.nats.client.impl package object, alongside the existing NatsKeyValue accessor) and skips
any status that does not answer the current pull. Genuine early termini carry the current pull's
subject and still hit the guard.

Same diagnosis as #22, different mechanism: removes the stale-status class instead of
compensating the 408 case with a timing slack — no RTT assumption, no change to stop/reconnect
timing. No API or configuration change; the callback engine is unaffected.

Summary by CodeRabbit

  • Bug Fixes

    • Improved paced message consumption by correctly matching pull responses and ignoring stale timeout responses.
    • Prevented unnecessary duplicate pulls and idle-window delays when streams are empty.
    • Ensured buffered messages are delivered promptly and in order without unnecessary waiting.
  • Documentation

    • Updated design documentation to describe pull-response tracking, stale-status handling, and idle-window behavior.
  • Tests

    • Added regression coverage for stale responses, pull timing, message ordering, and empty-stream behavior.

…tale pull statuses

The engine arms its local window deadline when a pull is published, while the
server starts the same expiresIn clock only on receipt, so on an idle consumer
the client always timed out first and the server's routine 408 for the
just-expired pull arrived at the head of the next window. There it looked like
a terminus arriving a full window early - a dead consumer answering instantly -
and tripped the 500ms EarlyEmptyGuard: a non-draining guard sleep (delaying
anything published meanwhile by up to 500ms) and a redundant second pull, every
window, on a perfectly healthy consumer.

Statuses are now matched to the pull they answer: jnats publishes every pull
request with a distinct reply subject, and the transport takes that subject
from the internal jnats call that computes it (pullReturningSubject in the
io.nats.client.impl package object, following the existing NatsKeyValue
precedent there), skipping any status that does not answer the current pull -
the same subject matching jnats's own sync fetch path uses. A stale status can
no longer end (or fail) the live pull's window, whatever the round trip or
scheduling delays. Genuine early termini (e.g. a deleted consumer answering
the fresh pull instantly) still carry the current pull's subject and still hit
the guard.

No API or configuration change; stop/reconnect timing is unchanged; the
callback engine is unaffected.
@stasimus stasimus closed this Aug 5, 2026
@stasimus stasimus reopened this Aug 5, 2026
@stasimus stasimus closed this Aug 5, 2026
@stasimus stasimus reopened this Aug 5, 2026
@stasimus stasimus closed this Aug 5, 2026
@stasimus stasimus reopened this Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9dbbbfc-e5d5-4898-a9a0-61d4594a3a3d

📥 Commits

Reviewing files that changed from the base of the PR and between fbc389a and f599099.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/design/2026-07-13-paced-buffered-consumers.md
  • jetstream/src/main/scala/com/evolution/natseffect/jetstream/impl/BufferedPullTransport.scala
  • jetstream/src/main/scala/com/evolution/natseffect/jetstream/impl/PacedPullEngine.scala
  • jetstream/src/main/scala/io/nats/client/impl/package.scala
  • jetstream/src/test/scala/com/evolution/natseffect/jetstream/BufferedPullTransportSpec.scala
  • jetstream/src/test/scala/com/evolution/natseffect/jetstream/PacedOrderedConsumerContextSpec.scala
  • jetstream/src/test/scala/com/evolution/natseffect/jetstream/PacedPullEngineSpec.scala

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Paced pull responses now correlate with their originating reply subjects. Stale timeout statuses are skipped. The engine polls buffered directives before deadline-bound waits. Tests and design documentation cover the new behavior.

Changes

Paced consume engine

Layer / File(s) Summary
Pull response correlation
jetstream/src/main/scala/.../impl/package.scala, jetstream/src/main/scala/.../impl/BufferedPullTransport.scala
Pull execution records each reply subject. Status messages terminate only the matching pull; stale statuses produce Directive.Skip.
Buffered directive draining
jetstream/src/main/scala/.../impl/PacedPullEngine.scala
ActiveSubscription exposes tryNext. drainWindow processes buffered messages before deadline-bound waiting.
Regression coverage and documentation
jetstream/src/test/scala/..., docs/design/..., CHANGELOG.md
Tests cover stale statuses, buffered delivery, deadline handling, and one pull per idle-window expiry. Documentation describes the updated behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f5990

The change is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant PacedPullEngine
  participant BufferedPullTransport
  participant JetStreamSubscription
  participant MessageHandler
  PacedPullEngine->>BufferedPullTransport: request pull
  BufferedPullTransport->>JetStreamSubscription: pullReturningSubject(options)
  JetStreamSubscription-->>BufferedPullTransport: reply subject
  BufferedPullTransport-->>PacedPullEngine: classified directive
  PacedPullEngine->>MessageHandler: handle buffered Deliver directive
  MessageHandler-->>PacedPullEngine: directive outcome
Loading

Suggested reviewers: devilab

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: skipping stale JetStream pull statuses in paced consumers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (8 skipped: 8 unsupported.)
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/paced-stale-pull-terminus

Comment @coderabbitai help to get the list of available commands.

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.

3 participants