Skip to content

streaming: generation-qualified stream lifecycle and exact publication - #82

Merged
raphael merged 4 commits into
mainfrom
streaming-generations
Jul 25, 2026
Merged

streaming: generation-qualified stream lifecycle and exact publication#82
raphael merged 4 commits into
mainfrom
streaming-generations

Conversation

@raphael

@raphael raphael commented Jul 25, 2026

Copy link
Copy Markdown
Member

Contract

Streams bind lazily to one Redis-owned generation; every explicit recreation gets a distinct physical key, so stale handles can never touch a later incarnation. Every metadata mutation — publication, event removal, consumer-group recovery, consumer registration, keep-alive refresh, stale recovery, acknowledgement, destruction — verifies state + generation + physical_key atomically in Redis, which makes destroyed-generation metadata impossible to resurrect (the shipped state-only lifecycle fence could not prevent an unfenced keep-alive tick or membership append from recreating deleted maps).

New capabilities: idempotent AddOnce publication and side-effect-free Snapshot reads (generation-scoped, Lua-linearized), Redis-owned TTL/deadline retention (expiry applied in the same operation as the write; metadata cannot outlive data), and a fenced Redis-time stale-recovery lease for XAUTOCLAIM/stale-consumer cleanup.

Binding is write-once: a bound *Stream is an immutable capability for one exact generation, so post-bind reads need no locking. Consumer registration is one atomic script (consumer group entry + membership + initial keep-alive), removing the previous three-step attach and its rollback surface.

Compatibility

  • v1 option-struct fields keep name, type, and position (stable ordered prefix, enforced by a reflect-based pin plus keyed-literal compilation); new fields are append-only. Unkeyed cross-package literals were only guaranteed within the v1 patch line.
  • Exported Stream.MaxLen stays the immutable construction value (v1 semantics); the generation's canonical bound is tracked privately.
  • Destroy contract narrowed: an unbound handle for an absent name returns ErrStreamNotFound instead of silently succeeding. In-repo callers verified.

Acceptance

  • Full repo go test ./... -race against live Redis: green.
  • Fault injection: forced destroy + fenced-write rejection (TestDestroyedGenerationMetadataCannotBeRecreated), consumer rotation rollback, recovery cursor sharing, expired-stream recovery.
  • Diff-scoped review vs main completed; findings (dead helper, MaxLen post-bind write, order-pin gap) fixed in the follow-up commit on this branch.

Pre-existing non-blocking findings (max/delay shadowing in testing.go, BUSYGROUP string matching, etc.) are tracked on the wip/full-hardening backlog, not here.

raphael and others added 2 commits July 24, 2026 18:27
Streams bind lazily to one Redis-owned generation with a distinct physical
key per explicit recreation. Every metadata mutation - publication, event
removal, consumer-group recovery, consumer registration, keep-alive refresh,
stale recovery, acknowledgement, and destruction - verifies the exact
generation atomically in Redis, so destroyed-generation metadata can never be
resurrected by a concurrent sink. Adds idempotent AddOnce publication and
side-effect-free Snapshot reads, Redis-owned TTL/deadline retention, and a
fenced stale-recovery lease. Exported option structs keep their v1 fields as
a stable ordered prefix; unkeyed literals are pinned internally and keyed v1
construction is pinned for external compatibility.
Delete the orphaned rollbackStreamRegistration helper (atomic registration
made Redis rollback unnecessary), keep the exported Stream.MaxLen immutable
after construction so concurrent readers never race with generation binding
(the canonical bound lives in the private snapshot), and pin the v1 option
fields to their leading positions with a reflect-based prefix check that
keyed literals cannot provide.
raphael added 2 commits July 24, 2026 22:13
The hasher closure is shared by two nodes and invoked from concurrent
routing and rebalance goroutines; its job counter must be atomic. Latent on
main, exposed by the streaming lifecycle's timing changes under -race.
@raphael
raphael merged commit 6aa54da into main Jul 25, 2026
5 checks passed
@raphael
raphael deleted the streaming-generations branch July 25, 2026 16:24
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.

1 participant