R4.05.3 · Leaderboards — dive 3
The score-update path.
A score changes in one of two ways: ZADD overwrites it, ZINCRBY
accumulates onto it. Neither touches a rank, because no rank is stored — the rank is recomputed from
the order on the next read, so it is always consistent.
Grounding: echo/apps/codemojex/lib/codemojex/board.ex · Codemojex.Board.record/3
— a best-of ZADD folded by new_base = max(old, base), idempotent on a
re-delivered guess.
mango at 510, then
pick an update — ZADD replaces the score, ZINCRBY adds a delta.mango. ZADD overwrites to an absolute value; ZINCRBY adds a delta and is
safe under concurrency. The rank is read from the re-ordered board.An update writes one score; the rank is never written. ZADD 420 drops
mango down the order; two ZINCRBY +50 calls sum to +100 with no lost
update.
ZADD — overwrite to an absolute value
leaderboards.md.txt · Core OperationsZADD sets a member's score to the value given, whether or not the member is already on the
board. This is the update to use when the new score is known outright — a final result, a recomputed
total, a snapshot value. The member is re-placed in the order by the new value; lift one player above three
and they move three places on the next read. ZADD carries the whole new state of the score in one
write.
ZADD leaderboard 2000 "mango" # mango's score is now 2000, replacing any prior score
ZINCRBY — accumulate a delta
leaderboards.md.txt · Core OperationsZINCRBY adds a delta to the current score and returns the new score. This is the update to use
when points arrive incrementally — fifty more for a win, ten for a step completed. Because the increment
happens inside Redis, two concurrent ZINCRBY calls on the same member both apply: the score rises
by the sum of the deltas, with no lost update. A plain read-modify-write in the application would lose one of
two concurrent increments; ZINCRBY does not. A member not yet on the board is treated as starting
from 0.
ZINCRBY leaderboard 50 "mango" # mango's score rises by 50; returns the new total
Notes on ValkeyZINCRBY key delta member increments a
member's score atomically inside the server, so concurrent increments compose without a lost update — a
member absent from the set is treated as a 0 score —
valkey.io/commands/zincrby.
The rank is recomputed on read, never stored
leaderboards.md.txt · Rank vs Reverse RankNeither update writes a rank. The rank of any member is its position in the score order, and the set
computes that position when ZREVRANK or a range read asks for it — in O(log N), by walking
the order. Two consequences follow. First, the rank is always exactly the current order: there is no stored
rank that an update could leave stale, so a score change is reflected the instant the rank is next read.
Second, updating one member's score can shift the ranks of many others — lift one player past three and
those three each drop a place — but none of those neighbours is rewritten. Their scores are unchanged;
only their computed position differs on the next read.
The work of a score update is O(log N) for the re-placement; the ripple through everyone else's rank costs nothing at write time, because it is computed at read time.
Update the score, not the rank. ZADD to overwrite, ZINCRBY to
accumulate, and the rank is read from the order — so it is never stale.
In codemojex — the best-of overwrite
echo/apps/codemojex · game.ex · board.excodemojex's score update is ZADD, with a best-of fold so a rank only ever
climbs. The score worker is the authority: Codemojex.ScoreWorker.handle/1 scores a guess with the
pure linear engine and records the result on the board.
s = Scoring.score(secret, emojis) ← the linear total, out of 600 gid = EchoData.BrandedId.generate!("GES") Store.put_guess(gid, %{game: game, player: player, emojis: emojis, points: s.total, …}) Cmd.incr("cm:" <> game <> ":attempts") |> Wire.run(conn) eff = Board.record(game, player, s.total) ← ZADD the player's best base to cm:<game>:board
Codemojex.Board.record/3 reads the player's current best from cm:<game>:base,
takes new_base = max(old, base), and writes it to the board with
Cmd.zadd("cm:<game>:board") |> Cmd.score(new_base, player). That is an absolute
overwrite to the best-so-far — never ZINCRBY, because the rank is the best single result,
not a running sum. A re-delivered guess re-scores identically (the engine is pure) and the
max/2 fold makes the write idempotent: a lower or equal base leaves the rank where it was. No rank
is stored; the next Board.top/2 read recomputes the whole order.
The queue's re-ordering on a change
How the EchoMQ queue updates a job's standing and re-orders its work belongs to the Queue pillar —
the same score-order idea, in the emq:{q}: keyspace.
How a score survives a restart
The board is a derived view. The persistence floor is the durable substrate that rebuilds the volatile board from the record of what was scored — the durability dial beneath the bus.
A score is updated by ZADD (overwrite) or ZINCRBY (accumulate), and the rank is
recomputed from the order on read, never stored.
Codemojex.Board.record/3 writes each player's best linear total with
Cmd.zadd("cm:<game>:board") |> Cmd.score(new_base, player) — a best-of
overwrite, idempotent on re-delivery; the rank is read from the order, never written.
Update the score, not the rank — ZADD to overwrite, ZINCRBY to
accumulate. The rank is read from the order, so it is never stale. codemojex's Board.record/3 is
that ZADD, folded to the player's best base.
References
sources · relatedSources
- Redis — ZADD — set a member's score to an absolute value, overwriting any prior score.
- Redis — ZINCRBY — add a delta to a member's score in place, with no lost update under concurrency.
- Redis — ZREVRANK — read the rank from the order, recomputed on each read.
- Redis — Sorted sets — the ordered structure an update re-places a member in.
Related in this course
- R4.05 · Leaderboardsthe module hub
- R4.05.2 · Top-N and around-methe two reads over the order
- R4.05.1 · ZADD and ZREVRANKthe score in, the rank out
- R4.03 · Composite priority scoresthe same sorted-set ordering, packed for ties
- /echo-persistencehow a derived board survives a restart