The Protocol · module 04
The native module
The core job transitions, compiled into the server. Same keys, same fields, the same replies on the ordinary path — one command each, behind a fence that only reads.
Every transition of the Protocol is one atomic server-side step, and its core job transitions
have one executor. EchoMQ.Script declares each operation as the
EMQ.* verb that performs it, and EchoMQ.Connector sends that
verb with the call's numkeys, KEYS and ARGV; it sends no Lua. The native module is
trafikey: a Valkey module compiled from Rust, loaded at server start
with loadmodule on a node of its own, which registers each core job transition as one
EMQ.* command. The command keeps the numkeys KEYS ARGV tail of
the Lua scripts it replaced, with no sha, no SCRIPT LOAD and no
NOSCRIPT path. The module keeps every key and field those scripts used, and their
replies on the ordinary path, adds two row fields
(lease_epoch, failures) and checks before it writes. This
module reads the command form, the fence every write passes through, and one transition held to its Lua reference.
The executor
A native command is the transition written in Rust, compiled into a module the server loads at start, and
run by its name. Before the module, the same transition was a Lua script, sent to the server once and run by
its sha. The command kept that line's whole numkeys KEYS ARGV tail and changed only
the head. Put the retired line for a claim beside the line the connector sends today:
# retired: the Lua script's line, before the module (history) EVALSHA <sha1 of the script> 3 emq:{orders}:pending emq:{orders}:active emq:{orders}: 30000 # today: the same tail, the sha replaced by the command name EMQ.CLAIM 3 emq:{orders}:pending emq:{orders}:active emq:{orders}: 30000
On the native side nothing is hashed or loaded; the server already holds the
handler, so there is no NOSCRIPT path to catch. In
EchoMQ.Jobs the claim is declared as @claim2, which is
Script.native(:claim2, "EMQ.CLAIM"), and claim/3 sends it
through the connector's one seam, eval/5, after its host-side pause read. A raw
EVAL or EVALSHA in any pipeline is refused.
What the module changed is not the protocol but where the transition runs: the same braced keys, the same
job hash and field names, the same replies on the ordinary path, checked against the Lua script each command
replaced where one existed; where a reply differs, the native verb refuses an edge the script let through.
This module reads the core job transitions on flat jobs; lanes, flows and the reaper have native verbs of
their own (EMQ.GROUP.CLAIM, EMQ.FLOW.ADD,
EMQ.GROUP.REAP and their siblings), which the Trafikist chapter reads. The verbs that settle, move or recover a job refuse a lane row,
EMQ.COMPLETE and EMQ.FAIL also refuse a flow row, and the
lease verbs and EMQ.RESCHEDULE do not read the lane at all. The module runs on
its own node, loaded with loadmodule
at server start; the Echo service node loads no module. The load gate refuses the load unless every host API
the module calls is present, the module has no arguments, and the host is a Valkey 9 server.
One executor, one protocol: the retired Lua scripts remain the reference semantics, and each native command is held to the script it replaced or, where none existed, to its specification.
One table, fourteen job-core commands
The registration table is the only list of commands the module serves. Fourteen job-core rows (the
read-only EMQ.VERSION and thirteen write verbs), three bus write verbs
(EMQ.APPEND, EMQ.FAILURES.INCR,
EMQ.PUBLISH, which the Elixir bus does not send) and the administrative
EMQ.INSTALL, the one deny-script row: eighteen
EMQ.* rows, beside six TRA.* rows for the module's own
metadata and bitmap commands. Every write verb
registers with EVAL's key-spec, so the server finds the declared keys and checks them against the caller's
ACL exactly as it does for EVAL; its flags are Valkey's, and its ACL categories
follow the flags. Pick a command to read its row.
The registration table · pick a command
arity 6 · flags write deny-oom fast · key-spec EVAL's · ACL write fast
capabilities: claim, claim.pause
declared as: EchoMQ.Jobs @claim2, with paused?/2 folded in
does: leases the oldest pending flat job on server time, the pause read folded in; a paused queue answers empty; stamps lease_epoch.
The key-spec every write verb shares, and two rows of the table, verbatim from the registry:
// trafikey — the key-spec every EMQ.* write verb registers with: begin-search at // argument 1, the key count at index 0, the first key at 1, step 1 — EVAL's own const EVAL_KEYS: &[Keys] = &[Keys { flags: &["READ_WRITE", "ACCESS", "UPDATE"], index: 1, find: Find::Keynum { key_num_idx: 0, first_key: 1, key_step: 1, }, }];
// trafikey — two rows of the registration table: the read-only fence command, which // declares no key and advertises no token, and one write verb with EVAL's key-spec emq_version => crate::emq::version, Row { name: c"EMQ.VERSION", family: Family::Emq, arity: 1, flags: "readonly fast", keys: &[], acl: c"read fast", caps: &[], summary: Some("Wire-6 compatibility facts: wire epoch, module version and major, build, capabilities"), complexity: Some("O(1)"), since: Some("0.1.0") }; // … emq_claim => crate::emq::jobs::claim, Row { name: c"EMQ.CLAIM", family: Family::Emq, arity: 6, flags: "write deny-oom fast", keys: EVAL_KEYS, acl: c"write fast", caps: &["claim", "claim.pause"], summary: Some("Lease the oldest pending flat job on server time; a paused queue answers empty (@claim2 + paused?)"), complexity: Some("O(log N)"), since: Some("0.1.0") };
Six families: admission (EMQ.ENQUEUE,
EMQ.SCHEDULE), claim and leases (EMQ.CLAIM,
EMQ.EXTEND, EMQ.EXTENDS), completion
(EMQ.COMPLETE), scheduling (EMQ.PROMOTE,
EMQ.RESCHEDULE, EMQ.DELAY), failure and
recovery (EMQ.FAIL, EMQ.REPROCESS,
EMQ.REQUEUE, EMQ.STALLED.CHECK) and the
fence (EMQ.VERSION). A negative arity is a minimum. A command's capability
tokens are advertised by EMQ.VERSION only while the command is registered, so a
capability always names a served command. Sweeps and promotion run only when a client calls them: the
module runs no timer.
A command exists by having its row in the table; everything else — registration, ACL categories, the capability list — derives from that one row.
A fence that only reads, on a node of its own
Every write verb reads {emq}:version as the caller and requires the module's wire
identity. It never writes the value. The check order is shape, then layout, then the fence, then the domain
checks, then the write guard, then the writes — so a call any check refuses writes nothing. On a
standalone server two texts carry the refusal:
# the two fence refusals, as the server replies them; <value> is whatever string the key holds
EMQWIRE expected echomq:6.0.0 but {emq}:version is absent
EMQWIRE expected echomq:6.0.0 found <value>
The value is installed administratively, once, before ordinary clients connect;
ordinary clients only read it. EMQ.VERSION reports the facts a client compares
before it trusts a node — the wire identity, the module's major, the capability tokens — without
reading any key. The module serves standalone servers only: it runs on its own node, where
every key is local, and that node holds one fence value. The connector speaks one wire: at boot it calls
EMQ.VERSION, checks the capability facts, and refuses a node whose
{emq}:version is not echomq:6.0.0, so it opens only on a node
that serves the module's value. The second dive
reads the fence line by line.
Read before every write, never written on the module's ordinary path: the fence is a promise the module checks and never changes.
The dives
Three dives read the native module in depth, each grounded in the real trafikey
registry and handlers beside the real echo/apps/echo_mq declarations: the command
form, the fence, and one transition held to its Lua reference.
Commands, not scripts
The command form: the numkeys KEYS ARGV tail, called by name; EVAL's key-spec and the ACL it keeps; EMQ.ENQUEUE's handler and the order difference from the retired script; effects replicated as plain commands.
The native fence
Every write verb reads {emq}:version as the caller; the EMQWIRE texts; who installs the value; EMQ.VERSION, its capability tokens and the client rule; one node, one fence value.
One transition, two executors
The claim as EMQ.CLAIM, held to the retired Lua claim: same keys, same reply; pause folded in, checks before any write, the cached clock, lease_epoch; the restart exemption, the failures budget, and how parity is kept.
Pattern & implementation
Where Redis Patterns Applied teaches the move — a pattern, pushed below the language line, becomes a protocol — this module takes the move one step further: the core job transitions, compiled into the server.
An atomic multi-step update, expressed as one server-side step with every key declared, becomes a protocol when the agreement is pushed below the language line. Patterns become protocol and Atomic updates teach it.
The job-core write verbs in the command form, called by name — each checked against the Lua script
it replaced where one existed, and declared today in EchoMQ.Jobs (the stall sweep in
EchoMQ.Stalled), the rest against their specification — plus the read-only
EMQ.VERSION; a read-only fence on every write; the same keys and fields, the same
replies on the ordinary path, plus lease_epoch and failures.
Take the dives in order. This is the last module of the Protocol chapter: after the third dive, the chapter closes in the workshop.
Up next
After the three dives, close the Protocol chapter in the workshop — decode a real key and build the native line for one transition — or step back to the chapter. The Trafikist chapter opens the module itself: how Valkey loads it, the grammar every job-core write verb runs, and the job core it carries.
References
Sources
- Valkey — Modules introduction — what a Valkey module is: native code loaded into the server that registers commands.
- Valkey — MODULE LOAD — the runtime form of what
loadmoduledoes at server start; the module is loaded at start. - Valkey — Modules API reference — command registration, key-specs and ACL categories, the surface the registration table drives.
- Valkey — EVALSHA — the retired scripts' line, whose
numkeys KEYS ARGVtail the command form keeps. - Valkey — ACL — the key permissions a declared key-spec lets the server check.
Related in this course
- The Protocol — the chapter this module belongs to.
- Immutability & branded ids — the fences that hold the wire still.
- Workshop — the chapter's workshop, up next.
- Trafikist — Valkey and the module in depth.
- redis-patterns · Patterns become protocol — the near side of the door.
- redis-patterns · Coordination — atomic updates and one-slot Lua.