Codemojex · C3.2 · a dive of Guesses on Fair Lanes
Charged, then enqueued.
A validated, lock-merged guess is not yet work. Two things happen, in this order, and the order is the point: the wallet charges the room’s currency, and only a charged guess is enqueued as a branded JOB. Charge-before-enqueue over an idempotent bus is what makes an at-least-once system safe with real money on the line.
Grounding — echo/…/codemojex/game.ex (the submit/3 charge branch) · wallet.ex (charge_guess/3, the paired TXN) · design §Messaging (the idempotent enqueue) / §Core flows. Every guess pays its fee — the all-pay structure.
TXN. Only then is a fresh branded JOB minted and Lanes.enqueued on the player’s cm lane. The surface answers on its way; the score arrives later.Charge first
game.ex · wallet.exOnly after a successful charge does the guess become a JOB: a fresh branded id, the guess tupled into an opaque payload, dropped on the player’s lane with EchoMQ.Lanes.enqueue/5. If the player cannot afford the guess, charge_guess returns {:error, reason} and submit/3 returns it unchanged.
case Wallet.charge_guess(player, r, game) do
{:ok, _balance} ->
job = EchoData.BrandedId.generate!("JOB")
payload = :erlang.term_to_binary({:guess, game, player, guess})
Lanes.enqueue(Bus.conn(), @queue, player, job, payload) # @queue "cm", lane = PLR
{:error, reason} ->
{:error, reason}
end
Wallet.charge_guess/3 picks the currency by the game’s kind: clips for a free game, keys for a paid classic game, and a two-sided charge for a Golden Room — the keys fee debited and the diamond pool funded in one transaction, so every guess funds the pool it competes for (cm.5; the economy is C5). Each branch locks the player row, checks the non-negative invariant, writes the balance, and inserts the paired TXN ledger row — all or nothing.
def charge_guess(player, game_map, ref) do
cond do
Map.get(game_map, :free, false) ->
debit(player, :clips, Map.get(game_map, :clip_cost, 1), "guess", ref)
Map.get(game_map, :golden, false) ->
charge_guess_golden(player, Map.get(game_map, :guess_fee, 1), ref) # keys + fund pool
true ->
debit(player, :keys, Map.get(game_map, :guess_fee, 1), "guess", ref)
end
end
Two windows, both closed
design §Messaging · §Fault toleranceAt-least-once delivery means a message can arrive more than once, and a process can crash mid-flight. Both windows are closed by construction — the enqueue dedupes (a re-enqueued JOB answers :duplicate), scoring is a pure function of the secret and the guess, and each consumer leases the job it claims.
JOB as :duplicate; and even if the same guess reached the scorer twice, scoring is pure — the second pass writes the identical GES and board total. Re-delivery is harmless.The paired TXN row is the audit trail that ties the money to the guess: every charge is one ledger entry, so a charge and its guess reconcile exactly. This is Helland’s idempotent activity — the write is safe to retry because its effect is defined once.
References
sources · relatedSources
- Helland — Life Beyond Distributed Transactionsidempotent activities across boundaries
- Repo.transactionthe all-or-nothing balance-and-ledger transaction
- All-pay auctionevery guess pays a fee whether or not it wins
Related
- C2 — Rooms, Modes, and the Secretthe room whose currency path the charge follows
- Redis Patterns Appliedthe idempotent-enqueue and dedupe patterns
- EchoMQ — the protocolthe bus and its
Lanes.enqueue/5