jonnifyechomq

The job core, natively · dive 01

Fourteen capabilities

The capability list is a reading of the registration table: the tokens of every registered EMQ row, in table order. A client checks it before its first call.

EMQ.VERSION answers six fields, and the sixth, capabilities, is never written by hand. Each command trafikey registers is one row of registry::TABLE, and a row's caps field holds the tokens it advertises. The handler filters the table for the EMQ rows whose command is registered, flattens their tokens in row order and joins them with one space. A crate test pins the result, and a wire-6 client compares it with the tokens it needs before it sends a single write. Toggle the rows, then follow four clients through the check.

A token lives on its row

The module registers every command from one table. A row names the command, its family, its arity, its flags, its key-spec, its ACL categories and, in caps, the capability tokens it advertises. The field's own comment states the rule the list follows: the tokens come in the family's order.

// trafikey — registry::Row, the fields of one registration row
pub struct Row {
    pub name: &'static CStr,
    pub family: Family,
    pub arity: i64,
    // …
    /// Capability tokens this command advertises, in the family's order.
    pub caps: &'static [&'static str],
    // …
}

The EMQ family has eighteen rows. Thirteen are the write verbs of the job core and three are the bus verbs that follow them in the table, EMQ.APPEND, EMQ.FAILURES.INCR and EMQ.PUBLISH; each of those sixteen carries its tokens, and EMQ.VERSION and EMQ.INSTALL carry caps: &[]. EMQ.CLAIM is the one row with two tokens: claim for the lease and claim.pause for the pause read folded into the same command.

// trafikey — registry::TABLE, its first EMQ row and its third: VERSION carries no token,
// CLAIM carries two
registry! {
    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") };
    // …

EMQ.INSTALL registers in every build. It writes the brand {emq}:version once, under administrative authority, and it advertises no token: a wire-6 client never requires an install capability, because it never seeds the brand.

Fourteen job-core tokens on thirteen rows, three bus tokens on three more, and two rows with none: the list has no entry a row does not carry.

Derived, never a constant

registry::capabilities walks the table once. It keeps the rows of the requested family whose command passes the registered test, and it flattens their caps in the order the rows stand. registry::registered answers that test through the module API's RM_GetCommand, which only finds the calling module's own commands: a command another module registered does not count.

// trafikey — registry::capabilities and registry::registered: filter the table by family and
// registration, flatten the tokens in table order; ask the server whether a name is registered
/// The capability tokens of `family`'s rows whose command is `registered`, in table order.
pub fn capabilities(
    family: Family,
    registered: impl Fn(&CStr) -> bool,
) -> impl Iterator<Item = &'static str> {
    TABLE
        .iter()
        .filter(move |r| r.family == family && registered(r.name))
        .flat_map(|r| r.caps.iter().copied())
}
// …
/// Whether this module registered `name` (`RM_GetCommand` only sees the caller's own commands).
pub fn registered(ctx: &Context, name: &CStr) -> bool {
    unsafe { raw::RedisModule_GetCommand }
        .is_some_and(|get| !unsafe { get(ctx.ctx, name.as_ptr()) }.is_null())
}

emq::version joins the iterator with one space and answers it as the last of six fields. On a standalone server host::serving() is true and the derived text is the reply. On a cluster-enabled server every EMQ row stays registered and every write verb refuses at its fence, so the handler answers the empty text instead. The other five fields, and the rule a client applies to them, are in the Protocol's native fence; this dive follows the sixth.

// trafikey — emq::version, where the capabilities field comes from
pub(crate) fn version(ctx: &Context, _: Vec<ValkeyString>) -> ValkeyResult {
    // G1 order is the EMQ rows' table order; a token is advertised only while its command is
    // registered (R14), so a capability always names a served command.
    // on a cluster-enabled server every EMQ row stays registered but refuses, so the list is empty
    let caps = if host::serving() {
        registry::capabilities(Family::Emq, |name| registry::registered(ctx, name))
            .collect::<Vec<_>>()
            .join(" ")
    } else {
        String::new()
    };
    // … the other five fields: wire, module, module_version, module_major, build
    text(b"capabilities");
    text(caps.as_bytes());
    Ok(ValkeyValue::NoReply)
}

Derive the list · choose the registered rows

the test's cases
server mode
registry::TABLE18 EMQ rows, in order filterfamily + registered flat_map caps, joinone space
capabilities = "enqueue claim claim.pause extend extends complete.flat schedule promote reschedule delay fail.flat reprocess requeue stalled.check append failures.incr publish"
rows: 16 of 16 token rows registered, 17 tokens in table order; EMQ.VERSION and EMQ.INSTALL add none
mode: standalone, so host::serving() is true and the derived text is the reply
pinned: the test’s first case, caps(|_| true)

The list is the table filtered by registration: a row that is not registered contributes nothing, and a cluster-enabled server answers no token at all.

The test that pins it

The order is part of the contract: the version specification lists the tokens in row order and says the list is derived from the same registration as the commands, never a constant. The crate's test holds both halves. It calls registry::capabilities with three stand-ins for the registration test, so it runs without a server.

// trafikey — the crate's test that pins the list: every row, no row, ENQUEUE and CLAIM
    /// G1 pins the token list and its order; the registry derives it from the EMQ rows, so a
    /// reordered table cannot silently reorder G1.
    #[test]
    fn capabilities_follow_registration_in_grammar_order() {
        let caps = |registered: fn(&std::ffi::CStr) -> bool| {
            registry::capabilities(Family::Emq, registered)
                .collect::<Vec<_>>()
                .join(" ")
        };
        assert_eq!(caps(|_| true), "enqueue claim claim.pause extend extends complete.flat \
                                    schedule promote reschedule delay fail.flat reprocess requeue stalled.check \
                                    append failures.incr publish");
        assert_eq!(caps(|_| false), "");
        assert_eq!(
            caps(|c| c == c"EMQ.ENQUEUE" || c == c"EMQ.CLAIM"),
            "enqueue claim claim.pause"
        );
    }

Every row registered gives seventeen tokens, the fourteen of the job core and then append failures.incr publish; no row gives the empty text; EMQ.ENQUEUE and EMQ.CLAIM alone give enqueue claim claim.pause. A reordered table fails the first assertion, and a token left off a row, or added to EMQ.VERSION's, fails it too. The test pins the derivation; the cluster branch sits in the handler, outside the iterator it calls.

One assertion per case: the full list, the empty list and a prefix, each in table order.

A client checks before it calls

The version specification states the client's side as rule R14: a client requires wire to be equal, module_major to be equal, and every capability it needs to be listed. A capability is listed only when its command is registered, so a client can never pass this check and then hit unknown command. The wire-6 contract turns it into a handshake of three steps: call EMQ.VERSION, where an unknown command reply means the server has no native EMQ; apply R14, where build and module_version are reported and never compared; then read GET {emq}:version and refuse anything but the module's brand.

A client may list only the tokens of the commands it sends, not the whole set. A flat worker needs the claim, the lease extension, completion and failure; a scheduler needs scheduling and promotion; an operator needs the dead-letter return and the stall sweep. A worker for lane jobs needs grouped completion, and no row carries a complete.group token. Pick a client and a node.

The handshake · pick a client and a node

client
node
1 · EMQ.VERSIONa six-field map 2 · R14every token listed 3 · the brandechomq:6.0.0 the call4 commands
1 · EMQ.VERSION → a map of six fields: wire, module, module_version, module_major, build, capabilities
2 · R14 → wire echomq:6.0.0 equal; module_major equal to the client’s; needs claim extend complete.flat fail.flat in "enqueue claim claim.pause extend extends complete.flat schedule promote reschedule delay fail.flat reprocess requeue stalled.check append failures.incr publish"
  every needed token is listed
3 · GET {emq}:version → echomq:6.0.0
calls EMQ.CLAIM, EMQ.EXTEND, EMQ.COMPLETE, EMQ.FAIL; every write verb repeats the brand check at its own fence

Each refusal happens before the first write. A cluster-enabled server answers EMQ.VERSION but lists no token, so every client stops at R14. A node without the module has no EMQ.VERSION to answer, and a node whose brand is absent passes R14 and stops at the read: only an administrator's EMQ.INSTALL writes the brand. The lane worker stops on every node, because grouped completion has no native row.

EchoMQ's connector runs this handshake when it opens with wire: 6. Its list of needed tokens is fixed: the fourteen job-core tokens, requeue and reschedule among them although no script of the library stands in for those two, and none of the three bus tokens. A missing token, a different wire or a different module_major refuses the connection with {:r14, failures}, which reports every condition that failed; a node without the module refuses it with {:emq_absent, msg} and an absent brand with {:version_fence, :absent}. Each of these refusals is fatal: the connector stops instead of reconnecting, and it never writes the brand. The default wire: 4 connection makes no EMQ.VERSION call: it reads the brand and seeds echomq:4.0.0 on an empty node.

The client compares tokens, not builds: what it can call is exactly what the server registered.

Pattern & implementation

the scripted form (the Lua layer)

A scripted client sends EVALSHA and, on NOSCRIPT, loads the body and sends it again: it finds a missing script by calling it.

the native form (trafikey)

A native client reads capabilities first and refuses a node that lacks a token it needs. There is nothing to load: a command exists because its row is registered, and the list says so.

The next dive reads one field the native rows add: the lease epoch.

References

Sources

Related in this course