R3.01.3 · dive 3
The in-flight list.
The in-flight list is not a buffer jobs pass through. It is a ledger: at any instant it holds exactly the jobs being processed right now.
A job moved into active stays there for the whole duration of the work, and only
an explicit acknowledgement removes it. So the list is a live record of in-flight work — and a worker
that dies mid-job leaves its job sitting in the record, found and returnable. EchoMQ scores each member at its
lease deadline, so the score names when a job is due back; the ack runs only when the work is truly done.
Select a worker dead and read where its job stays.
The list is a record, not a buffer
reliable-queue.md.txt · the ledgerA job moved into active does not pass through — it stays there for the entire duration of
the work, and only an explicit acknowledgement removes it. So active is a live record: every job
in it is a job a worker has taken and not yet finished. Reading the list answers a question that matters for
reliability — what work is in flight right now — without any extra bookkeeping.
emq:{queue}:active → [ job 7, job 6 ] # two jobs in flight, each scored at its lease deadline
This is what makes a crash recoverable. When a worker dies between taking a job and acknowledging it, the job
is still in active. Nothing was lost, because nothing removed it. A recovery process reads the set
and finds the parked job, exactly because the set is a record of in-flight work and not a transient buffer.
The in-flight list is a live record of work in progress; a crashed worker leaves its job in the record, recoverable, because only an explicit ack removes it.
Per-worker lists, or a lease score
echo_mq/jobs.ex · reap/2One shared in-flight list works, but it cannot tell which worker holds which job. The classic LIST form gives
each worker its own in-flight list: processing:worker1, processing:worker2. A job in
processing:worker1 is held by worker 1. When worker 1 dies, a recovery monitor reads worker 1's
list, finds the parked jobs, and returns them to the waiting list for another worker —
LMOVE processing:worker1 work_queue RIGHT RIGHT. Per-worker lists make the attribution direct: the
list name is the worker.
A real engine refines the death signal beyond "how long has the job sat here." EchoMQ scores each member of
emq:{queue}:active at its lease deadline on the server clock; a member whose score has passed
now is a worker that stopped renewing, and EchoMQ.Jobs.reap/2 returns it to
pending. The lease score is the death signal, so EchoMQ needs no per-worker list — the
ledger's own score names the deadline. R3.03 is the count-thresholded sweep on top.
- per-worker list
- Each worker has its own in-flight list (
processing:workerN), so a recovery monitor attributes a parked job to the worker that holds it. - lease score
- EchoMQ scores each
activemember at its due-back time; a score pastnowis the death signal, no per-worker list needed. - recovery ledger
- The in-flight set read as a record: exactly the jobs in flight, each with its deadline, found and returnable after a crash.
Name when each parked job is due back — a per-worker list, or a lease score — so a recovery sweep returns it; EchoMQ uses the lease score as the death signal.
The ack removes it only when done
reliable-queue.md.txt · complete/4A job leaves the in-flight list with one command: LREM active 1 job — remove one occurrence
of this job from the list. The discipline is when it runs: only after the work is truly complete. Run
it too early — before the side effect is durable — and a crash after the ack but before the effect
loses the job for real, because the ledger no longer holds it. Run it after the effect, and a crash before the
ack leaves the job parked, recoverable, and redelivered.
job = RPOPLPUSH wait active # take the job into the in-flight ledger process(job) # do the work; durable side effect lands here LREM active 1 job # ack: remove from the ledger ONLY now
That ordering is what makes delivery at-least-once rather than at-most-once. A job is delivered again if the worker died before the ack, and delivered once if the ack landed — never zero times. The cost is that a worker which finished the work and died before the ack redelivers the job, which is why R3.02 makes the consumer idempotent. This dive's job is the ledger and the ack; the redelivery cost is the next module's. Slide the crash point and read where the ack ordering protects the job.
The ack is one LREM, and its timing is the rule: remove the job only after the work is
durable, which is what makes delivery at-least-once.
In EchoMQ — the in-flight ledger under a lease
echo_mq/jobs.ex · stalled.exEchoMQ's in-flight list is the emq:{queue}:active sorted set, built by
EchoMQ.Keyspace.queue_key(queue, "active"), real in echo/apps/echo_mq. Each member is
scored at its lease deadline on the server clock, so the score is both the in-flight marker and the recovery
clock. The ack is EchoMQ.Jobs.complete/4: it is token-fenced on the row's attempts
value, so only the worker that holds the current lease can retire the job — a worker whose lease was
reaped and re-claimed by another worker is refused, and a redelivered completion is a no-op. A crash leaves the
id in active, and EchoMQ.Jobs.reap/2 returns any member past its lease score to
pending.
-- the ack and the recovery clock (echo/apps/echo_mq/lib/echo_mq/jobs.ex) -- complete/4: token-fenced on attempts, only the lease holder may retire the row local att = redis.call('HGET', KEYS[2], 'attempts') -- KEYS[2] = the job row if att ~= ARGV[2] then return redis.error_reply('EMQSTALE complete token mismatch') end redis.call('ZREM', KEYS[1], ARGV[1]) -- KEYS[1] = emq:{queue}:active, remove from the ledger -- reap/2: any member whose lease score has passed now returns to pending
A job that stalls repeatedly past EchoMQ.Stalled's max_stalled threshold is not
recovered forever: EchoMQ.Stalled.check/3 counts each stall on the row's stalled
field and, at the threshold, dead-letters the job to emq:{queue}:dead. From the morgue the
completion folds onward to the durable floor — the persistence frontier the
echo-persistence course follows, where the bus archive lands in the page engine
and replicates off-box. The lease score, the reaper, and the archive are the recovery machinery; this dive
teaches the ledger and the ack.
Notes on ValkeyZADD key score member scores a member of a
sorted set; EchoMQ scores each active member at its lease deadline, so a member whose score has
passed now is the death signal the reaper reads —
valkey.io/commands/zadd.
The in-flight list is a recovery ledger: a parked job stays there until an explicit ack, so a crashed worker leaves it found and returnable, and the ack runs only when the work is done.
emq:{queue}:active is the ledger, each member scored at its lease deadline;
EchoMQ.Jobs.complete/4 is the token-fenced ack and reap/2 returns an expired lease,
EchoMQ.Stalled dead-lettering a repeat-staller to the durable floor.
The in-flight list is a recovery ledger, not a buffer; EchoMQ's active set scored by
lease, with the token-fenced complete/4 ack and the reaper, is exactly that ledger.
A door, not a depth
The reaper that returns an expired lease, the stalled sweep that counts repeated stalls, and the token-fenced ack across a worker pool are the dedicated EchoMQ course. From here, open onto the Queue pillar — the as-built leased state machine. This dive teaches the ledger and the ack.
Where a dead-lettered job lands
A job past its stall threshold is dead-lettered, and the completion folds onto the durability floor — the bus archive into the page engine and off-box replication. The durability dial is the subject of the persistence course.
References
sources · relatedSources
- Redis — Lists — the list as a record of ordered elements, the in-flight ledger read as a record of work.
- Redis — LREM — the ack: remove one occurrence of a job from the in-flight list after the work is done.
- Redis — RPOPLPUSH — the move that parks a job in the in-flight ledger atomically.
- Valkey — ZADD — score a member of a sorted set, how EchoMQ records the lease deadline that doubles as the recovery clock.
Related in this course
- R3.01 · Processing listthe module hub: the in-flight move
- R3.01.1 · List as wait + activethe two named places
- R3.01.2 · LMOVE / RPOPLPUSHthe atomic move that parks the job in this ledger
- R3 · The reliable queueat-least-once and stalled recovery
- /echomq · Queuethe leased state machine in depth
- /echo-persistencethe durability floor a dead-lettered job folds onto