Skip to content

Improve Redis stream reclaim and prefetch handling - #129

Draft
vvanglro wants to merge 1 commit into
taskiq-python:mainfrom
vvanglro:improve/stream-reclaim-prefetch
Draft

Improve Redis stream reclaim and prefetch handling#129
vvanglro wants to merge 1 commit into
taskiq-python:mainfrom
vvanglro:improve/stream-reclaim-prefetch

Conversation

@vvanglro

@vvanglro vvanglro commented Jul 29, 2026

Copy link
Copy Markdown

Summary

This PR improves RedisStreamBroker reliability and fairness by adopting stronger Redis Streams handling patterns: per-task-deadline reclaim, lock-free XCLAIM, listener-local prefetch backpressure, and faster handoff of buffered messages on listener close.

What changed

  • Replace lock-based pending recovery with timeout-aware XCLAIM recovery.
  • Reclaim messages using the task's own timeout label plus reclaim_timeout_grace, falling back to idle_timeout for messages without a timeout label.
  • Deprecate and ignore unacknowledged_lock_timeout; Redis XCLAIM min_idle_time now provides the atomic claim guard.
  • Add reclaim_interval so pending-message scans are throttled instead of running on every listen loop.
  • Add local prefetch/backpressure tracking so xread_count also caps delivered-but-unacknowledged messages for this listener.
  • Track only messages delivered by the current listener instance as protected from reclaim, so a restarted worker using the same consumer_name can still recover pending messages from its predecessor.
  • On listener close, claim entries that were fetched into the broker-local buffer but not yet yielded to taskiq to an internal abandoned consumer and stamp them as very idle, so the next reclaim sweep can recover them immediately.
  • Recreate missing consumer groups on NOGROUP during stream reads/reclaim.
  • Update README documentation for stream broker reclaim/backpressure/close behavior.

Why

The previous reclaim path used a single global idle_timeout plus a broker-side Redis lock. That can reclaim long tasks too early (for example, a task with a 30m timeout reclaimed at the default 10m idle_timeout) and recover short crashed tasks slowly (for example, a 5s task waiting for the default 10m idle_timeout). The lock also adds a failure point (unacknowledged_lock_timeout) and extra round-trips, while Redis XCLAIM min_idle_time already deduplicates claims atomically on the server.

The close-time handoff is deliberately limited to the broker's internal buffer: messages already yielded to taskiq may be executing, so they are not abandoned immediately to avoid duplicate execution during shutdown.

Behavior notes

  • Reclaim sweeps run before XREADGROUP within a listen iteration, so overdue pending messages take priority over new messages when a sweep is due.
  • reclaim_interval defaults to 30000 ms; set it to 0 to scan on every iteration.
  • Only messages held by the current listener instance are protected from reclaim; pending entries from a dead predecessor (even with the same consumer_name) are recoverable.
  • xread_count=None disables the prefetch cap.
  • Broker-local buffered entries are handed to an internal abandoned consumer on listener close; already-yielded entries still follow their normal ack/reclaim lifecycle.

Tests

Added coverage for:

  • task-timeout based reclaim
  • idle-timeout fallback reclaim
  • reclaim with shared consumer_name
  • xread_count backpressure
  • buffered-message handoff on listener close
  • NOGROUP self-healing

Validation run locally:

uv run ruff check taskiq_redis/redis_broker.py tests/test_broker.py
uv run mypy taskiq_redis/redis_broker.py tests/test_broker.py
uv run pytest tests/test_broker.py -k "not cluster and not sentinel" -q

Result: 13 passed, 7 deselected for the single-node Redis broker tests. Cluster/sentinel tests are unchanged and were not run locally because no Redis cluster/sentinel service is available in this environment.

Reference

The reclaim/backpressure design was informed by the Redis Streams broker implementation in sylvinus/dramatiq-redis-streams, especially its use of deadline-aware pending recovery, bounded local prefetching, and abandoned-buffer handoff on close.

@vvanglro
vvanglro force-pushed the improve/stream-reclaim-prefetch branch from 13d4bf1 to 2503827 Compare July 29, 2026 09:40
RedisStreamBroker reclaim was driven by a fixed idle_timeout and a
broker-side Redis lock, which both double-ran long tasks and recovered
short crashed tasks slowly. Replace it with per-task-deadline reclaim:

- Resolve reclaim deadline from the message's `timeout` label (via
  formatter.loads) plus reclaim_timeout_grace, falling back to
  idle_timeout for messages without a timeout label.
- Drop the autoclaim Redis lock; rely on XCLAIM min-idle-time for
  server-side atomic dedup (unacknowledged_lock_timeout is deprecated
  and ignored).
- Protect only messages held by the current listener instance from
  reclaim (tracked via a local delivered set), so a worker sharing a
  consumer_name with a dead predecessor still recovers its pending.
- Gate reclaim sweeps with reclaim_interval (default 30s) to avoid
  scanning pending on every listen iteration.
- Enforce prefetch backpressure: do not XREADGROUP while xread_count
  delivered-but-unacked messages are outstanding.
- Claim broker-local buffered entries to an internal abandoned consumer
  on listener close, so the next reclaim sweep can recover not-yet-yielded
  messages immediately.
- Recreate a missing consumer group on NOGROUP during xpending/xreadgroup.

Tests cover timeout-label reclaim, idle_timeout reclaim, shared
consumer_name reclaim, prefetch backpressure, buffered-message handoff on
listener close, and NOGROUP self-heal. README documents the new
reclaim/backpressure/close behavior.
@vvanglro
vvanglro force-pushed the improve/stream-reclaim-prefetch branch from 2503827 to ee60f08 Compare July 29, 2026 09:41
@vvanglro
vvanglro marked this pull request as draft July 29, 2026 09:59
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