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
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 replypinned: 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
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
A scripted client sends EVALSHA and, on NOSCRIPT, loads the
body and sends it again: it finds a missing script by calling it.
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
- Valkey — Modules introduction — a module registers its own commands at load.
- Valkey — Modules API reference —
RM_GetCommand, the call behindregistry::registered. - Valkey — COMMAND INFO — the server's own command metadata, which the handshake does not need.
- Valkey — GET — the brand read, the handshake's third step.
Related in this course
- The job core, natively — the module this dive belongs to.
- The native fence — the six fields of
EMQ.VERSIONand the brand check on every write. - The registration table — every row of
registry::TABLE, both families. - Standalone only — the server mode behind the empty list on a cluster.
- The lease epoch — the next dive.