jonnify·redis /redis-patterns/time-delay-priority/priority-scores/zpopmin

R4.03.3 · Priority with composite scores — dive 3

Consuming with ZPOPMIN.

Pop the member with the smallest score: the highest-priority tier and, within it, the earliest arrival. One command returns the right job and removes it atomically.

Grounding: echo/apps/echo_mq/lib/echo_mq/lanes.ex · EchoMQ.Lanes.wclaim/3 — the real bus reaches the same head with a weighted ring rotation, the per-lane ZPOPMIN popping the mint-ordered front.

Step ZPOPMIN over six prioritized jobs. Each step pops the current smallest score. Hover a job to read its tier and counter; the readout names the next pop.
scored:jobs — pop order: smallest score first
readout · Six jobs, by composite score. Press "pop next" to remove the smallest; hover a job to read its tier and counter.

Static order — pops march down the tiers: B, D, F (tier 1 by arrival), then A, E (tier 2), then C (tier 3). Each pop removes the current smallest score.

§1

The smallest score is the right job

lexicographic-sorted-sets.md.txt

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 removes and returns the member with the smallest score. 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 set the instant it is chosen.

ZPOPMIN scored:jobs
# Returns: [ "job-A", "4294967297" ]  — the smallest-score member, removed

The composite score makes the right next job the smallest number, and ZPOPMIN returns the smallest atomically — one command serves the highest tier, earliest arrival, and removes it.

§2

Why a lower priority number wins

lexicographic-sorted-sets.md.txt · ZPOPMIN

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 formula
A higher-precedence tier is a smaller score: 1 × 232 < 5 × 232.
ZPOPMIN
Always returns the smallest score first.
composed
The lowest priority number is served first — "smaller is sooner," read by "pop the smallest."

A lower priority number wins because the formula makes it the smaller score and ZPOPMIN pops the smallest — precedence is "smaller is sooner," with nothing negated.

§3

In EchoMQ: the same head, by a rotating ring

echo_mq/lanes.ex · wclaim/3

EchoMQ serves the highest-precedence work next, but not by popping the smallest composite score from one set — it has no per-job priority score to pop. Instead EchoMQ.Lanes.wclaim/3 runs a weighted rotation: one LMOVE rotates a ring of serviceable lanes exactly once, then serves that lane a fair share of K = min(weight, lane depth, glimit headroom) heads in one atomic turn. The per-lane ZPOPMIN still does the pop — but it pops the head of the rotated lane, which the score-0 set keeps in mint order, so within a lane it is FIFO.

A higher-weight lane is served proportionally more over a window, never all of it — so precedence is the lane's weight and fairness is constructed by the rotation, not read from a packed number. The consumer codemojex rides this directly: Codemojex.NotificationWorker drains the cm.notify lanes, one fair lane per chat, so one chat's burst cannot starve others.

-- @gwclaim (lanes.ex) — rotate the ring once, then serve K heads of that lane, trimmed
local g = redis.call('LMOVE', KEYS[1], KEYS[1], 'LEFT', 'RIGHT')   -- rotate the ring of serviceable lanes
local lane = ARGV[1] .. 'g:' .. g .. ':pending'
local w = tonumber(redis.call('HGET', ARGV[1] .. 'gweight', g) or '1')  -- the lane's throughput share
local k = w
if depth < k then k = depth end                                    -- never over-pop the lane
-- for _ = 1, k do  local popped = redis.call('ZPOPMIN', lane)  ...  end

Notes on ValkeyZPOPMIN removes and returns the member with the lowest score, atomically — the per-lane pop EchoMQ runs inside the weighted rotation — valkey.io/commands/zpopmin.

the application · codemojex · the weighted rotation

One pop, but across a ring of lanes

The textbook serves the next job with one ZPOPMIN over a composite score. EchoMQ keeps the per-lane ZPOPMIN for within-lane FIFO, but reaches across tiers with the weighted ring rotation in wclaim/3: precedence is the lane's gweight. The lane ring, the rotation, and the order theorem are the Queue pillar of the EchoMQ course.

the pattern

One ZPOPMIN over a composite score returns the next job — the score already orders by tier then arrival, so the smallest is always the right one.

its EchoMQ application

EchoMQ keeps the per-lane ZPOPMIN for within-lane FIFO but reaches across tiers with a weighted ring rotation in wclaim/3: precedence is the lane's gweight, the head is the lane's mint-ordered front.

The pattern pops one smallest composite score; EchoMQ pops each lane's mint-ordered head and reaches across tiers by rotating a ring weighted per lane — same goal, the precedence on the lane.

§4

Recap — one pop, the right job

back → R4.03

The composite score makes the next job the smallest number, and one ZPOPMIN serves it: highest tier, earliest arrival, removed atomically — a lower priority number wins because it is the smaller score. EchoMQ reaches the same head differently: a weighted ring rotation in wclaim/3 picks the lane by its gweight and pops its mint-ordered front. Return to the module hub for the whole arc, or open the Queue pillar of the EchoMQ course for the rotation in depth.

§5

References

sources · related

Sources

  • Valkey — ZPOPMIN — pop the member with the smallest score, atomically; the consume EchoMQ runs per lane on the engine it ships on.
  • Redis — ZPOPMIN — the same command's Redis reference; pop the smallest-score member.
  • 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 score that ranks the job (score 0 in EchoMQ's lane sets).

Related in this course