jonnify·redis /redis-patterns/time-delay-priority/delayed-queue/the-next-wake

Redis Patterns/R4/The delayed queue/The next wake

R4.01.3 · The delayed queue — dive 3

The next wake.

Do not busy-poll an empty set. Read the head, learn when the next job is due, and wait that long. EchoMQ holds one block per queue, woken by the admit that creates work.

Grounding: echo/apps/echo_mq/lib/echo_mq/metronome.ex · the single BLPOP emq:{q}:wake block · EchoMQ.Jobs.reap/2 + promote/3 per beat — the real wake, on the bus’s own pump.

Busy-poll versus the metronome block. Select a path to light it and read what the queue spends while idle — a herd of polls, or one block an admit releases.
an admitschedules a job wake tokenemq:{q}:wake metronomeone BLPOP LPUSH BLPOP <beat>
readout · Select a path. An admit’s LPUSH releases the metronome’s single block at once; otherwise the BLPOP times out on the bounded beat and runs the promote sweep anyway.

Static verdicts — admit → LPUSH emq:{q}:wake 1 releases the block now · beat → BLPOP emq:{q}:wake <beat> times out, then reap + promote run on the fallback.

§1

The cost of busy-polling

delayed-queue.md.txt · Avoiding Busy Waiting

Polling an empty set on a fixed tick wastes work. Every tick the worker runs the due-range, gets nothing back, and sleeps a fixed interval — then repeats, whether or not anything has changed. The shorter the tick the more round trips wasted on an empty set; the longer the tick the later a due job is noticed. A fixed poll cadence cannot be both responsive and cheap.

The waste is not a job that runs slowly; it is a worker spending round trips to learn nothing. On an idle set, every poll returns the same empty due head, and the interval between polls is latency added to whatever job lands next.

A fixed poll cadence trades responsiveness against cost: a short tick wastes round trips on an empty set, a long tick adds latency to the next due job.

§3

EchoMQ’s metronome: one block, woken by the admit

echo_mq/metronome.ex · the beat

EchoMQ does not run a poll loop per worker. It holds one block per queue in EchoMQ.Metronome — a single process that owns the only BLPOP emq:{<queue>}:wake <beat> and a registry of idle consumers. The beat is a bounded fallback; the real wake is an admit. When a job is admitted, the producer LPUSHes the wake token, and the single block returns at once. Each beat — whether woken by an admit or by the timeout — the metronome runs the reap and promote pumps before poking the idle consumers:

# EchoMQ.Metronome (metronome.ex) — the beat (real)
{:ok, _} = Jobs.reap(conn, queue)                 # return expired leases to pending
{:ok, _} = Jobs.promote(conn, queue, pump_batch)  # promote the due schedule head
# then hold the single block until the next admit or the beat timeout:
Connector.command(conn, ["BLPOP", Keyspace.queue_key(queue, "wake"), secs], beat_ms + 2_000)

So promotion runs on demand. An admit that schedules a job LPUSHes the wake token; the metronome’s single block returns; the beat runs promote/3, which sweeps any due head onto the pending set; and the metronome pokes the idle consumers, each running its byte-frozen claim once. The herd of pollers is gone — one connection blocks, one beat sweeps, and the wake comes from the work. The beat_ms (default 1_000) bounds how long an idle queue waits before a fallback sweep; pump_batch (default 100) caps how many due jobs one beat promotes.

Notes on ValkeyBLPOP blocks the connection until a list has an element or the timeout elapses, so one block per queue replaces a herd of pollers — the admit’s LPUSH on the wake token releases it at once — valkey.io/commands/blpop.

the bus · the metronome and the pump, in depth

The wake, the promote pump, and the consumer pool

This dive cites the metronome’s single BLPOP block and its per-beat reap/promote as proof the wake ships. The whole pump — the consumer registry, the fair-poke dispatch, the lifecycle controls — is the dedicated EchoMQ Queue course. This module teaches the wake; that course teaches the metronome that holds it.

the pattern

Do not poll an empty delayed set; read the head, compute when the next job is due, and wait until then rather than re-polling on a tick.

its EchoMQ application

EchoMQ.Metronome holds one BLPOP emq:{<queue>}:wake <beat> per queue, and each beat runs EchoMQ.Jobs.reap/2 then EchoMQ.Jobs.promote/3; an admit’s LPUSH on the wake token releases the single block at once, so the promote sweep runs on demand.

The head’s score is the next wake; holding one metronome block per queue beats polling on a tick, and an admit’s LPUSH on the wake token releases it the instant there is something to promote.

§4

When a deferred job must survive a crash

delayed-queue.md.txt · the durable floor

A scheduled job lives on the schedule set in Valkey, which is fast but volatile — if the process holding it dies, an in-flight schedule entry is only as durable as the bus’s own persistence. For work that must survive a crash, the deferred run is folded onto the durable substrate beneath the bus: an ETS head over a Valkey L2, a durable local page tier, and a remote object store, with durability a dial a system turns rather than a fixed cost on every enqueue.

the floor · the durability dial

The durable floor a scheduled job reaches

The mechanics of that floor — the single-writer page engine, the lazy reader, the create-only commit fence — are a course of their own. Open the /echo-persistence course where a scheduled or retried job reaches the durable floor, and /bcs · Persistence for the same substrate read as architecture. This module schedules and wakes the job; that course makes the schedule durable.

The schedule set is fast but volatile; for a deferred run that must outlast a crash, the durable floor turns the durability dial — the subject of /echo-persistence.

§5

References

sources · related

Sources

  • Valkey — BLPOP — the blocking pop the metronome holds on the wake token, so one block per queue replaces a herd of pollers.
  • Valkey — LPUSH — the ring an admit writes to the wake token to release the single block at once.
  • Redis — ZRANGE — read the schedule head and its score: the earliest job and when it is due.
  • Redis — Sorted sets — the data type behind the schedule set’s head the wake reads.

Related in this course