jonnify·redis /redis-patterns/coordination/workshop

R2.06 · Coordination — capstone workshop

Order placement, made atomic.

Fold five coordination patterns into one Exchange Platform order placement that either lands completely or not at all — a parsed order admitted onto the {orders} queue by a single Lua script. It needs no lock and no Redlock, because the script is the serialization.

Grounding: echo/apps/exchange · Exchange.Gateway.parse_place/1 · echo/apps/echo_mq · EchoMQ.Jobs.enqueue → @enqueue — the parse-don't-validate door and the real atomic admission, every figure verbatim from the as-built code.

The admission transition — run @enqueue over two co-located keys and read the before→after state. Select an action to run the script's branches and read its return.
emq:{orders}:* — two co-located keys, one atomic script emq:{orders}:job:<id> (HASH) — emq:{orders}:pending (ZSET) { } · 0 members action: EVALSHA @enqueue(KEYS, id, payload) return — effect: press an action to run @enqueue over the two keys
readout · Start: no row, the pending set empty. Admit an order to run @enqueue; retry it to see the EXISTS refusal.

One script checks the kind, refuses a duplicate by id, writes the row, and inserts the pending entry — the whole admission lands together or not at all, with no WATCH loop and no lock.

§1

Two steps — parse-and-mint, then admit-as-one-script

exchange/gateway.ex:91

An order placement is two moves, and the chapter’s patterns govern the second.

First, the door. Exchange.Gateway.parse_place/1 turns an untrusted map() into either a typed command or one member of the closed six-atom error set. No exception escapes; no partially-built command is ever returned. On success it mints a branded id at the instant of acceptance and returns the place command:

# Exchange.Gateway.parse_place/1 — the success branch (gateway.ex:91)
{:ok,
 {:place,
  %{
    id: EchoData.Snowflake.next_branded("ORD"),   # minted at acceptance, success branch only
    instrument: instrument,
    account: account,
    direction: direction,                          # :buy | :sell
    type: type,                                    # :limit | :market
    quantity: quantity,                            # pos_integer
    price: price                                   # {units, nano} | :market — never a float
  }}}

The six closed errors are the whole failure channel — :unknown_instrument, :bad_direction, :bad_order_type, :nonpositive_quantity, :bad_price, :malformed — and a rejection mints nothing.

Second, admission. The parsed order is recorded onto the {orders} queue as one atomic Lua script. The branded id is the job key and the venue idempotency key (docs/exchange/trd.1.1.md), so admitting the order and refusing a duplicate are the same indivisible step.

The Gateway parses and mints; the queue admits. The chapter’s five patterns are all about that second step — making the admission one atomic, co-located, idempotent move.

§2

The keys that must move together

echo_mq/keyspace.ex

Admission touches two co-located keys, both declared in the script’s KEYS — the v2 law: every key a script touches is named in KEYS, and ARGV carries values only.

emq:{orders}:job:<branded>   # Hash: the order's row — state, attempts, payload
emq:{orders}:pending         # Sorted set: the pending queue, member = the id, score 0

EchoMQ.Keyspace.queue_key(q, type) always braces the queue name — "emq:{" <> q <> "}:" <> type — and job_key(q, branded) returns emq:{q}:job:<branded> after gating the id with EchoData.BrandedId.valid?/1. The row and the pending entry are one fact: an order on the pending set with no row, or a row with no pending entry, is a half-written order. They must change as one.

Two keys, one fact: the order’s row and its place in the pending queue. Atomicity is the requirement they impose, not a nicety.

§3

One Lua script — the whole admission, all-or-nothing

echo_mq/jobs.ex:14

EchoMQ.Jobs.enqueue/4 (jobs.ex:27) runs the admission as a single declared-keys script, @enqueue (jobs.ex:14), through EchoMQ.Connector.eval/5 (connector.ex:63), EVALSHA-first with a load-on-NOSCRIPT fallback. There is no WATCH/retry loop (R2.01): a Lua script is the serialization point — Valkey runs it to completion before any other command, so nothing interleaves.

echo/apps/echo_mq/lib/echo_mq/jobs.ex:14 · @enqueue, the atomic admissionverbatim
-- @enqueue  KEYS[1] = emq:{orders}:job:<id>   KEYS[2] = emq:{orders}:pending
--           ARGV[1] = the branded id          ARGV[2] = the payload
if string.sub(ARGV[1], 1, 3) ~= 'JOB' then
  return redis.error_reply('EMQKIND job id must be JOB-namespaced')   -- admit by kind
end
if redis.call('EXISTS', KEYS[1]) == 1 then
  return 0                                                            -- refuse duplicates
end
redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2])
redis.call('ZADD', KEYS[2], 0, ARGV[1])
return 1

enqueue/4 reads the script’s integer back into a verdict in input order — {:ok, 1} is :enqueued, {:ok, 0} is :duplicate, and the EMQKIND server error is {:error, :kind}. Three properties make this correct, and the interactive in the hero is each of them made concrete.

Admit by kind. The first line checks the id’s three-byte namespace; the queue admits jobs, so a non-JOB id is refused with EMQKIND before any write.

Idempotent on retry. The EXISTS check returns 0 for an id already on the queue, writing nothing. A dropped reply that triggers a client retry never double-admits the order — the branded id is the idempotency key (R2.01’s idempotency-key technique).

Never a torn write. The HSET row and the ZADD pending entry run inside one script, so the order’s row and its pending position land together or not at all — the atomic precondition R2.01 is built to hold.

Notes on ValkeyEVAL runs a Lua script as one atomic step: the script executes to completion before the next command, so a multi-key transition needs no transaction retry loop — valkey.io/commands/eval.

§4

Why the heavier machinery is unnecessary here

R2.02 · R2.03 · R2.04

The script is the serialization. Because Valkey runs one Lua script to completion before the next command, the admission is already mutually exclusive without a separate lock. Each of the chapter’s heavier tools is unnecessary for this admission — not because they are wrong, but because the co-located atomic script already does their work:

No per-order distributed lock (R2.02). A SET NX PX lock with a fencing token exists to serialize a read-modify-write that spans several round trips. Admission is one round trip; the script already serializes it, so a lock would only add a lease to renew and a failure mode to handle. EchoMQ’s lease and attempts fence belong to the claim of a pending job, not its admission.

No multi-master Redlock (R2.03). Redlock buys mutual exclusion across N independent Redis masters. EchoMQ reads one server clock inside one atomic script over one EchoMQ.Connector; its atomicity comes from the single server running the script, not from a majority of masters. Reaching for Redlock here would trade a real guarantee for a weaker, more expensive one.

No torn-write detection (R2.04). Cross-shard detection — version tokens, commit markers — is the fallback when keys cannot share a slot. The {orders} hash tag co-locates the row and the pending set, so the torn write the detection guards against cannot occur.

the chapter

Five coordination patterns: atomicity (R2.01), locking (R2.02), the Redlock contrast (R2.03), cross-shard detection (R2.04), and co-location (R2.05) — each the right tool for a different shape of multi-key change.

this workshop

One co-located, idempotent, atomic admission — EchoMQ.Jobs.enqueue’s @enqueue over emq:{orders}:* — that needs none of the heavier machinery, because the script is the serialization.

The chapter’s lightest pattern is the one that holds: co-locate the keys, run one script, and the three heavier coordination tools are not needed for this admission.

§5

The hash tag keeps the multi-key script legal

echo_mq/keyspace.ex:43

On a cluster the two keys must share one slot, or a script touching both raises CROSSSLOT (R2.05). The {orders} hash tag arranges that: Valkey hashes only the substring inside the first non-empty {...}, so emq:{orders}:job:<id> and emq:{orders}:pending both hash orders and land on one slot. EchoMQ.Keyspace.slot/1 computes this client-side — CRC16-XMODEM over the hash tag modulo 16384 — so the connector routes without a server round trip. Toggle the tag and read the real slots the two keys resolve to.

Co-location check · the tag on or off, and the real slot each admission key resolves to. The slot is the live EchoMQ.Keyspace.slot/1 computation (CRC16-XMODEM mod 16384; vector slot("123456789") == 12739).
tag on → Valkey hashes "orders" for every key job:<id> → CRC16("orders") % 16384 = 0000 pending → CRC16("orders") % 16384 = 0000 one slot → the multi-key script is legal
readout · tag on: both keys hash "orders" → one slot. The multi-key script over the row and the pending set is legal.

The tag is the precondition for the atomic admission. Without it the two keys scatter and the admission is not one script — it is two commands on two nodes, with nothing to make them atomic.

a door, not a depth · → the EchoMQ course

The full script bundle behind this admission

The admission model — one Lua script per state transition, the full script bundle (enqueue, browse, pending_size, claim, complete, retry, promote, reap), the EVALSHA / NOSCRIPT dispatch in EchoMQ.Connector.eval/5, and the lease-and-fence claim plane — is the subject of the dedicated EchoMQ course, the companion depth course that teaches the protocol in full. The next verb, claim, leases a pending job on the server clock and takes attempts as its fencing token — R2.02’s worked form, the move beyond this admission. Return to the Coordination chapter to continue, and see the Overview for where Valkey sits under the Exchange Platform.

Notes on ValkeyA cluster hashes a key by the substring inside the first non-empty {...} when present, else the whole key, then takes CRC16 modulo 16384 to pick one of the 16384 slots — valkey.io/topics/cluster-spec.

§6

References

sources · related

Sources

Related in this course