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.
@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.
Two steps — parse-and-mint, then admit-as-one-script
exchange/gateway.ex:91An 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.
The keys that must move together
echo_mq/keyspace.exAdmission 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.
One Lua script — the whole admission, all-or-nothing
echo_mq/jobs.ex:14EchoMQ.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.
-- @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.
Why the heavier machinery is unnecessary here
R2.02 · R2.03 · R2.04The 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.
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.
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.
The hash tag keeps the multi-key script legal
echo_mq/keyspace.ex:43On 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.
EchoMQ.Keyspace.slot/1 computation (CRC16-XMODEM mod 16384;
vector slot("123456789") == 12739).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.
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.
References
sources · relatedSources
- Redis — Programmability (EVAL intro) — one Lua script as a single atomic step;
EVALSHA/NOSCRIPTdispatch. - Valkey — EVAL — the command that runs the admission script as one atomic step on the engine.
- Valkey — Cluster specification — hash tags, the 16384 slots, and the
CROSSSLOTthat co-location prevents. - Redis — Documentation — the sorted set, hash, and scripting commands the admission is built from.
Related in this course
- R2 · Coordination & Consistencythe chapter this workshop closes
- R2.01 · Atomic updatesthe single-script atomic transition this admission is
- R2.02 · Distributed lockingthe claim lease and attempts fence this admission does not need
- R2.04 · Cross-shard consistencythe torn write the one-slot script cannot produce
- R2.05 · Hash-tag co-locationthe {orders} tag that keeps the multi-key EVAL legal
- /echomqthe EchoMQ protocol that teaches the full script bundle