jonnify·redis /redis-patterns/flow-control/fairness/the-weighted-share

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.

K = the smallest of three. Select a clamp to read what it guards. A lane's share per turn is the minimum of its weight, its pending depth, and the room left under its concurrency ceiling.
K = min( weight , depth , headroom ) weight 5 , depth 3 , headroom 4 glimit − gactive the minimum wins — here K = 3 (the depth clamps the weight of 5)
readout · Select a clamp. K is the smallest of the three. Here the weight asks for 5, the lane holds a depth of 3, and the headroom under the ceiling is 4 — so K = 3, the depth clamps the share below the weight, and 3 heads are served this turn.
§1

A weight, not a priority

weight/4 · the gweight hash

An 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.

§2

The multi-pop

the @gwclaim Lua

EchoMQ.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.

EchoMQ.Lanes · echo/apps/echo_mq — the @gwclaim script: one rota step, then K headsverbatim
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.

One turn, three lanes, three weights. Select a lane to read how many heads it is served this turn and which clamp decided K. Same ring, same rotation — the weight changes only the share per turn.
K heads served this turn — one row per lane A weight 1 → K = 1 (equal floor) B weight 3 → K = 3 (the weight) C weight 5, depth 2 → K = 2 (depth clamps)
readout · Select a lane. A (weight 1) is served 1 head — the equal floor. B (weight 3) is served 3, three times A's share. C (weight 5) holds only 2 jobs, so K clamps to the depth and 2 are served — the weight is a ceiling on the share, not a promise to fabricate work.

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.

§3

Two dials, no per-job number

the retired priority

Fairness 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 questionThe priority answerThe EchoMQ answer
Whose turn next?highest-priority jobthe rotation — claim/3
How big a share?more of the high-priority classthe weight — wclaim/3 + weight/4
This one job sooner?raise its priority numbermove its lane — reassign/4
the pattern

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.

its EchoMQ application

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”.

§4

References

sources · related

Sources

  • Valkey — HSET — set a lane's weight in the gweight hash; the @gweight script is one HSET.
  • Valkey — HGET — read the weight, the limit, and the active count the multi-pop clamps against.
  • Valkey — LMOVE — the one rota step wclaim/3 shares with claim/3.
  • Valkey — ZPOPMIN — the K heads of the rotated lane, popped in mint order.

Related in this course