jonnifyredis

R4.03.3 · dive 3

Consuming with ZPOPMIN

A worker takes the next prioritized job with ZPOPMIN — pop the smallest score: the highest-priority tier and, within it, the earliest arrival, removed atomically.

The composite score packs the tier and the arrival order so that the right next job is always the smallest number. This dive walks the consume side: the single pop that reads that number, why a lower priority value comes out first, and where the popped job goes. Step the pop on the right and watch the order march down the tiers and forward through arrivals.

Stepping ZPOPMIN · smallest score first

emq:{queue}:prioritized — pop the smallest score —

The smallest score is the right job

The composite score is priority × 0x100000000 + counter. A smaller priority number makes a smaller high-bit field; within a tier a smaller arrival counter makes a smaller low-bit field. So the smallest score in the set is, by construction, the highest-priority tier and the earliest arrival within it — exactly the job a priority queue should serve next.

ZPOPMIN emq:{queue}:prioritized
# Returns: [ "job:abc123", "4294967297" ] — the smallest-score member, removed

The pop is atomic: read-and-remove in one step, so two workers polling the same set never claim the same job. The job leaves the prioritized set the instant it is chosen.

The smallest composite score is by construction the highest tier and earliest arrival, so one ZPOPMIN returns the right next job and removes it atomically.

Why a lower priority number wins

The convention that surprises newcomers — priority 1 outranks priority 5 — is a direct consequence of two facts composed. The formula makes a higher-precedence tier a smaller score: 1 × 232 is smaller than 5 × 232. And ZPOPMIN always returns the smallest score first. Compose them and the lowest priority number is served first. Nothing reverses or negates the value; the precedence is encoded as "smaller is sooner" and read by "pop the smallest".

The hero figure above steps the pop over a fixed set, removing the current smallest score and naming the popped job, its tier, and its arrival counter — showing the order march down the tiers and, within each tier, forward through arrivals. The interactive below makes the encoding explicit, scoring two priorities and confirming which is smaller.

Smaller is sooner · priority 1 vs priority 5

two priorities at the same counter — smaller score pops first — — —

A lower priority number makes a smaller score and ZPOPMIN pops the smallest, so priority 1 is served before priority 5 — the precedence is "smaller is sooner", with nothing reversed.

After the pop: into the active list

In EchoMQ the pop does not stand alone — the popped job id moves straight onto the active list. moveToActive-11.lua runs the pop and the push together.

-- moveToActive-11.lua — real
local prioritizedJob = rcall("ZPOPMIN", priorityKey)
if #prioritizedJob > 0 then
  rcall("LPUSH", activeKey, prioritizedJob[1])
  return prioritizedJob[1]
end

prioritizedJob[1] is the member — the job id — and prioritizedJob[2] is its score. The script pushes the id onto the active list with LPUSH and returns it. The prioritized set ranks the work; the active list holds the job the worker is now running. One script does both, so a job cannot be popped and then lost between the two steps.

moveToActive-11.lua pops the smallest-score job and LPUSHes its id to the active list in one script, so a popped job is never lost between the pop and the push.

In EchoMQ — the consume, in real code

The prioritized set is EchoMQ.Keys.prioritized/1 → emq:{queue}:prioritized. The consume is moveToActive-11.lua: local prioritizedJob = rcall("ZPOPMIN", priorityKey) then rcall("LPUSH", activeKey, prioritizedJob[1]). The pop honours the composite score getPriorityScore wrote on the add side, so the job that comes out is the one the tier and arrival counter ranked first. Re-prioritizing a still-queued job is EchoMQ.Scripts.change_priority, which re-scores it with a fresh getPriorityScore so its place in the pop order changes.

the pattern

A textbook priority queue often needs a structure per tier plus a separate FIFO, then a selection step across them.

in EchoMQ

moveToActive-11.lua needs one sorted set and one ZPOPMIN: the composite score already orders by tier then arrival, so the single smallest-score pop returns the right job and removes it, with no scan and no second structure.

A textbook priority queue may need a structure per tier and a separate FIFO; EchoMQ needs one sorted set and one ZPOPMIN, because the composite score already carries both orderings.

Door · not depth How a group draws from its prioritized set, and how concurrent workers across runtimes share one pop point, is the subject of the dedicated EchoMQ course. The intra-group priority and control plane are E4 · Groups. This dive teaches the smallest-score pop; that course teaches the groups that draw from it.

References

The pop that serves the highest-priority, earliest-arrival job, the sorted set behind it, the composite score it reads back, and the queue protocol EchoMQ ports.

Sources

  • Redis — ZPOPMIN — pop the member with the smallest score, atomically; the consume that serves the highest-priority, earliest-arrival job.
  • Redis — Sorted sets — score-then-member ordering, the basis for "smallest score is the right job".
  • Redis — ZADD — the add side ZPOPMIN reads back; the composite score that ranks the job.
  • BullMQ — the queue protocol — the prioritized-jobs protocol EchoMQ ports, where the Lua scripts are the protocol.

Related in this course