Redis Lists (work queue)¶
A list is a competing-consumers queue: a producer LPUSHes, consumers pop from the right, and each
entry goes to exactly one consumer (no fan-out, no replay). Simple mode is at-most-once (BRPOP, no
ack):
// Simple at-most-once work queue: BRPOP, no ack.
#[subscriber(RedisList::new("jobs"))]
async fn run_job(job: &Job) -> HandlerResult {
println!("running job {}", job.id);
HandlerResult::Ack
}
Reliable mode moves each entry to a processing list and removes it on ack (at-least-once), so a crashed handler does not silently lose its job:
// Reliable at-least-once work queue: the job moves to a processing list and is removed on ack.
#[subscriber(RedisList::new("jobs.reliable").reliable())]
async fn run_reliable_job(job: &Job) -> HandlerResult {
println!("running reliable job {}", job.id);
HandlerResult::Ack
}
Publish with broker.list_publisher() (LPUSH). Headers travel in the same frame as Pub/Sub: a
lossless binary frame by default, or a readable codec-serialized envelope when a codec is set on both
ends (.codec(JsonCodec)).
Orphan recovery¶
A dead consumer can strand a reliable entry on its processing list, since Redis lists have no native pending tracking. Opt into a recovery watchdog by naming a ZSET key (off by default):
// Reliable mode with orphan recovery: a dead consumer's in-flight job is returned to the queue. The
// recovery ZSET key is named explicitly, and min_idle must exceed the longest legitimate handler
// runtime (set it too low and a still-running job gets recovered and processed twice).
#[subscriber(
RedisList::new("jobs.recoverable")
.reliable()
.min_idle(Duration::from_secs(30))
.recovery_zset("jobs.recoverable.inflight")
)]
async fn run_recoverable_job(job: &Job) -> HandlerResult {
println!("running recoverable job {}", job.id);
HandlerResult::Ack
}
Each claim is recorded in the ZSET (score = claim time); a sweeper folded into the subscription's
read loop returns entries idle past min_idle to the main list, where a live consumer re-claims
them. Like the Streams reclaim path, min_idle must exceed the longest legitimate handler runtime,
or a still-running entry is recovered and processed twice. An optional recovery_ttl cleans up an
abandoned ZSET key but must exceed min_idle. Without recovery, Redis Streams remain the recommended
durable, recoverable path.
List publisher TTL¶
An idle list can be bounded with a key TTL: broker.list_publisher().ttl(Duration::from_secs(300))
re-arms a PEXPIRE on the list key on every publish, so an actively used queue never expires and
only an idle one lapses. It is off by default and per-key (the whole list), not per-entry - Redis
lists have no per-element expiry. Pub/Sub has no equivalent (PUBLISH stores nothing to expire), and
streams bound their size with trimming rather than a TTL.