Codemojex · C3.3 · a dive of Guesses on Fair Lanes
Fair lanes and the worker.
The bus is the spine of the runtime. Codemojex.Application starts four EchoMQ.Consumer children, one per kind of work, and each lane is grouped so one heavy producer cannot starve the rest. The guess lane’s group is the player’s PLR — which is where procedural fairness lives.
Grounding — echo/…/codemojex/application.ex (the four consumers, their beat_ms/lease_ms) · game.ex (Codemojex.ScoreWorker.handle/1) · rate_limiter.ex · design §Messaging / §Privacy and fairness.
cm, keyed by the player’s PLR, drained by :cm_score — the single scoring authority. The bus rotates service across the per-player lanes, so paying for speed buys a turn, not the field.Four lanes, grouped
application.exThe four consumers are supervised children with an explicit id each. The guess lane is keyed by the player’s PLR: a player who taps twice as fast gets their own lane serviced in turn, not twice the field’s throughput. The fairness is structural — the group key on the queue, not a throttle at the edge.
Supervisor.child_spec({EchoMQ.Consumer, score_opts(conn)}, id: :cm_score), # guesses → "cm"
Supervisor.child_spec({EchoMQ.Consumer, settle_opts(conn)}, id: :cm_settle), # payout → "cm-settle"
Supervisor.child_spec({EchoMQ.Consumer, notify_opts(conn)}, id: :cm_notify), # telegram (rate-limited)
Supervisor.child_spec({EchoMQ.Consumer, command_opts(conn)}, id: :cm_commands), # inbound bot commands
defp score_opts(conn) do
[queue: ScoreWorker.queue(), handler: &ScoreWorker.handle/1,
connector: conn, beat_ms: 100, lease_ms: 10_000]
end
Two patterns recur. The move-then-settle split: the guess queue competes and a separate cm-settle queue pays, so contention and payout never share a path. The delayed re-enqueue: when the RateLimiter reports a notification arrived too soon, the worker re-enqueues the same job with EchoMQ.Jobs.enqueue_in/5 after a computed delay — a token bucket sized for Telegram’s ~30/s aggregate and ~1/s per-chat limits, so it defers rather than blocks.
One scoring authority, leased
game.ex · ScoreWorker:cm_score is the single authority. ScoreWorker.handle/1 receives the claimed job with the player id arriving as the lane group. It reads the secret through the near-cache (immutable for the game’s life), scores with the pure linear engine, writes the GES, counts the attempt, and records the board — then the two modes diverge.
def handle(%{id: job_id, payload: payload, group: player}) do
{:guess, game, ^player, emojis} = :erlang.binary_to_term(payload)
case Cache.fetch_game(game) do
%{secret: secret} = g ->
s = Scoring.score(secret, emojis) # pure, linear (C4)
gid = EchoData.BrandedId.generate!("GES")
Store.put_guess(gid, %{game: game, player: player, points: s.total, ...})
eff = Board.record(game, player, s.total)
if Map.get(g, :feedback, "score") == "score" do
Events.publish(conn, @queue, "scored", job_id, ...) # the bus log
Phoenix.PubSub.broadcast(PubSub, "game:" <> game, {:scored, ...}) # live
if s.total == 600 and started?(game), do: Rooms.close_game(game)
end
:ok
_ -> :ok # unknown game → a drop, no retry loop
end
end
scored event on the bus log and broadcasts :scored over Phoenix PubSub on the game’s topic — both carrying a name, a percentage, and an effective score, never the secret or the guess. A perfect crack of 600 closes an open game there and then.Because the worker leases each job, a crash mid-scoring is survivable: the lease expires and the job becomes visible again, so the paid-for attempt is scored by another consumer rather than lost. Delivery is at-least-once and the handler is idempotent — scoring is pure — so a re-delivery is harmless.
References
sources · relatedSources
- Kreps — The Logthe ordered log work is dropped onto and drained from
- Erlang/OTP — gen_serverthe supervised, leased consumers
- All-pay auctionthe fee structure the blind mode leans on
Related
- EchoMQ — the protocolthe lanes, leases, and events in depth
- Redis Patterns Appliedthe queue and worker patterns applied
- C1 — The Game as Branded Systemsthe
PLRandGAMbrands the lanes are keyed by