Repository navigation
fix(broadcast): recover a stream consumer's pending entries and keep caught-up groups - #291
Open
cosmin-staicu wants to merge 1 commit into
Open
cosmin-staicu wants to merge 1 commit into
cosmin-staicu wants to merge 1 commit into
Conversation
cosmin-staicu
requested review from
alinahornet,
cosminvlad,
florin-munteanu-uipath,
litheon,
lucianaparaschivei and
razvalex
as code owners
October 24, 2026 07:11
cosmin-staicu
force-pushed
the
fix/stream-pending
branch
from
October 24, 2026 10:21
ebb88c4 to
9118b37
Compare
This was referenced Oct 24, 2026
cosmin-staicu
force-pushed
the
fix/stream-pending
branch
from
October 24, 2026 10:37
9118b37 to
8aad0ab
Compare
…caught-up groups
A timed-out stream read lost its messages. Redis moves the consumer group
past the entries of an XREADGROUP and puts them in the consumer's pending
list even when the answer never reaches the client, and the reader only
ever read new entries (">"), so those invalidations were never applied.
The reader now reads its own pending entries (from "0", then on from the
last one) when it starts and after every failed read, before reading new
entries again.
- A pending entry trimmed or deleted from the stream before it was read is
acknowledged and, once acknowledged, logged as a Warning and reported as
missed, which clears the local tier for the topic.
- An entry that cannot be decoded is acknowledged and dropped, not reported
as missed: a bad payload is not a lost invalidation. It is tracked as the
UndecodableEvent telemetry event and logged as a Warning once per 100.
- A read timeout is logged as a Warning once per 100, at Debug otherwise,
instead of as an Error each time.
The stream maintainer no longer deletes the consumer group of a live
subscriber on a quiet stream. A group with consumers counted as stale once
its last delivery was older than MaintainerTrimInterval, even when nothing
newer had been written. A group whose last-delivered-id has reached the
stream's last-generated-id is now healthy, and a quarantine it was under is
cleared.
The broadcast docs and the sample settings no longer describe Limit as
capping a read: neither the reader nor the publisher uses it.
A writer test that waited for a state lasting one poll interval now holds
the reader's retry until it has seen that state, so a slow runner cannot
miss it.
The writer tests that wait for, count or sequence the reader's reads now
mean its reads of new entries: its first read is now of its pending list,
which leaves the reader not yet in use. Their read mocks answer pending reads
with nothing (StreamReads.NewOnly), and the gap-check tests wait for the
first read of new entries.
Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
cosmin-staicu
force-pushed
the
fix/stream-pending
branch
from
October 24, 2026 10:52
8aad0ab to
6e3528f
Compare
|
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.



One PR for the stream reader fixes on
main. It folds in #290 (caught-up consumer groups) and #296 (theLimitdocs), which are closed.Pending entries after a failed read
When an
XREADGROUPtimes out on the client, Redis has already moved the group past the entries and put them in the consumer's pending list. The reader only ever read new entries (>) and never read its pending list, so those entries were never dispatched. Their invalidations were lost, and the node kept the old values in its local tier until they expired.Changes in
RedisStreamSubjectWriter:0before going back to>. A full batch continues from its last ID rather than restarting from0, so an entry that never gets acknowledged is not read in a loop. The consumer name (theSourceUri) stays the same for the writer's whole life, so the pending list it reads is its own, as long as theSourceUriis unique per process.XREADGROUPand doesn't move the group, so it skips the gap check. If it fails, it is handled the same way as any other failed read.Caching.RedisStreamSubjectWriter.DispatchEventsAsync.UndecodableEventtelemetry event and logged as a Warning once per 100, at Debug otherwise.Not changed: the background poll still uses the connection's request timeout. StackExchange.Redis has no per-command timeout on a shared connection.
Tests:
RedisStreamSubjectWriterIntegrationTests: entries handed out by a read whose answer was thrown away are dispatched and acknowledged against a real Redis.XREADGROUPcall. Their read mocks now go throughStreamReads.NewOnly, which answers pending reads with nothing. What they assert is unchanged.0until empty, deleted entries not acknowledged, timeouts still logged as errors, warning on every undecodable entry.-warnaserrorbuild is clean.Caught-up consumer groups on a quiet stream
The stream maintainer counted a consumer group with consumers as stale once its
last-delivered-idwas older thanMaintainerTrimInterval. It never checked whether anything newer had been written. On a stream with no traffic, every live group was quarantined, deleted afterMaintainerQuarantineInterval, and recreated by its reader on the nextNOGROUP. That repeated every cycle, with warnings,LogRecreatingTopic, andXGROUPcalls that sometimes timed out.A group whose
last-delivered-idhas reached the stream'slast-generated-idis now healthy, and a quarantine it was under is cleared.Tests:
-warnaserrorbuild is clean.LimitdocsThe settings tables in the broadcast guide and the settings reference, and the sample's full settings file, still described
RedisStreamsTopicOptions.Limitas the number of entries one read returns. Nothing readsLimit: the reader asks forPollBatchSizeentries, and the publisher trims withMaxLengthonly. #294 fixed the guide's prose. This fixes the three remaining places, so the docs no longer contradict each other.Docs only.
Flaky test
A_subscriber_joining_while_the_readers_recreate_is_pending_...waited for a state that lasts about one poll interval whileWaitUntilpolls every 20 ms, so a slow runner could miss it. The reader's retry now waits until the test has seen that state.