jonnifyechomq

The native module · dive 01

Commands, not scripts

One tail, called by name: the retired EVALSHA line with the sha replaced by the command.

The native module registers each core job transition as a command; lanes, flows and the reaper have native verbs of their own. The form is EMQ.<VERB> numkeys key … arg … — the tail of the retired EVALSHA <sha> numkeys KEYS ARGV line, with the sha replaced by the command name. Nothing is loaded and nothing is hashed; the server already holds the handler. Each write verb registers with EVAL's key-spec, so Valkey checks the declared keys against the caller's ACL as it does for EVAL, and the keys a handler reaches beyond the declared ones are opened as the caller too. The handler then runs the retired script's checks in a stricter order — shape, layout, the fence, then the domain — and its effects replicate as plain commands. Build a line on the right, then read EMQ.ENQUEUE beside the @enqueue script it replaced.

Build the line · pick a verb and a queue

EMQ.ENQUEUE EVALSHA a5980be1… the retired Lua head the same tail 2 emq:{orders}:job:JOB0KHTOWnGLuC emq:{orders}:pending JOB0KHTOWnGLuC <payload> the native head
EMQ.ENQUEUE 2 emq:{orders}:job:JOB0KHTOWnGLuC emq:{orders}:pending JOB0KHTOWnGLuC <payload>
retired: EVALSHA a5980be13628669477e64a5afad5370194ea6f1c 2 emq:{orders}:job:JOB0KHTOWnGLuC emq:{orders}:pending JOB0KHTOWnGLuC <payload>
handle: EchoMQ.Jobs @enqueue · numkeys 2 · host side: the host built both keys through EchoMQ.Keyspace; job_key/2 runs the full branded-id check

One form, called by name

The connector builds its line in EchoMQ.Connector.eval/5, its one seam for queue operations: the key count, the declared keys, the values, behind the verb the script declares with Script.native/2. A script with no declared verb is refused with {:unsupported, name} before anything is sent, and so is a raw EVAL or EVALSHA in any pipeline. Before the module, this seam sent EVALSHA with the script's sha and, on a cold script cache, loaded the source on NOSCRIPT and retried. That load-once dance is gone; the tail it sent is the tail the command keeps.

# echo_wire — EchoMQ.Connector, the one seam for queue operations
# The line is the key count, the declared keys and the values; the verb the
# script declares (Script.native/2,3) goes in front. No sha, nothing to load.
def eval(conn, %Script{} = s, keys, argv, timeout \\ 5_000) do
  parts = [Integer.to_string(length(keys)) | keys ++ argv]

  case GenServer.call(conn, {:eval, s.name, s.verb, parts}, timeout) do
    {:ok, [{:error_reply, msg}]} -> {:error, {:server, msg}}
    {:ok, [reply]} -> {:ok, reply}
    {:error, _} = err -> err
  end
end

The native line is the same parts with the sha replaced by the name:

# retired: the @enqueue Lua script's line, before the module (history)
EVALSHA <sha1 of the script>                    2 emq:{orders}:job:JOB0KHTOWnGLuC emq:{orders}:pending JOB0KHTOWnGLuC <payload>
# today: EchoMQ.Jobs @enqueue is Script.native(:enqueue, "EMQ.ENQUEUE"); no sha, nothing to load
EMQ.ENQUEUE                                       2 emq:{orders}:job:JOB0KHTOWnGLuC emq:{orders}:pending JOB0KHTOWnGLuC <payload>

On the native side the server holds the handler, so there is no sha to precompute, no SCRIPT LOAD on a cold cache, and no NOSCRIPT to catch. The command name is the dispatch.

A name is a sha that never goes stale: the handler is in the server, so the line that runs it needs no load step.

Why EVAL's key-spec matters

A module command tells the server where its keys are through a key-spec. Every EMQ.* write verb registers the one EVAL has — begin-search at argument 1, the key count at index 0, the first key at 1, step 1, flags READ_WRITE ACCESS UPDATE — so the server finds the declared keys and checks them against the caller's ACL before the handler runs, exactly as it does for a script.

// trafikey — the one key-spec every write verb registers with
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 — the EMQ.ENQUEUE row of the registration table: its handler, arity,
// Valkey flags, key-spec, ACL categories and the capability token it advertises
emq_enqueue => crate::emq::jobs::enqueue, Row { name: c"EMQ.ENQUEUE", family: Family::Emq, arity: 6,
    flags: "write deny-oom fast", keys: EVAL_KEYS, acl: c"write fast", caps: &["enqueue"],
    summary: Some("Admit a flat JOB-namespaced job: row and pending entry, idempotent (@enqueue)"),
    complexity: Some("O(log N)"), since: Some("0.1.0") };

The flags are Valkey's: write, deny-oom (the server refuses the command while it is over its maxmemory limit) and fast. The ACL categories follow the flags — write fast here; a write row without fast is write slow. Keys a handler reaches beyond the declared ones (<base>meta and <base>job:<id> built from the queue's base, and the fixed {emq}:version) are outside the key-spec, so the handler opens each one as the caller through one checked accessor before reading it and before the first write; a denial is NOPERM No permissions to access a key with nothing written. Only the administrative EMQ.INSTALL is deny-script; no job-core verb is.

EVAL's key-spec keeps the ACL where it was; the checked accessor extends it to every key a handler reaches beyond the declared ones.

EMQ.ENQUEUE beside its retired script

Three beats: the Elixir declaration that sends the verb, the retired Lua body it replaced, then the native handler.

Beat one — the declaration. EchoMQ.Jobs @enqueue, which is Script.native(:enqueue, "EMQ.ENQUEUE"), sent by enqueue/5: the host builds both keys through the keyspace and hands them in declared.

# echo_mq — EchoMQ.Jobs
# The host builds KEYS through the keyspace (job_key/2 runs the full branded-id
# check) and hands [row, pending] declared and [id, payload] as values.
def enqueue(conn, queue, job_id, payload, opts \\ [])
    when is_binary(job_id) and is_binary(payload) do
  Keyspace.valid_queue!(queue)
  via = Keyword.get(opts, :via, Connector)
  keys = [Keyspace.job_key(queue, job_id), Keyspace.queue_key(queue, "pending")]

  case via.eval(conn, @enqueue, keys, [job_id, payload]) do
    # 1: the row and the pending entry were written
    {:ok, 1} -> {:ok, :enqueued}
    # 0: the row existed, nothing written
    {:ok, 0} -> {:ok, :duplicate}
    # the JOB gate refused
    {:error, {:server, "EMQKIND" <> _}} -> {:error, :kind}
    other -> other
  end
end

Beat two — the retired script body, the reference the handler is held to. Its gate was the first line.

-- the retired @enqueue Lua script (history) — KEYS[1] = the job row, KEYS[2] = the pending set
-- The gate comes first: the three-byte JOB prefix, nothing more.
if string.sub(ARGV[1], 1, 3) ~= 'JOB' then
  return redis.error_reply('EMQKIND job id must be JOB-namespaced')
end
-- idempotency: an existing row answers 0
if redis.call('EXISTS', KEYS[1]) == 1 then
  return 0
end
-- the three-field row, then the pending entry at score 0
redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2])
redis.call('ZADD', KEYS[2], 0, ARGV[1])
return 1

Beat three — the native handler. EMQ.ENQUEUE 2 emq:{q}:job:<id> emq:{q}:pending <id> <payload>, arity 6. Read it top to bottom: every check comes before the first write.

// trafikey — EMQ.ENQUEUE, the verb EchoMQ.Jobs @enqueue declares
/// G2 `EMQ.ENQUEUE 2 emq:{q}:job:<id> emq:{q}:pending <id> <payload>`.
pub(crate) fn enqueue(ctx: &Context, args: Vec<ValkeyString>) -> ValkeyResult {
    // shape: numkeys is exactly 2 and both keys are present, then exactly two ARGV
    let s = host::shape("EMQ.ENQUEUE", &args, 2)?;
    s.expect_argc(2)?;
    // layout: KEYS[2] is emq:{q}:pending for one non-empty q, and KEYS[1] is
    // byte for byte emq:{q}:job: + ARGV[1]; anything else is refused before any read
    if !enqueue_layout(&s) {
        return Err(s.layout_error());
    }
    // the fence: {emq}:version must hold the module's wire identity; read only
    host::fence(ctx)?;
    let (row, pending, id, payload) = (s.key(1), s.key(2), s.arg(1), s.arg(2));
    // the kind door: the first three bytes are JOB, as string.sub(ARGV[1], 1, 3) in Lua
    if !keyspace::is_job_id(id) {
        return Err(errors::emq(errors::KIND_JOB));
    }
    // idempotency: an existing row answers 0 and writes nothing
    match host::read(ctx, cmd::EXISTS, &[row])? {
        Reply::Int(0) => {}
        Reply::Int(_) => return Ok(ValkeyValue::Integer(0)),
        _ => return Err(errors::emq(errors::HOST_REPLY)),
    }
    // every touched key is typed before the first write: pending is a zset or absent
    host::expect_type(ctx, pending, b"zset")?;
    // the write guard: a write-paused server refuses here, with nothing written
    let w = host::begin_writes(ctx)?;
    // the same two writes as the script, each replicated as itself
    w.call(
        cmd::HSET,
        &[
            row,
            b"state",
            b"pending",
            b"attempts",
            b"0",
            b"payload",
            payload,
        ],
    )?;
    w.call(cmd::ZADD, &[pending, b"0", id])?;
    Ok(ValkeyValue::Integer(1))
}
// trafikey — the layout check the handler calls
/// G2 layout: `KEYS[2]` = `emq:{q}:pending`, `KEYS[1]` = `emq:{q}:job:` + `ARGV[1]`.
fn enqueue_layout<S: Bytes>(s: &Shape<'_, S>) -> bool {
    Queue::from_key(s.key(2), b"pending").is_some_and(|q| s.key(1) == q.job(s.arg(1)).as_slice())
}
// trafikey — keyspace::is_job_id, the kind door the handler calls
// the kind door (R6, parity): the first three bytes are JOB; string.sub(id, 1, 3) of a
// shorter id is shorter, so JO fails as it does in Lua
pub fn is_job_id(id: &[u8]) -> bool {
    id.starts_with(b"JOB")
}

The order difference. The retired script gated first: its first line was the JOB check. The handler gates later: shape, then layout, then the fence, and only then the JOB gate, EXISTS, the type of the pending set, the write guard, and the writes. The layout check is new on the native side — the script had none, because the host built its keys — and it is exact: KEYS[1] must equal emq:{<q>}:job: + ARGV[1] byte for byte, with a non-empty queue name that holds no brace and no NUL; otherwise the reply is ERR EMQ.ENQUEUE keys do not match the wire-4 layout. The kind gate is the same three-byte prefix as in Lua; the full fourteen-byte branded-id check stays host-side, in EchoMQ.Keyspace.job_key/2. A duplicate answers 0 with no write, as the script's did.

Same keys and writes, and on the ordinary path the same replies; the native handler adds a layout check and a fence in front of the gate the script runs first, and refuses what they catch before any write.

The check order, on bad input

Every check runs before the first write, so a call any check refuses leaves the dataset untouched. Run fixed inputs through the handler's checks in order and read which one refuses, what the reply is, and what the script answers on the same input.

EMQ.ENQUEUE check order · pick an input

shape layout fence JOB gate EXISTS type guard writes every check passed → HSET, ZADD → reply 1
a good call — reply 1
native: shape ok (numkeys 2, two keys, two ARGV) · layout ok (KEYS[1] = emq:{orders}:job: + ARGV[1]) · fence ok (echomq:6.0.0) · JOB gate ok (the first three bytes are JOB) · EXISTS ok (0, no row) · type ok (pending is a zset) · guard ok (not write-paused) · writes ok (HSET emq:{orders}:job:JOB0KHTOWnGLuC state pending attempts 0 payload order-7; ZADD emq:{orders}:pending 0 JOB0KHTOWnGLuC)
retired script: JOB gate ok · EXISTS 0 · the same HSET and ZADD → 1

A refusal is a reply and nothing else: the dataset after a refused call is the dataset before it.

Effects replicate as plain commands

A handler writes only through the write guard, and every write is issued with the replicate flag: the AOF and the replicas receive the effect — HSET <row> state pending attempts 0 payload <payload>, ZADD <pending> 0 <id> — never the verbatim EMQ.* command, and the writes of one command reach them inside one MULTI/EXEC. A replica or a restart replays the same keys the script would have written, with no module needed to read the log.

// trafikey — the write path: the guard, then writes that replicate as their effects
const WRITE: &CStr = c"v!E";
// …
/// Proof that the write guard passed in this command. It is the only way to write.
pub struct Writes<'c> {
    ctx: &'c Context,
}
impl Writes<'_> {
    /// A replicated host write (`"v!E"`): the AOF and replicas get this command with these
    /// arguments, for example `ZADD <key> <absolute score> <id>`. A failure after an earlier
    /// write leaves that write in place; validate everything before [`begin_writes`].
    pub fn call(&self, name: &CStr, args: &[&[u8]]) -> Result<Reply, ValkeyError> {
        call(self.ctx, WRITE, name, args)
    }
}

/// The write guard. Call it after every read and validation, immediately before the first
/// write: with `AvoidReplicaTraffic` set (CLIENT PAUSE, manual failover) it refuses with
/// `ERR EMQ write-paused` and nothing has been written (C7).
pub fn begin_writes(ctx: &Context) -> Result<Writes<'_>, ValkeyError> {
    let avoid = api(unsafe { raw::RedisModule_AvoidReplicaTraffic })?;
    if unsafe { avoid() } != 0 {
        return Err(errors::emq(errors::WRITE_PAUSED));
    }
    Ok(Writes { ctx })
}
the pattern (Redis Patterns Applied)

Atomic updates as one server-side step, with every key declared so the server can place and police them. Atomic updates and Coordination teach it.

the implementation (trafikey)

EMQ.ENQUEUE keeps @enqueue's keys, replies and writes, registers with EVAL's key-spec, refuses bad shape and layout before any read, and replicates its two writes as themselves.

The log holds HSET and ZADD, never EMQ.ENQUEUE: what replicates is the effect, so the effect is all a replica needs.

Recap — commands, not scripts

One tail, called by name: the command form keeps the retired EVALSHA line's tail and replaces the sha with the name. EVAL's key-spec keeps ACL checks where they were; derived keys are opened as the caller. The native handler checks shape, layout and the fence before the gate the retired script ran first, and a call those checks refuse writes nothing. Effects replicate as plain commands. The next dive reads the fence every write verb passes through.

Next in the native module: The native fence — the read on every write, the EMQWIRE texts, who installs the value, and why the connector opens only on a node that holds the module's value.

References

Where the command form and its registration are grounded, and the engine behaviour they rely on.

Sources

Related in this course