Skip to content

docs: spec — heartbeat-based claim leasing for lib-data-stream-redis#73

Open
pditommaso wants to merge 2 commits into
masterfrom
spec/stream-heartbeat-claim
Open

docs: spec — heartbeat-based claim leasing for lib-data-stream-redis#73
pditommaso wants to merge 2 commits into
masterfrom
spec/stream-heartbeat-claim

Conversation

@pditommaso

Copy link
Copy Markdown
Contributor

Summary

Design spec (no code) for lib-data-stream-redis addressing a class of duplicate / churning message processing caused by claim-timeout being silently overloaded with two conflicting roles.

In RedisMessageStream, XAUTOCLAIM's only liveness signal is idle time, so it cannot distinguish a dead consumer from a slow-but-alive one. claim-timeout therefore doubles as both:

  1. the dead-consumer failover window, and
  2. the re-poll interval for not-yet-terminal commands (which never XACK, so their stream entry only gets revisited once idle crosses claim-timeout).

These conflict: a handler whose accept() runs longer than claim-timeout gets its message reclaimed and re-executed while the original is still running — duplicate, possibly cross-pod.

Approach

Lease a message to its consumer only while accept() is on the stack: a background daemon re-claims it to itself (XCLAIM … 0 <id> JUSTID, resets idle without transferring data) every claim-timeout/3. The instant accept() returns (true or false), the lease stops:

  • actively-running handler → never reclaimed mid-execution (no duplicate);
  • handler returned false (RUNNING, waiting) → not heartbeated → existing claim-timeout re-poll cadence unchanged;
  • pod dies mid-accept() → heartbeat dies with the JVM → real failover preserved.

Plus a bounded max-processing-time safety valve (default 15m): if a single accept() runs longer, the heartbeat stops refreshing that entry so it becomes reclaimable again — bounding a hung-but-alive handler (does not interrupt the handler thread).

Scope

  • All changes confined to RedisMessageStream + three additive default methods on RedisStreamConfigzero required downstream change; consumers just bump to 1.6.0.
  • Orthogonal to the lib-cmd-queue-redis execute/checkStatus threading model — this is purely message-ownership leasing in the stream layer.
  • LocalMessageStream untouched (no PEL, no claim concept).

The spec includes thread-safety analysis, a failure-mode table, a Testcontainers test plan, rejected alternatives (raise-the-knob, per-consumer liveness registry, ack-on-receipt + re-enqueue), and a risk register.

Spec file: docs/superpowers/specs/2026-06-30-lib-data-stream-redis-heartbeat-claim.md

Motivated by seqeralabs/sched#600.

🤖 Generated with Claude Code

pditommaso and others added 2 commits June 30, 2026 22:38
Design for decoupling claim-timeout's two overloaded roles (dead-consumer
failover vs. re-poll cadence) by leasing a message to its consumer while
accept() runs, plus a bounded max-processing-time safety valve.

Motivated by seqeralabs/sched#600.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
Add §4.2 with four scenario timelines (the bug today, heartbeat keeping a
blocking handler owned, a fast RUNNING poll with cadence preserved, and
pod-death failover) plus a summary table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
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