jonnifyechomq

Valkey and the module · dive 03

Standalone only

Loaded and registered on a cluster-enabled server too; served only on a standalone one.

The module reads the server mode once, while it loads, and keeps the answer for the life of the process. On a standalone server nothing changes. On a cluster-enabled server the module still loads and every command stays registered, but every EMQ.* write verb refuses with an EMQWIRE text before it reads a key, EMQ.INSTALL refuses after its shape check, EMQ.VERSION answers with an empty capability list, and the TRA.* commands carry no mode check at all. Send a call to each kind of server on the right.

One call, three server modes · pick both

shape mode check key reads reply admitted · the fence reads {emq}:version next
EMQ.CLAIM 3 emq:{orders}:pending emq:{orders}:active emq:{orders}: 30000 on a standalone server
shape: numkeys 3, one ARGV, KEYS[3] the base with its pending and active, lease_ms 30000 — ok
mode check: standalone — admitted
reads: the fence reads {emq}:version as the caller, then the claim's own reads
reply: the claim runs as on any standalone node

The mode, read once at load

init's third step asks the server one thing: is the context flagged as a cluster? The answer goes into a process-wide cell that can be set once. Whether a server runs in cluster mode is a startup setting, so the answer cannot change while the module is loaded, and no command needs to ask again.

// trafikey — the server mode, cached by init
// whether the server is cluster-enabled, read once at load: cluster-enabled is a startup-only
// setting, so the value cannot change while the module is loaded
static CLUSTER: OnceLock<bool> = OnceLock::new();

// init's third step; a second call keeps the first value
pub(crate) fn load_mode(ctx: &Context) {
    let _ = CLUSTER.set(ctx.get_flags().contains(ContextFlags::CLUSTER));
}

The read is RedisModule_GetContextFlags, the last name in the load gate's API list, so a host without it is refused before this line can run. The step itself cannot fail: it records true or false and init moves on to the shared API.

One read at load, one cached boolean: every later call consults memory, never the server.

Three answers, and the test that pins them

The cell can hold false, true, or nothing at all, if init never recorded it. One pure function turns those three states into three answers, and two small functions put it to use: standalone for the commands that refuse, serving for the one that only reports.

// trafikey — host::mode_refusal, standalone and serving
// pure: standalone admits; a cluster refuses with its EMQWIRE text; a mode init never
// recorded refuses as a host failure rather than guess
fn mode_refusal(cluster: Option<bool>) -> Result<(), ValkeyError> {
    match cluster {
        Some(false) => Ok(()),
        Some(true) => Err(errors::emq(errors::WIRE_CLUSTER)),
        None => Err(errors::emq(errors::HOST_CALL)),
    }
}

// refuse on a cluster-enabled server, before any key is read or written
pub fn standalone() -> Result<(), ValkeyError> {
    mode_refusal(CLUSTER.get().copied())
}

// EMQ is served only on a server known to be standalone; EMQ.VERSION advertises its
// capabilities only then, so a client following the version contract refuses a cluster
pub fn serving() -> bool {
    CLUSTER.get() == Some(&false)
}

The two texts come from the closed error registry. The cluster refusal is one text in the EMQWIRE class, beside the two fence texts; the unknown mode answers with the generic host-failure text, since a loaded module always has its mode recorded and the arm exists only so that a missing value refuses rather than guesses.

// trafikey — the two texts, from emq::errors
// a cluster-enabled server: refused at the fence, and by EMQ.INSTALL after its shape checks
pub const WIRE_CLUSTER: &str = "EMQWIRE expected a standalone server but cluster mode is enabled";
// …
/// RM_Call returned no reply object, or a host API pointer is missing.
pub const HOST_CALL: &str = "ERR EMQ host call failed";
// trafikey — the unit test that pins the three answers
#[test]
fn a_cluster_refuses_and_an_unknown_mode_refuses() {
    assert!(mode_refusal(Some(false)).is_ok());
    assert_eq!(
        errors::text(&mode_refusal(Some(true)).unwrap_err()),
        errors::WIRE_CLUSTER
    );
    assert_eq!(
        errors::text(&mode_refusal(None).unwrap_err()),
        errors::HOST_CALL
    );
}

Standalone admits, a cluster refuses with its own text, and an unknown mode refuses rather than guesses.

Where the check runs, and where it does not

Two call sites hold standalone(). The first is the fence, the read every EMQ.* write verb makes after its shape and layout checks: the mode check is its first line, ahead of resolving the caller and ahead of the read of {emq}:version. So on a cluster-enabled server a well-formed write is refused having read nothing and written nothing, and a malformed one still gets its own shape reply first.

// trafikey — host::fence, the first lines
pub fn fence(ctx: &Context) -> Result<Caller, ValkeyError> {
    // the mode check, before the caller is resolved and before {emq}:version is read
    standalone()?;
    let caller = Caller::of(ctx)?;
    caller.open(keyspace::VERSION_KEY, Access::Read)?;
    // …
}

The second is EMQ.INSTALL, which runs no fence of its own kind: it installs the value the fence reads. It checks its shape, then the mode, then makes its one read:

// trafikey — EMQ.INSTALL, the administrative write
/// `EMQ.INSTALL 1 {emq}:version` (install.md#grammar).
pub(crate) fn install(ctx: &Context, args: Vec<ValkeyString>) -> ValkeyResult {
    check_shape(&args)?;
    // a cluster refuses here: after the shape check, before the one read
    host::standalone()?;
    match decide(host::read(ctx, cmd::GET, &[keyspace::VERSION_KEY]))? {
        Step::Equal => Ok(ValkeyValue::Integer(0)),
        Step::Install => {
            let w = host::begin_writes(ctx)?;
            w.call(cmd::SET, &[keyspace::VERSION_KEY, keyspace::EPOCH])?;
            Ok(ValkeyValue::Integer(1))
        }
    }
}

No other code calls it. The TRA.* commands share nothing with the EMQ.* seam, so they run the same on either kind of server, and EMQ.VERSION refuses nothing: it reads the mode through serving() and reports.

The check sits at one choke point, the fence, plus the one write that installs what the fence reads.

the pattern (Redis Patterns Applied)

On a cluster, keys spread across slots by the hash of their tag; keys that must move together share one braced tag, or a multi-key step across them is refused. Hash-tag co-location teaches it.

the implementation (trafikey)

A queue's keys share its {q} tag, while every write verb also reads {emq}:version, a key under the {emq} tag. The module serves standalone servers, where every key is local, and refuses its writes on a cluster-enabled one.

EMQ.VERSION advertises nothing, so a client refuses

EMQ.VERSION reads no key and runs no fence, so it answers on any server. Its reply is the same six-field map everywhere, with one difference: the capability list is filled only when serving() is true.

// trafikey — EMQ.VERSION, the branch that empties the list
// a read-only command: it reads no key and runs no fence, so it answers on any server
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 command 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 version contract gives a client one rule before it trusts a node: the wire field must be equal, module_major must be equal, and every capability the client needs must be listed. On a cluster-enabled server the first two still hold and the third fails for every client that needs any capability, so the node is refused at the handshake, before the client sends a single write. One shipped client runs these checks: an EchoMQ.Connector opened with wire: 6, an option no production caller passes, calls EMQ.VERSION at connect and requires echomq:6.0.0, module_major 0 and the fourteen job-core tokens, so on a cluster-enabled server it stops with {:r14, failures} before it reads {emq}:version. Pick a server and a client below.

The client's three checks · pick a server and a client

wire module_major capabilities accepts
a worker (needs claim, extend, complete.flat, fail.flat) against a standalone server
EMQ.VERSION: wire echomq:6.0.0 · module trafikey · module_major 0 · capabilities “enqueue claim claim.pause extend extends complete.flat schedule promote reschedule delay fail.flat reprocess requeue stalled.check append failures.incr publish”
checks: wire equal — yes · module_major equal — yes · every needed capability listed — yes
verdict: the client accepts the node

An empty list is a refusal a client can read: the node answers EMQ.VERSION truthfully, and the truth is that it serves nothing.

Recap — standalone only

The module records the server mode once, in init. On a cluster-enabled server every command stays registered: every EMQ.* write verb refuses at the fence's first line with EMQWIRE expected a standalone server but cluster mode is enabled, having read nothing; EMQ.INSTALL refuses after its shape check; EMQ.VERSION answers with an empty capability list, so a client following the version contract refuses the node; the TRA.* commands carry no check. An unrecorded mode refuses as a host failure. The module's three dives end here; the part continues with the grammar every handler runs.

Next in the part: The command grammar — shape, layout, the fence, the domain checks, one write guard, then the writes.

References

Where the server mode and its refusals are grounded, and the cluster behaviour they relate to.

Sources

Related in this course