You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Reader session: an open reader reads (2.5.1) - #55
Stacked on #54. Fixes a reader which stayed open and deaf for good after a recovery cut short by the network (seen in a long-running service after a power outage), and the review finding on #54.
The failure
MsgReader._reopen() removed the old subscription before the new one existed. When the JetStream API timed out in the middle (consumer info, then three attempts to create a consumer), the reader was left holding the unsubscribed handle while its consumer stayed alive on the server. From then on the health check passed (the consumer exists), every pull request kept that consumer alive, and the answers went to an inbox nobody listened on: Timeout after 2.0s waiting for server response, for ever.
What changes
A consumer and its subscription are one session, replaced as a whole, the new one first. A failed or cancelled replacement leaves the session in use untouched. A half-made session leaves no subscription behind.
A session is judged by the server's answers to the reader's own pull requests, not by "the consumer exists". The periodic consumer_info poll is gone: one JetStream API request per reader every 10 s less.
One retry authority. The inner three attempts with their own pauses are removed; RetryPolicy paces everything.
Only a missing stream is fatal. A 404 is classified by err_code: a missing consumer (10014) is ordinary life of ephemeral consumers, a missing stream (10059, or the client library's own "no stream for subject") is a deployment error, as before.
A pull request to a deleted consumer is answered with status 503 by nats-server 2.14 and with nothing at all by 2.10.x. The reader handles both: after an unanswered pull it asks about the consumer once, to tell an expired consumer (replaced without an error) from lost answers (a failure, paced by the retry policy).
Tests
New tests/test_reader_session.py, 10 cases: lost subscription, lost subscription while no consumer can be created, failed replacement of a working session, cancelled replacement, half-made session, expired consumer under OnError.RAISE, consumer-level 404, missing stream at open, deadline of a recovery attempt.
Full suite: 457 passed on nats-server 2.10.22, 2.10.7 (container) and 2.14.0.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Replacement-session cleanup can delete a durable consumer still used by the new session, invalidating the replacement and potentially losing the durable cursor.
This PR strengthens reader recovery for version 2.5.1, building on #54’s deadline work to prevent interrupted recovery from leaving readers open but unable to receive messages.
Changes:
Replaces consumer/subscription pairs together and checks session health through pull responses.
Centralizes retries, bounds recovery attempts, and distinguishes missing consumers from missing streams.
Adds recovery regression tests and updates documentation and release metadata.
File
Description
tests/test_resl_consumer_expiry.py
Updates comments to describe session replacement.
tests/test_reader_session.py
Adds session recovery, cancellation, and deadline tests.
tests/test_delivery_policies.py
Uses the new session-replacement method.
serverish/messenger/msg_reader.py
Implements session lifecycle, response-based health checks, and recovery deadlines.
serverish/connection/nats_errors.py
Distinguishes missing streams from missing consumers.
README.md
Adds the 2.5.1 release notes.
pyproject.toml
Bumps the package version to 2.5.1.
doc/messenger.md
Documents automatic recovery and fatal configuration errors.
Unexpected pull rejections cause an unpaced retry loop
serverish/messenger/msg_reader.py:755
A persistent unexpected closing status such as a 409 rejection returns an empty successful batch, so read_batch() calls recovered() and can immediately repeat the pull after its blocking fetch is also rejected. This creates a round-trip-speed polling loop that bypasses retry pacing and total_timeout. Raise an error for these unexpected statuses so OnError and RetryPolicy handle the rejection.
Consumer capacity exhaustion makes reconnect recovery fatal
serverish/messenger/msg_reader.py:947
Creating a replacement ephemeral consumer requires a spare slot, so a reconnect or lost subscription cannot recover when the stream is already at max_consumers (for example, one reader on a stream limited to one consumer). JetStream rejects creation with BadRequestError, which stops the reader as a configuration error even under OnError.WAIT. Add a capacity-aware recovery path, such as safely rebinding the existing consumer, without making the temporary need for an extra slot fatal.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Named ephemeral readers are omitted from delete-before-create handling, so recovery can either reject a delivery-policy change or delete the consumer used by the replacement session.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because session-recovery edge cases can permanently skip queued messages, block consumer replacement, and reset retry budgets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #54. Fixes a reader which stayed open and deaf for good after a recovery cut short by the network (seen in a long-running service after a power outage), and the review finding on #54.
The failure
MsgReader._reopen()removed the old subscription before the new one existed. When the JetStream API timed out in the middle (consumer info, then three attempts to create a consumer), the reader was left holding the unsubscribed handle while its consumer stayed alive on the server. From then on the health check passed (the consumer exists), every pull request kept that consumer alive, and the answers went to an inbox nobody listened on:Timeout after 2.0s waiting for server response, for ever.What changes
consumer_infopoll is gone: one JetStream API request per reader every 10 s less.RetryPolicypaces everything.err_code: a missing consumer (10014) is ordinary life of ephemeral consumers, a missing stream (10059, or the client library's own "no stream for subject") is a deployment error, as before.RetryPolicy.total_timeoutof a reader bounds an attempt to recover, not only the pauses between attempts (review finding on Deadlines, cancellation and the reader session, Python 3.11+ (2.5.1) #54).Server versions differ
A pull request to a deleted consumer is answered with status 503 by nats-server 2.14 and with nothing at all by 2.10.x. The reader handles both: after an unanswered pull it asks about the consumer once, to tell an expired consumer (replaced without an error) from lost answers (a failure, paced by the retry policy).
Tests
New
tests/test_reader_session.py, 10 cases: lost subscription, lost subscription while no consumer can be created, failed replacement of a working session, cancelled replacement, half-made session, expired consumer underOnError.RAISE, consumer-level 404, missing stream at open, deadline of a recovery attempt.Full suite: 457 passed on nats-server 2.10.22, 2.10.7 (container) and 2.14.0.
🤖 Generated with Claude Code