jonnifyechomq

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

admission2 verbs claim & leases3 verbs completion1 verb scheduling3 verbs failure & recovery4 verbs the fenceVERSION one standalone node · loadmodule at start · fourteen job-core commands
EMQ.CLAIM · claim and leases
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.

built page ready to read

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.

the pattern (Redis Patterns Applied)

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 implementation (trafikey)

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

Related in this course