Redis 5.0.2 restores consumer-specific stream history
Redis 5.0.2 fixes history reads that could return new messages or duplicate a later entry when an older payload had been deleted. The patches make history mode explicit and constrain payload lookups to an exact ID.
A consumer recovering its unfinished work asks a different question from a consumer requesting a new delivery. Redis 5.0.2 fixes cases where XREADGROUP could confuse those requests. It also repairs the lookup used to recover a pending entry whose original stream payload has disappeared.
The November 22 release entry assigns critical urgency to users of Streams with consumer groups. The version-pinned notes describe duplicate history results and history appearing when the consumer pending list is empty. Those symptoms have separate causes, though both arise where delivery bookkeeping meets stored stream data.
I like Redis when the command says which state the application intends to access. A pending-history read should refer to unfinished deliveries for that consumer. These fixes matter because recovery code depends on that distinction after an interruption, when receiving unrelated data is particularly difficult to interpret.
A group position cannot identify the requested mode
With XREADGROUP, the special > ID requests new messages through the group delivery position. A concrete ID instead requests the named consumer pending history after that position. The application expresses the distinction in its command, before Redis examines any stored entry.
The history-mode patch shows that the lower-level range function previously inferred history mode by comparing its starting ID with the group last-delivered ID. That numerical comparison did not preserve the original request meaning in every state. A low group position could cause an explicit history request to follow the ordinary stream-reading path.
The repair introduces STREAM_RWR_HISTORY. The command handler sets this flag when a group request contains an ID other than >, and passes it into the range-reply function. That function selects the consumer pending list when the flag is present. Request intent now travels explicitly through the internal API instead of being reconstructed from the relationship between two IDs.
The empty-history regression test makes the distinction concrete. It creates a stream with entries, creates a group at the beginning, and verifies that the pending list is empty. A history read must then return no entries, despite the stream containing data that the group has not delivered yet.
XADD events 1-0 kind created
XADD events 2-0 kind updated
XGROUP CREATE events processors 0-0
XREADGROUP GROUP processors worker-a COUNT 2 STREAMS events 0-0
XREADGROUP GROUP processors worker-a COUNT 2 STREAMS events >
This example assumes a new key. The first read has no pending deliveries to return. The second requests new work and can populate the consumer pending history. Repeating the first read after that delivery addresses the outstanding entries that now belong to worker-a.
The important state is the consumer pending list, not merely the stream length. A stream can contain data while a particular consumer has no unfinished deliveries. Treating those as equivalent would make a restart path unexpectedly consume new work while it believes it is replaying its own history.
Pending metadata can survive payload deletion
The second defect concerns a pending ID whose stored entry no longer exists. Retention can remove an entry after delivery but before acknowledgement. The group still records an unfinished delivery, while the stream no longer contains the fields associated with that ID.
The deleted-entry regression test constructs that situation with two deliveries. It appends and delivers the first entry, then appends a second while trimming the stream to length one. After delivering the second entry, the consumer has two pending records but only the second payload remains.
A history response should preserve those facts. It should identify the first pending delivery with a null payload and return the second entry with its surviving fields. Returning the second payload in place of the first would hide a retention loss and duplicate data in the response.
The exact-ID lookup patch changes one argument to the range helper. Previously, history lookup supplied the pending ID as the range start, left the end unbounded, and requested one result. That asks for the first surviving entry at or after the requested ID, which may be a different entry.
The corrected call supplies the same ID as both start and end. If that exact entry is absent, the helper returns no match and the history code can emit the pending ID with a null payload. The fix preserves identity while reporting the missing data. In the regression test, the expected response names both IDs, with empty fields for the first and the surviving field-value pair for the second. Checking the response identities makes the test stronger than checking that two results arrived.
A count of one does not make a range exact
The deleted-entry bug is easy to understand once the query bounds are explicit. Limiting a range to one result controls cardinality. It does not require that the result have the same identity as the lower bound. The missing upper bound was therefore a semantic error even though the caller expected only one entry.
This distinction matters in recovery tooling. A pending ID is a reference to a particular delivery. Replacing its missing payload with a neighboring entry produces a plausible-looking result that describes the wrong work. Preserving the ID and exposing the absent payload gives the application an accurate state to handle.
I regard that explicit absence as useful information. A deleted payload requires a policy about missing work, such as consulting another authoritative record. A duplicated later entry can conceal the need for that policy and make processing appear more complete than the stored history supports.
Neither correction makes retention depend on acknowledgement. The fixed consumer-history implementation still handles missing payloads because stream contents and pending deliveries remain separate. What changes is the accuracy of the response describing that state.
The two regression tests preserve different boundaries. One verifies that a history request stays within consumer-specific pending state even when the group position is low. The other verifies that a lookup for a pending ID cannot substitute a later surviving payload. Redis 5.0.2 restores those distinctions without requiring a new client command or changing the application syntax for asking for history.