Valkey and the module · dive 02
The registration table
One row per command, and everything else read off the rows: registration, key-specs, ACL categories, capabilities.
registry::TABLE is the only list of commands the module serves. A
row carries the command's name, family, arity, Valkey flags, key-specs, ACL categories and capability
tokens, with the summary, complexity and since strings registered beside them. One macro turns each row into
a registration entry and a trampoline; init proves the table equals the registered
set and sets every row's categories; EMQ.VERSION and
TRA.CAPABILITIES read the rows in table order. Pick a row on the right and read
what it yields.
One row, five outputs · pick a row
flags: write deny-oom fast
key-spec: EVAL's — READ_WRITE ACCESS UPDATE; begin at argument 1; key count at index 0, first key 1, step 1
ACL: write fast ← derived from the flags: write fast (equal)
capabilities: claim, claim.pause → listed by EMQ.VERSION
complexity: O(log N)
A row, field by field
A row is a plain constant. Its key-specs are a list of Keys, each one a set of
key-spec flag names, a begin-search index and a rule for finding the keys after it. Two rules exist:
Keynum, where an argument says how many keys follow (the form
EVAL uses), and Range, a fixed run of keys. The
TRA.* rows with a key use ARGV1: a range that ends where it
begins, so exactly one key, at argument 1.
// trafikey — registry::Keys and the one-key rule the TRA rows use, then registry::Row /// One key-spec: SDK key-spec flag names, begin-search index and the find-keys rule. #[derive(Clone, Copy)] pub struct Keys { pub flags: &'static [&'static str], pub index: i32, pub find: Find, } // … const ARGV1: Find = Find::Range { last_key: 0, steps: 1, limit: 0, }; pub struct Row { pub name: &'static CStr, pub family: Family, pub arity: i64, /// Valkey command flags, as `CreateCommand` takes them. pub flags: &'static str, pub keys: &'static [Keys], pub acl: &'static CStr, /// Capability tokens this command advertises, in the family's order. pub caps: &'static [&'static str], pub summary: Option<&'static str>, pub complexity: Option<&'static str>, pub since: Option<&'static str>, }
The family is one of Emq, Tra
and Probe; the probe rows compile only under the probe-type
feature. The flags are a single string in the form the server's command registration takes. The arity
follows the server's rule: a positive arity is the exact argument count with the command name included, a
negative one the minimum.
Everything a command is, apart from its handler, fits in one constant.
From a row to a registered command
One macro walks the rows. For each it builds a small module holding the row, an
extern "C" callback that hands the call to the row's handler, and a registration
entry in the SDK's list of commands to register; then it lists the same rows, in the same order and under
the same feature gates, as TABLE.
// trafikey — the registry! macro: per row, a trampoline and a registration entry; then TABLE /// Rows in table order. Per row: a module holding the row, its trampoline and its SDK registration /// entry (`COMMANDS_LIST`), all under the row's `#[cfg]`; then [`TABLE`] lists the same rows. macro_rules! registry { ($( $(#[$gate:meta])* $id:ident => $handler:path, $row:expr; )*) => { $( $(#[$gate])* mod $id { use super::*; pub(super) const ROW: Row = $row; extern "C" fn callback(ctx: *mut raw::RedisModuleCtx, argv: *mut *mut raw::RedisModuleString, argc: c_int) -> c_int { dispatch($handler, ctx, argv, argc) } // one entry in the SDK's registration list, registered between preload and init #[linkme::distributed_slice(COMMANDS_LIST)] fn register() -> Result<CommandInfo, ValkeyError> { ROW.info(callback) } } )* /// Every command this build registers, in family order (`EMQ.*` in G1 capability order). pub const TABLE: &[Row] = &[$( $(#[$gate])* $id::ROW, )*]; }; }
ROW.info(callback) maps the row onto the SDK's command
description: the name, the flags, the summary, complexity and since strings, the arity, one key-spec per
Keys entry, and the callback. The callback is the trampoline the SDK's own
#[command] attribute generates, written once: it decodes the arguments, calls the
handler and sends its reply. Because the registration list and TABLE come from the
same rows, they can only disagree if a command registers some other way, and init
checks for that.
Registration is generated from the rows, never written by hand next to them.
Three real rows
An EMQ.* write verb, a TRA.* write command and a
TRA.* read command, verbatim from the table:
// trafikey — registry::TABLE, three rows // a job-core write verb: EVAL's key-spec, write fast, two capability tokens 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") }; // … // a bitmap write: one key at argument 1, no fast flag so slow, plus its twin's bitmap category tra_setbit => crate::tra::bitmap::setbit, Row { name: c"TRA.SETBIT", family: Family::Tra, arity: 4, flags: "write deny-oom", keys: &[Keys { flags: &["READ_WRITE", "ACCESS", "UPDATE"], index: 1, find: ARGV1 }], acl: c"write bitmap slow", caps: &["bitmap"], summary: Some("Set one MSB-first bit of a string; returns the previous bit"), complexity: Some("O(1) or O(N) in counted bytes"), since: Some("0.1.0-tra7") }; // … // a read: READ_ONLY alone, fast, and the string category of its built-in twin tra_strlen => crate::tra::bitmap::strlen, Row { name: c"TRA.STRLEN", family: Family::Tra, arity: 2, flags: "readonly fast", keys: &[Keys { flags: &["READ_ONLY"], index: 1, find: ARGV1 }], acl: c"read string fast", caps: &["bitmap"], summary: Some("Logical byte length of a string"), complexity: Some("O(1)"), since: Some("0.1.0-tra7") };
Read the three against each other. EMQ.CLAIM declares
its keys the way EVAL does, so the server finds the three declared keys from the key
count and checks them against the caller's ACL before the handler runs; it advertises two capability
tokens. TRA.SETBIT declares one key at argument 1 with update access; it carries no
fast flag, so its category is slow, and it adds
bitmap, the category of the built-in command it parallels.
TRA.STRLEN is a read whose key-spec carries READ_ONLY without
ACCESS, as it returns a length and not the value, and its added category is
string. Every bitmap row
advertises the same token, bitmap, and TRA.CAPABILITIES
counts them.
One row stands apart: EMQ.INSTALL, the administrative write that initializes the
wire-6 brand, is the only row with the deny-script flag and the only write row with admin, so its categories are write admin dangerous slow, and it declares a key-spec
of its own with INSERT access. It advertises no token. Pick it in the explorer
above.
The three bus rows sit between EMQ.STALLED.CHECK and
EMQ.INSTALL. EMQ.APPEND,
EMQ.FAILURES.INCR and EMQ.PUBLISH declare
EVAL's key-spec, as the job-core verbs do, and advertise one token each:
append, failures.incr and publish.
EMQ.APPEND and EMQ.PUBLISH take arity −5, one below
the valid minimum 1 <key> <n> <evt> <m> with the name, so a
lone entry missing its <m> reaches the handler's own
entries do not match n refusal instead of the server's arity error.
EMQ.APPEND carries no fast flag, so its category is
slow.
Three rows, three key-specs, three category sets: the differences between commands are differences between rows.
ACL categories follow the flags
A row's categories are not free text. The rule is stated at the top of the registry and pinned by a unit
test: write gives write, readonly
gives read, fast gives fast and
its absence slow, admin gives
admin dangerous; a TRA.* bitmap row may add its built-in
twin's data-type category, and nothing else may appear.
// trafikey — the rule, as the registry tests derive it from a row's flags /// The rule the decision fixes: categories follow flags; only TRA rows add a data-type category. fn derived(row: &Row) -> Vec<&'static str> { let has = |f: &str| row.flags.split(' ').any(|x| x == f); let mut cats = vec![]; if has("write") { cats.push("write"); } if has("readonly") { cats.push("read"); } cats.push(if has("fast") { "fast" } else { "slow" }); if has("admin") { cats.extend(["admin", "dangerous"]); } cats }
The test that uses it, categories_follow_flags, checks
every row: each derived category is present, nothing else is present apart from a TRA row's
bitmap or string, and every row is exactly one of
write or readonly. The categories reach the server in
init, which first proves the table and the registered set equal:
// trafikey — registry::set_acl_categories, init's first step /// Assign every row's ACL categories after registration, inside OnLoad. Distinct rows, as many /// as registered commands (a stray `#[command]` outside the table counts), each found in this /// module: the table and the registered set are equal. Any miss, or a category the host rejects /// (module.c `VM_SetCommandACLCategories`), refuses the load. pub fn set_acl_categories(ctx: &Context) -> Status { // … let distinct = TABLE .iter() .enumerate() .all(|(i, row)| TABLE.iter().take(i).all(|other| other.name != row.name)); // a duplicate name, or a registered command with no row, refuses the load if !distinct || TABLE.len() != COMMANDS_LIST.len() { ctx.log_warning("trafikey: registration table does not match the registered commands"); return Status::Err; } for row in TABLE { // the command as this module registered it, then its categories from the row let command = unsafe { get(ctx.ctx, row.name.as_ptr()) }; if command.is_null() || unsafe { set(command, row.acl.as_ptr()) } != raw::Status::Ok as c_int { ctx.log_warning(&format!( "trafikey: ACL categories refused for {}", row.name.to_string_lossy() )); return Status::Err; } } Status::Ok }
So the categories COMMAND INFO reports for an
EMQ.* or TRA.* command are the row's, set while the module
loads; a row whose categories the server rejects refuses the whole load rather than leaving one command without
them.
The categories are derived, tested and then set in one pass at load: a command is never registered without them.
Capabilities: the rows, in table order
Two commands read the table back at run time. Both call one function: the capability tokens of one family's rows whose command is registered, in table order.
// trafikey — registry::capabilities and the registered test it takes /// 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 Emq
tokens with spaces; TRA.CAPABILITIES counts the Tra tokens
equal to bitmap:
// trafikey — EMQ.VERSION, the first reader 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 the list is empty: the third dive reads this branch let caps = if host::serving() { registry::capabilities(Family::Emq, |name| registry::registered(ctx, name)) .collect::<Vec<_>>() .join(" ") } else { String::new() }; // … }
// trafikey — TRA.CAPABILITIES, the second reader pub(crate) fn capabilities(ctx: &Context, _: Vec<ValkeyString>) -> ValkeyResult { increment(&CAPS_CALLS); // Derived from the registration table: only registered bitmap rows are counted. let bitmap_commands = registry::capabilities(Family::Tra, |name| registry::registered(ctx, name)) .filter(|token| *token == "bitmap") .count() as i64; // … }
In a loaded module every row is registered, since init
refuses the load otherwise, so on a standalone server the live list is the full one: seventeen tokens from sixteen commands
(EMQ.CLAIM carries two; EMQ.VERSION and
EMQ.INSTALL carry none), and four bitmap commands. The
registered test is what makes "a token always names a served command" true by
construction, and the unit tests drive the function with other answers to pin the order:
// trafikey — the EMQ.VERSION test: the token list and its order come from the table /// 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" ); }
The capability derivation · choose which rows count as registered
EMQ.VERSION capabilities (17 tokens): enqueue claim claim.pause extend extends complete.flat schedule promote reschedule delay fail.flat reprocess requeue stalled.check append failures.incr publish
TRA.CAPABILITIES bitmap_commands: 4
pinned by a unit test: the EMQ list yes · the bitmap count yes
The order is the table's, never the order a client asked in: reordering the rows is the only way to reorder the list, and a test pins it.
An atomic step declares every key it touches, so the server can place the keys and police them before the step runs. Atomic updates teaches it.
Each row declares its command's keys as a key-spec, its flags and its ACL categories, and the module derives registration, the category check at load and both capability readers from the same rows.
Recap — the registration table
A row holds a command's name, family, arity, flags, key-specs, ACL categories and capability tokens. A
macro turns every row into a trampoline and a registration entry and lists the rows again as
TABLE. The categories follow the flags under a tested rule and are set in
init, which refuses the load if the table and the registered set differ.
EMQ.VERSION lists the Emq tokens and
TRA.CAPABILITIES counts the bitmap rows, both in table order. The next dive reads
what happens to all of it on a cluster-enabled server.
Next in this module: Standalone only — every EMQ command stays registered on a cluster, refuses its writes, and leaves the capability list empty.
References
Where the table and its derivations are grounded, and the server surfaces they fill.
Sources
- Valkey — Modules API reference — command creation, command info, key-specs and ACL categories for a module command.
- Valkey — COMMAND INFO — where a command's arity, flags, key-specs and ACL categories are reported.
- Valkey — ACL — the command categories (
write,read,fast,slow,admin,dangerous,bitmap,string). - Valkey — EVAL — the key-count form every
EMQ.*write verb's key-spec repeats. - Valkey — SETBIT — the built-in twin whose
bitmapcategoryTRA.SETBITadds.
Related in this course
- Valkey and the module — the module this dive belongs to.
- Loading the module — the gate before registration and the init after it.
- Commands, not scripts — why EVAL's key-spec keeps the ACL where it was.
- The native fence —
EMQ.VERSION's reply and the client rule. - redis-patterns · Atomic updates — declared keys, the near side of the door.