jonnifyechomq

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

EMQ.CLAIM registration · flags · arity key-spec ACL categories, set in init EMQ.VERSION capabilities TRA.CAPABILITIES bitmap_commands
EMQ.CLAIM · family Emq · arity 6: exactly 6 arguments, the name included
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

every row 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.

the pattern (Redis Patterns Applied)

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.

the implementation (trafikey)

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

Related in this course