jonnifyredis

R4.01.1 · dive 1

The score is the fire-time

The score of a delayed job is the time it should run. The textbook scores by a raw timestamp; EchoMQ scores ts × 0x1000 — the fire-time, shifted twelve bits.

Score a job by its fire-time and the set orders its members by when they are due, head first. The whole pattern rests on one decision: what number goes in the score. The textbook answer is the fire-time itself. EchoMQ's answer is the fire-time shifted left twelve bits, so a single millisecond can hold more than one ordered job. Move the slider on the right to set a delay and watch the two scores it produces.

The fire-time, scored · textbook vs × 0x1000

5000
fire-time (ms) = now + delay — textbook score = fire-time — EchoMQ score = fire-time × 0x1000 — 12b recover: score / 0x1000 = fire-time

The textbook score: the fire-time itself

The classic delayed-queue pattern puts the Unix timestamp at which the task should run directly into the score. The set sorts by it, so the earliest-due task is at the head, and a range query bounded by the current clock returns the due tasks.

ZADD delayed_queue 1706649000 "task:abc123"   # score = the fire-time, second-for-second

The fire-time of a deferred task is now + delay — to defer five minutes, score by now + 300. One number, one meaning: when this task should run. This is enough for most schedulers. Its one weak spot is resolution: every task scheduled in the same second gets the identical score, and a sorted set keeps equal-score members in lexicographic order of the member, not arrival order. For a queue that schedules thousands of jobs a second and wants them to keep arrival order within a tick, the bare timestamp is too coarse.

The textbook score is the fire-time itself: one number that says when the task runs; its weak spot is resolution, since equal scores fall back to member order.

EchoMQ's score: the fire-time, shifted twelve bits

EchoMQ keeps the structure and sharpens the score. In addDelayedJob-6.lua, the included getDelayedScore computes the fire-time, shifts it left twelve bits, and names the band's ceiling.

-- getDelayedScore.lua (included by addDelayedJob-6) — real
local delayedTimestamp = timestamp + delay         -- the fire-time, in milliseconds
local minScore         = delayedTimestamp * 0x1000  -- shift left 12 bits (× 4096)
local maxScore         = (delayedTimestamp + 1) * 0x1000 - 1

0x1000 is 4096, which is two-to-the-twelfth. Multiplying the millisecond fire-time by it shifts the value left twelve binary places, leaving the low twelve bits at zero. Those twelve bits are now a free field below the millisecond: every value from minScore to maxScore belongs to the same fire-time millisecond, and there are 4096 of them. The job is added with ZADD delayedKey score jobId. A job with no contention takes minScore — the millisecond's floor.

The recovery is exact, never lossy. getNextDelayedTimestamp reads the head and returns nextTimestamp / 0x1000 — it divides the shift back out to recover the real millisecond. The shift packs an extra field into the low bits without losing the time it sits above.

Scoring by millisecond × 0x1000 keeps the time and frees the low twelve bits for an ordering field; dividing by 0x1000 recovers the time exactly.

The low twelve bits: a within-millisecond discriminator

The reason for the shift shows when two jobs land on the same millisecond. The textbook bare-timestamp score gives them the identical number, and the set falls back to member order to break the tie. EchoMQ's shift gives the set somewhere to put the tie-break. getDelayedScore checks the millisecond's band before inserting.

local result = ZREVRANGEBYSCORE delayedKey maxScore minScore WITHSCORES LIMIT 0 1
local currentMaxScore = -- score of the highest job already in this ms band
if currentMaxScore == nil      then return minScore          -- first job this ms: the floor
if currentMaxScore >= maxScore then return maxScore          -- band full: the ceiling
else                            return currentMaxScore + 1  -- one tick above the last

The first job to claim a millisecond takes minScore, the band's floor. The second job that lands on the same millisecond takes currentMaxScore + 1 — one tick above the first, a distinct score still inside the same millisecond band. So two jobs due in the same millisecond get two distinct, ordered scores, and the set keeps them in arrival order without falling back to member order. The band holds up to 4096 jobs per millisecond before it saturates at maxScore.

Two jobs, one millisecond · the low-12-bit bump

fire-time millisecond band — minScore … maxScore (4096 wide) minScore maxScore — — —

Multiplying the fire-time by 0x1000 reserves twelve low bits per millisecond; the second job to claim a millisecond takes one tick above the first, so the two keep their order inside a single tick.

In EchoMQ — the delayed score, in real code

The whole score is real code in echo/apps/echomq. The delayed set is EchoMQ.Keys.delayed/1 → emq:{queue}:delayed. The score is computed by getDelayedScore, an included function in addDelayedJob-6.lua: delayedTimestamp = timestamp + delay, minScore = delayedTimestamp × 0x1000, the same-millisecond bump to currentMaxScore + 1. The add is rcall("ZADD", delayedKey, score, jobId). The recovery is getNextDelayedTimestamp returning nextTimestamp / 0x1000.

-- getDelayedScore.lua (included by addDelayedJob-6) — score the job by its fire-time (real)
local delayedTimestamp = timestamp + delay
local minScore = delayedTimestamp * 0x1000          -- fire-time shifted left 12 bits
local maxScore = (delayedTimestamp + 1) * 0x1000 - 1
ZADD delayedKey score jobId                       -- alone → minScore; same-ms collision → currentMaxScore + 1

The number chosen to carry the meaning is the whole pattern. The bare timestamp says only "this millisecond"; the shifted timestamp says "this millisecond, and this position within it".

the pattern

A delayed job is scored by its fire-time, so the sorted set orders its members by when they are due.

in EchoMQ

getDelayedScore scores by (timestamp + delay) × 0x1000, reserving the low twelve bits so a same-millisecond collision takes currentMaxScore + 1 — a distinct, ordered score inside one tick; getNextDelayedTimestamp recovers the real time by dividing by 0x1000.

Scoring by the bare fire-time orders jobs by the millisecond; scoring by fire-time × 0x1000 orders them by the millisecond and by arrival inside it, and the time is recovered exactly by dividing the shift back out.

Door · not depth The full scheduler subsystem — how the delayed score feeds the promote sweep, the retry reschedule, and the worker pool that runs them — is the subject of the dedicated EchoMQ course. The delay, promote, and retry coordination is E6 · Lifecycle controls. This dive teaches the score; that course teaches the scheduler that reads it.

References

The write that scores a delayed job by its fire-time, the sorted set behind it, the range query the same score is later read by, and the queue protocol EchoMQ ports.

Sources

Related in this course