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: 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.
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.
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
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
- Valkey — Cluster specification — hash slots, hash tags and the cluster mode the module refuses to serve.
- Valkey — Modules API reference — the context flags a module reads to learn the server mode.
- Valkey — CLUSTER KEYSLOT — the slot a key such as
{emq}:versionhashes to. - Valkey — Modules introduction — module commands, registered once and served by the loading server.
Related in this course
- Valkey and the module — the module this dive closes.
- The registration table — the capability list this page empties.
- The native fence — the fence the mode check opens, and its two texts.
- The owned keyspace — the
{q}tag and the{emq}:reserve. - redis-patterns · Hash-tag co-location — slots and tags, the near side of the door.