Trafikist · module 02
The command grammar
One order for every EMQ command, one registry for every EMQ-class and shape refusal.
Every job-core and bus write verb in trafikey runs the same order: shape, layout, the fence, the domain
checks, one write guard, then the writes. The order carries one
promise: a call any check refuses writes nothing. Past the write guard nothing is undone, so every check
that can refuse sits in front of it. Every EMQ-class and shape refusal on the way is a constant or a builder
in one closed registry, byte-exact where it shares a text with the Lua scripts; the others are host conditions
and the server's own texts. The Protocol's native module
introduced the order; this module reads it in the source, check by check.
The order, stated once
The module root states the grammar in four lines of documentation, and every handler file builds on the
three files it names: errors (the closed registry), keyspace
(the layout checks) and host (the only route to the server). The same lines name
what a handler never does: it never calls the SDK's own Context::call, never
indexes an argument by position, never unwraps.
// trafikey — the module root states the grammar every job-core and bus write verb runs //! A handler runs the grammar's order (G0.3): shape (numkeys, counts, key layout, integers), //! then the fence, then its domain checks, then its writes. It never calls `Context::call`, //! never uses `ValkeyError::WrongArity`, never indexes `args[n]`, never unwraps, and never holds a //! `c"…"` literal inside a `#[command]` body (the macro rejects it; use [`host::cmd`]).
Read the order as three zones. Before the first read: shape
(numkeys spelled exactly, the keys present, the ARGV count), layout (every declared key rebuilt from one
queue name, or a bus verb's one key checked against its form) and argument values
(lease_ms, counts, a mode and a due time; a bus verb's entry count, its split into
entries, the field names and a TTL). A call refused here has touched no key. Reads: the
fence ({emq}:version read as the caller), the domain checks (each a pure verdict,
after a host read where it needs stored state), and a type check on every key the call is about to
write. Writes: the write guard, once, then the writes, each
issued through the guard's proof and replicated as its effect.
Shape, layout, values, fence, domain, types, guard, writes: every job-core and bus write verb runs in that order (EMQ.VERSION and EMQ.INSTALL run no fence), and a refusal anywhere before the guard leaves the dataset as it was.
One handler, the whole order
EMQ.CLAIM shows every zone in one function. Its domain reads — the pause
flag, the peek at the head of pending, the row's type and attempts, the type of
active — sit between the fence and the guard; they are elided here and read in full in
One transition, two
executors.
// trafikey — EMQ.CLAIM: the whole order in one handler (the domain reads elided) pub(crate) fn claim(ctx: &Context, args: Vec<ValkeyString>) -> ValkeyResult { // shape: numkeys is exactly 3 and the keys are present, then exactly one ARGV let s = host::shape("EMQ.CLAIM", &args, 3)?; s.expect_argc(1)?; // layout: KEYS[3] is a base emq:{q}:, and KEYS[1], KEYS[2] are its pending and active let q = claim_queue(&s).ok_or_else(|| s.layout_error())?; // a value: lease_ms is parsed before the first read let lease = s.lease(1)?; // the fence: the first read of the call, {emq}:version as the caller let caller = host::fence(ctx)?; // … the pause flag, the peek at pending, the row's type and attempts, the type of active // the write guard, once, after every read and every type check let w = host::begin_writes(ctx)?; match w.call(cmd::ZPOPMIN, &[pending])? { // the pop must return the member the peek found Reply::Array(popped) if popped.first() == Some(&Reply::Bulk(id.clone())) => {} _ => return Err(errors::emq(errors::HOST_REPLY)), } // … match w.call(cmd::HINCRBY, &[&row, b"attempts", b"1"])? { // the reply must equal the attempt predicted before the guard Reply::Int(n) if n == att => {} // anything else: ERR EMQ unsupported host reply, and the pop before it stays in place _ => return Err(errors::emq(errors::HOST_REPLY)), } // … the HSET of state and lease_epoch, the ZADD to active, the reply }
Two lines of the claim run after the guard and still check. ZPOPMIN must pop
the member the peek found, and HINCRBY must return the attempt the handler computed before the guard from
the stored value and Valkey's own integer rules. Inside one command no other client runs, so on a healthy
host both hold. When a host reply differs, there is no rollback: the handler answers
ERR EMQ unsupported host reply and the pop stays in place. Single-threaded
execution is not rollback; the order is what keeps a refusal from writing.
A refusal writes nothing; past the guard nothing is undone. The guarantee is the order, and it lives in the handler, not in a transaction.
Where a call stops
Run one claim against fixed datasets, each broken at a different place, and read where the handler stops, which host reads it made on the way, and what it wrote. The handler is ported check for check over the fixed data; nothing here talks to a server.
EMQ.CLAIM through the order · pick a dataset
call: EMQ.CLAIM 3 emq:{orders}:pending emq:{orders}:active emq:{orders}: 30000
stops at: nowhere; the reply follows the writes
host reads: GET {emq}:version · HGET emq:{orders}:meta paused · ZRANGE emq:{orders}:pending 0 0 · TYPE emq:{orders}:job:JOB0KHTOWnGLuC · HGET emq:{orders}:job:JOB0KHTOWnGLuC attempts · TYPE emq:{orders}:active · HGET emq:{orders}:job:JOB0KHTOWnGLuC payload
written: ZPOPMIN emq:{orders}:pending · HINCRBY emq:{orders}:job:JOB0KHTOWnGLuC attempts 1 · HSET emq:{orders}:job:JOB0KHTOWnGLuC state active lease_epoch <run_id> · ZADD emq:{orders}:active <now + 30000> JOB0KHTOWnGLuC
Every refusal left of the guard writes nothing. Its text is a registry entry or the server's own
(WRONGTYPE …, or the HINCRBY text the claim predicts); the one failure right of the
guard is a host fault, and it keeps what was already written.
One registry for every refusal
Every EMQ-class and shape text an EMQ handler sends is a constant or a builder in
errors.rs, so those bytes are reviewable in one file. The first word of a text is its
class. Wire 4 holds eleven texts, byte-exact with the
redis.error_reply('EMQ…') literals of the Elixir scripts, in five classes:
EMQKIND, EMQLOCK, EMQRATE,
EMQSTALE and EMQSTATE. A unit test re-reads
echo/apps/echo_mq/lib and fails when a text is added, removed or changed there.
Wire 6 adds twenty-two fixed texts — the fence's EMQWIRE
refusals, the native verbs' EMQSTATE and EMQSTALE
refusals for lanes, flows and states, and the bus verbs' EMQKIND brand refusals and
their stream-order EMQSTATE text — and builders for the per-verb shape texts and for
EMQWIRE … found. Host refusals are three
ERR EMQ texts and Valkey's own NOPERM No permissions to access a
key; they are not registry entries. The only other texts are the server's own:
WRONGTYPE …, the two HINCRBY texts in jobs.rs, and
INCR's ERR value is not an integer or out of range in bus.rs,
which shares HINCRBY's overflow text.
The module's other family runs a different grammar. A TRA.* command has no numkeys
and no fence, lets the host enforce its arity and sends its own three ERR TRA texts,
and the family's one write, TRA.SETBIT, replicates its command line as it came; the
third dive sets the two side by side.
The class is the first word, the text is fixed, and a test holds the wire-4 texts to the Elixir source byte for byte.
The dives
Three dives read the grammar in the trafikey source: the checks that run before
any read, the guard and the effects, and the registry the refusals come from.
Shape before the fence
numkeys spelled exactly, the keys and the ARGV counted, every declared key rebuilt from one queue name or checked against its form, the values parsed: a malformed call reads nothing.
One guard, then effects
Reads through one adapter, every written key typed, begin_writes once as the only route to a write, then writes replicated as their effects inside one MULTI/EXEC.
The closed error registry
Eleven wire-4 texts byte-exact, twenty-two wire-6 additions, the builders, four host refusals; the test that keeps wire 4 in step with Elixir; TRA's own grammar.
Pattern & implementation
An atomic update checks before it writes and runs as one server-side step, so no other client reads it half done. Atomic updates and Lua scripts for atomic logic teach it.
Every job-core and bus write verb: shape, layout and values before any read; the fence and the domain reads; every written key typed; one write guard; writes replicated as their effects; every EMQ-class and shape refusal from one closed registry.
Take the dives in order: the checks before any read, then the guard, then the registry. The module after this one is The job core, natively.
References
Sources
- Valkey — Modules API reference —
RedisModule_Calland its format flags,RedisModule_AvoidReplicaTraffic, command registration. - Valkey — Modules introduction — what a module is: native code the server loads, registering commands.
- Valkey — CLIENT PAUSE — the write pause the guard refuses under.
- Valkey — HINCRBY — the integer rules the attempt prediction follows.
- Valkey — ZPOPMIN — the pop the claim checks against its peek.
Related in this course
- Trafikist — the part this module belongs to.
- The native module — where the order is introduced.
- The native fence — the fence, line by line.
- One transition, two executors — the claim in full on both executors.
- redis-patterns · Atomic updates — the near side of the door.