R4 · Time, Delay & Priority — orientation dive
Score as meaning.
The sorted set is one mechanism; the score is the whole semantic axis. Score a job by a future millisecond and the set is a delayed queue; flatten the score and the mint-ordered id serves as the queue; score it by a next-run time and the set is a scheduler registry.
Grounding: echo/apps/echo_mq · EchoMQ.Jobs.enqueue_at/6 · EchoMQ.Repeat.register/6
— three real sets, emq:{q}:schedule, emq:{q}:pending,
emq:{q}:repeat, each carrying one number per member.
Pure verdict — one example job JOB0Nb1VTbfnu4, due in 90 s on a queue
minted at t₀. Each reading computes the literal score: run_at_ms,
0, or next_run_ms. No fact is packed into another; each set carries one number.
The score is a number that sorts
delayed-queue · sorted-setsA sorted set stores members ordered by a numeric score. The textbook delayed queue uses the simplest
possible score: a millisecond timestamp. The member is the job id; the score is when it should run;
ZRANGEBYSCORE reads back everything due.
ZADD schedule 1706649000000 "JOB0Nb1VTbfnu4" ZRANGEBYSCORE schedule -inf 1706648500000 LIMIT 0 10 ZREM schedule "JOB0Nb1VTbfnu4"
The member is the job id, the score is the fire-time, and the set stays ordered by fire-time with no extra
bookkeeping. Nothing in Valkey is told what the score "means" — it is a number, and the set sorts by it. The
meaning lives in how the writer chooses the number and how the reader interprets the order. That freedom is
the lever: because the score is an arbitrary number, the member can carry meaning too. EchoMQ uses
this on its pending set — it flattens every score to 0 and lets the branded job id, which sorts
as its mint instant, carry the order.
The score is a number, and the set sorts by it; the meaning is the writer's choice, not the engine's.
One number, three real sets
jobs.ex · repeat.exEchoMQ never packs two facts into one score. It keeps each reading on its own set and lets each set carry exactly one number per member.
- Fire-time —
emq:{q}:schedule.EchoMQ.Jobs.enqueue_at/6writes the caller's absolute run-at millisecond;enqueue_in/6computesnow + delay_msfrom the server clock inside the@scheduleLua. The score is the millisecond. - Order —
emq:{q}:pending.EchoMQ.Jobs.promote/3ZADDs a due job with score0. Every member shares score0, so the set sorts by the brandedJOBid; theclaimscript'sZPOPMINserves the oldest. - Next-run —
emq:{q}:repeat.EchoMQ.Repeat.register/6ZADDs a name scored by its next-run millisecond, and the@registerLua upserts — it guards onEXISTSthe record hash and returns0without re-adding when the name is already live, so a reboot adds no duplicate.
Three sorted sets, one mechanism. The commands stay constant — ZADD to write,
ZRANGEBYSCORE or ZPOPMIN to read — and the score is the whole semantic axis.
The writer encodes the meaning
echo_mq/jobs.ex · @scheduleThe textbook reaches the same "next job to serve" order by packing two fields into one score — a
priority tier in the high 32 bits, an arrival counter in the low 32 bits
(score = priority × 2³² + arrival), read by ZPOPMIN. That is a valid
encoding, and it is the contrast worth knowing: it puts the order in the score. EchoMQ chose the
opposite — it puts the order in the member. A score-0 pending set ordered by a mint-ordered
branded id needs no arrival counter to break ties, because the id already is one.
The run-at score, as the inline Lua computes it
The @schedule script writes the run-at millisecond — the caller's absolute ms for
at, the server clock plus the delay for in. The body is verbatim from
jobs.ex:
local score
if ARGV[3] == 'in' then
local t = redis.call('TIME')
local now = t[1] * 1000 + math.floor(t[2] / 1000)
score = now + tonumber(ARGV[4]) ← server clock + delay
else
score = tonumber(ARGV[4]) ← the caller's absolute run-at ms
end
redis.call('ZADD', KEYS[2], score, ARGV[1])
The sorted set does not store time, priority, or schedule — it stores one number per member and keeps the set ordered by it. The number is the meaning, and the writer encodes it.
Three real sets — emq:{q}:schedule scored by run-at ms, emq:{q}:pending scored
0 and ordered by the mint-ordered JOB id, emq:{q}:repeat scored by
next-run ms and upserted — each carrying exactly one number per member.
Notes on ValkeyThe @register upsert reads
EXISTS on the per-name record hash before ZADD, so a re-register is idempotent;
ZADD would otherwise overwrite the score and could double-schedule a name —
valkey.io/commands/zadd.
The number is the meaning, and the writer encodes it. EchoMQ's choice is to keep each number simple and let the branded id do the ordering work a packed counter would otherwise carry.
References
sources · relatedSources
- Valkey — Topics — the sorted-set type: members ordered by a numeric score, the substrate every reading shares.
- Redis — ZADD — add or update a member's score; the one write behind all three readings, and the repeat upsert.
- Redis — ZRANGEBYSCORE — read members by score range; the due-head read the promote and repeat sweeps both use.
- Redis — ZPOPMIN — pop the lowest-scored member; the read that serves the oldest job from the score-0 pending set.
Related in this course
- R4 · Time, Delay & Prioritythe chapter: the sorted-set-as-clock family in one place
- R4 · The sorted set as a clockthe prior dive: one set, two readings
- R4 · The road aheadthe arc R4.01→R4.06 and the door to the EchoMQ Queue
- The Queue — EchoMQthe state machine, the schedule set, and the promote pump, in depth