R6.02.2 · Fairness under load — dive
The weighted share — more, never all.
Equal rotation is the floor. A weight tilts how much of the machine a lane gets per turn
— a per-lane throughput share, not a per-job priority. wclaim/3 serves a lane
K = min(weight, depth, glimit headroom) heads in one turn, so a higher-weight lane drains faster and
never takes the whole ring.
Grounding: echo/apps/echo_mq · EchoMQ.Lanes.wclaim/3 and the inline @gwclaim Lua (the weighted multi-pop) · EchoMQ.Lanes.weight/4 (the gweight hash) · the keys emq:{q}:gweight, emq:{q}:glimit, emq:{q}:gactive.
A weight, not a priority
weight/4 · the gweight hashAn earlier model reached for a numeric per-job priority — a score stamped on each job so the line sorts the urgent work to the front. EchoMQ retired that by design. The replacement is a weight on the lane, not the job. The distinction is the whole point: a priority says “this job matters more”; a weight says “this producer's stream gets a larger share of the machine”. Urgency travels with the identity, never with a single piece of work.
A weight is an integer, one or more. EchoMQ.Lanes.weight/4 sets it, writing the value into
the emq:{q}:gweight hash (group → weight) — the same shape as the concurrency keys,
no new key family. A weight of one is plain rotation: the lane is served a single head per turn, identical
to claim/3. A higher weight serves the lane more heads per turn, proportionally more over a
window — and never the whole ring, because every other lane still takes its turn.
:ok = EchoMQ.Lanes.weight(conn, "cm", group, 3) # this lane gets 3 heads per turn — a throughput share
A weight is never zero. A weight is a throughput share, and weight/4 requires
w ≥ 1; parking a lane is a different operation — the operator's pause/3,
which removes the lane from the ring entirely. Setting a weight never changes whether a lane is serviceable
(serviceable means nonempty, unpaused, and below its limit, all weight-independent), so weight/4
touches no ring bookkeeping at all — the @gweight script is a single HSET.
Notes on ValkeyThe weight rides a per-queue hash:
HSET emq:{q}:gweight <group> 3 sets it, and the claim reads it with
HGET emq:{q}:gweight <group>. One hash holds every lane's weight, the same key shape as the
glimit and gactive hashes —
valkey.io/commands/hset.
A weight is per-lane, never per-job: weight/4 writes it to the
gweight hash, w ≥ 1, and it never parks a lane. “Served more” is a
property of the identity, not the work.
The multi-pop
the @gwclaim LuaEchoMQ.Lanes.wclaim/3 is the weighted rotation. It rotates the ring one step exactly as
claim/3 does — one LMOVE, the same rota step — then, instead of serving
one head, it serves the rotated lane K heads in one atomic turn, where:
K = min(weight, the lane's pending depth, the glimit headroom)
Each clamp guards a different invariant. The weight is the requested share, read from
emq:{q}:gweight (absent or below one, it clamps to one). The lane's pending
depth means a turn never serves more than the lane holds — a weight of five against a lane of
two serves two. The glimit headroom — the room left under the concurrency ceiling
(glimit minus gactive) — means the multi-pop can never push
gactive past glimit: a weight is a throughput share, the limit is a concurrency
ceiling, and the share bends to the ceiling. With no headroom the lane is at its ceiling, so it is de-ringed
and nothing is served this turn.
local g = redis.call('LMOVE', KEYS[1], KEYS[1], 'LEFT', 'RIGHT') ← the same one rota step as claim/3
if not g then return {} end
local lane = ARGV[1] .. 'g:' .. g .. ':pending'
local depth = redis.call('ZCARD', lane)
if depth == 0 then
redis.call('LREM', KEYS[1], 0, g)
return {}
end
local w = tonumber(redis.call('HGET', ARGV[1] .. 'gweight', g) or '1') ← the weight (defaults to 1)
if w < 1 then w = 1 end
local k = w
if depth < k then k = depth end ← depth clamp
local lim = redis.call('HGET', ARGV[1] .. 'glimit', g)
if lim then
local cur = tonumber(redis.call('HGET', ARGV[1] .. 'gactive', g) or '0')
local headroom = tonumber(lim) - cur
if headroom < k then k = headroom end ← ceiling clamp: never past glimit
end
The K served jobs share one lease deadline, computed once from the server clock for the whole turn, and
gactive is incremented by the actual count served, after which the same re-ring guard as
claim/3 runs once (the ceiling test and the empty-lane test). The result is a fair share that
respects the ceiling: a higher-weight lane drains faster, but never past the concurrency it is allowed, and
never to the exclusion of the other lanes.
K = min(weight, depth, glimit headroom). The weight is the share, the depth caps it at what
the lane holds, and the headroom keeps the turn from pushing gactive past the concurrency
ceiling. A higher weight drains faster, never the whole machine.
Two dials, no per-job number
the retired priorityFairness now has two dials, and they are orthogonal. The rotation gives every
serviceable lane an equal turn — this is claim/3, the floor. The weight
tilts how many heads a lane gets per turn — this is wclaim/3, the share. Neither
dial is a number on a job.
EchoMQ.Lanes.weight/4 carries the design intent in its own documentation: weight is
per-lane, never per-job — there is no numeric per-job priority (retired by design); “served
more” is a property of the identity, not the work. The companion reassign/4 carries the
other half of the retarget: “matters more now” is a change of lane, mint order is the order
theorem — a job that should be served sooner is moved to a different lane, not given a higher
number.
| The usual question | The priority answer | The EchoMQ answer |
|---|---|---|
| Whose turn next? | highest-priority job | the rotation — claim/3 |
| How big a share? | more of the high-priority class | the weight — wclaim/3 + weight/4 |
| This one job sooner? | raise its priority number | move its lane — reassign/4 |
Serve one producer a larger share of the machine — but never all of it, and never by stamping a priority number on individual pieces of work.
A weight in the emq:{q}:gweight hash, set by EchoMQ.Lanes.weight/4, and
EchoMQ.Lanes.wclaim/3 serving the rotated lane K = min(weight, depth, glimit headroom) heads
per turn. A weight is a throughput share, not a pause, and not a per-job priority.
A consumer keeps it concrete. In codemojex (echo/apps/codemojex) the lanes are players (the
lane is named by the player's PLR id), and the rota serves players in turn through
Codemojex.ScoreWorker. A weight on a player's lane would let that lane drain more heads per
turn — a larger share of the scoring machine — without ever halting the others; the weight is
the dial for “more”, the ring still guarantees “everyone”.
The weighted share in the pillar
The Queue pillar drives wclaim/3 from the consumer loop and composes the weight with the
per-group ceiling and the lease — the scaling layer above this primitive.
The fair-lanes architecture
Part B3 builds the Valkey-native bus and its fair-lane design: lanes named by branded ids, drained fairly, with “served more” a property of the identity.
R6.02.3Why one queue, not N
The weight is a cross-lane share — only possible because the lanes live in one ring. N separate queues cannot weight one against another without an external scheduler.
References
sources · relatedSources
- Valkey — HSET — set a lane's weight in the
gweighthash; the@gweightscript is oneHSET. - Valkey — HGET — read the weight, the limit, and the active count the multi-pop clamps against.
- Valkey — LMOVE — the one rota step
wclaim/3shares withclaim/3. - Valkey — ZPOPMIN — the K heads of the rotated lane, popped in mint order.
Related in this course
- R6.02 · Fairness under loadthe module hub
- R6.02.1 · Starvation under loadthe FIFO flaw and the rota guarantee
- R6.02.3 · Lanes vs separate queuesone queue with lanes against N queues
- /echomq/queuethe weighted share wired at the claim point
- /bcs/busPart B3, the fair-lanes architecture