Every command mithril accepts is in a static table carrying its arity,
flags, key positions, ACL categories and routing kind. Arity, key
positions, flags and categories come from COMMAND INFO of Redis 8.10 and
Valkey 9.1 (tools/command-info.json, regenerated by
tools/gen-command-table.py); only the routing kind and the proxy’s own
flags are curated by hand. Unknown commands get ERR unknown command
without touching a backend.
Single-key commands (strings, hashes including field expiration, lists, sets, sorted sets, streams and consumer groups, geo, bitmaps, HyperLogLog, the EXPIRE family, SORT, DUMP/RESTORE, per-key scans, OBJECT/XINFO/XGROUP subcommands, the Redis 8.4-8.10 additions (DELEX, DIGEST, INCREX, the array type’s AR* commands, XNACK, XCFGSET, XIDMPRECORD, TS.READ) and the keyed commands of the Redis 8 modules — JSON, Bloom, Cuckoo, Count-Min, Top-K, T-Digest, TimeSeries) hash their key and go to the owning master — or to a replica when read splitting applies and the command is read-only. Multi-key commands with single-node semantics (RENAME, SMOVE, MSETNX, BITOP, the *STORE family, LMOVE, LCS, PFMERGE, …) route by their first key, and so do the numkeys forms (LMPOP, ZMPOP, ZUNION/ZINTER/ZDIFF and their STORE variants, ZINTERCARD, SINTERCARD, MSETEX); the owning node enforces same-slot, exactly as a direct cluster connection would. EVAL, EVALSHA, FCALL and their _RO forms route by their first key (or any master when they declare none).
MGET, MSET, DEL, UNLINK, EXISTS, TOUCH and PFCOUNT split per slot, execute
in parallel, and merge: MGET order-preserving, MSET all-OK, the rest summed.
A redirected part is retried once against its new owner before merging.
A part answered TRYAGAIN is first retried whole with short waits (2 ms,
doubling, up to six), which sees an atomic slot migration through with the
command intact; only when the refusal outlasts the waits (a legacy migration
with the keys split across source and target) is it re-issued key by key so
each request follows ASK, and a same-slot MSET or DEL then executes as
independent single-key commands rather than one atomic command. A redirect
that outlasts the waits ends the command with TRYAGAIN. A single-slot
multi-key command takes that path when nothing is queued behind it,
otherwise the TRYAGAIN reaches the client and the session routes that
slot’s multi-key commands through the ordered fan-out path until the
topology changes. PFCOUNT is
the exception: over several keys it counts one union, so it is not
re-issued key by key and the TRYAGAIN reaches the client. Cluster-wide
commands (DBSIZE, FLUSHALL, SCAN) wait for every pending fan-out first.
Keys that all hash to one slot (hash tags) skip the split: the command
routes as a single request with no merge step.
SCAN iterates the whole cluster with synthetic cursors (master index packed into the cursor’s high bits), DBSIZE sums the slot-owning masters, FLUSHALL broadcasts to them and requires every one to acknowledge.
SCRIPT LOAD reaches every node (scripts do not replicate since Redis 7) and
returns the sha once all agree; SCRIPT EXISTS answers true only for a sha
every master holds; SCRIPT FLUSH clears every node. The proxy remembers the
body of every script it loaded, so an EVALSHA that meets NOSCRIPT on a
node — after a restart or a flush behind the proxy’s back, redirected there
or not — reloads it there and reruns transparently when nothing later from
that session is in flight.
FUNCTION LOAD/DELETE/FLUSH/RESTORE broadcast to the masters (libraries
replicate); FUNCTION LIST/DUMP/STATS/HELP and SCRIPT HELP/SHOW answer from
one master. SCRIPT KILL, FUNCTION KILL and SCRIPT DEBUG are not proxied.
MULTI queues key-addressed commands locally, enforcing single-slot at queue
time (CROSSSLOT otherwise); EXEC ships the whole transaction as one native
MULTI/EXEC to the owning master. WATCH pins its keys on an exclusive
connection to that master (all watched keys, and the transaction that
follows, must share one slot — CROSSSLOT otherwise); every command whose
keys all live in that slot, and the EXEC, then run on that connection, so
a read after WATCH sees the master’s current value (never a replica’s or
the reply cache’s), the commands stay ordered with EXEC, and a key changed
by anyone else aborts EXEC with the nil reply, exactly as on a single node.
A multi-key command spanning the watched slot and others is refused with
CROSSSLOT while the watch is held or its connection is still draining
(the EXEC, UNWATCH, DISCARD or RESET reply arrives once the connection is
quiet unless more was queued behind it, so only a pipelining client meets
it, and a retry succeeds). A WATCH takes effect once every earlier request of the session has answered
(blocking commands, fan-outs and redirect retries included), while the
session keeps reading. UNWATCH, DISCARD and RESET release the connection
behind the requests it already accepted, and the slot keeps using it until
it is quiet; a WATCH inside MULTI is refused as Redis refuses it.
BLPOP, BRPOP, BRPOPLPUSH, BLMOVE, BLMOVEM, BLMPOP, BZPOPMAX, BZPOPMIN, BZMPOP
and blocking XREAD run on dedicated backend connections and always on the
slot’s master.
(P)SUBSCRIBE/(P)UNSUBSCRIBE run over a per-client pubsub connection with
fully ordered confirmations; a regular command, QUIT, RESET or a bare
unsubscribe waits for the confirmations and pushes in flight, so it sees the
settled subscriptions as it would on a direct connection. PUBLISH and PUBSUB forward to a master. Shard
channels: SPUBLISH routes by the channel’s slot; SSUBSCRIBE dials the
pubsub connection to that slot’s master (or checks the connection already
open reaches it — shard channels of one client live on one node, as on a
direct connection, and the channels of one SSUBSCRIBE share one slot);
SUNSUBSCRIBE and the server’s own sunsubscribe pushes after a slot moves
pass through, and the latter end the subscription on the proxy’s side as
well; a subscription the server refuses (a redirect from stale topology)
is not kept, and the redirect prompts a topology refresh. Under RESP3, subscribed clients may keep issuing regular
commands.
SELECT 0 is answered by the proxy. Any other index is checked against the
cluster’s cluster-databases (Valkey 9; a Redis cluster answers with its
cluster-mode error, an index beyond the setting with ERR DB index is out
of range) and then binds the session to that database: every backend
connection it uses from there on, shared or dedicated, selected that
database at handshake, so single-key and multi-key commands, scripts,
transactions and blocking commands all act on it; a database change ends an
open WATCH, whose backend lease is database-bound. Databases 0 through 255
are addressable (a cluster set beyond that is not a supported deployment).
The reply cache serves database 0 only; pubsub is not per database; RESET
returns to 0.
PING, ECHO, SELECT, TIME, AUTH, HELLO, RESET, QUIT, INFO,
CONFIG (GET over every key but user and the backend credentials, with
requirepass redacted; SET accepts loglevel, acl-pubsub-default,
acllog-max-len, slowlog-log-slower-than, slowlog-max-len),
SLOWLOG (GET/LEN/RESET/HELP over the proxy’s own slow log, see
operations.md), PSLOWLOG (the same subcommands forwarded to every node,
answered as [address, reply] pairs), CLIENT (ID/SETNAME/GETNAME/LIST),
COMMAND (COUNT, INFO with flags and ACL categories, GETKEYS covering
ranges, numkeys blocks, STREAMS lists and STORE targets, DOCS answered
empty), ACL (CAT,
SETUSER, GETUSER, DELUSER, USERS, LIST, WHOAMI, GENPASS, LOG),
MULTI/EXEC/DISCARD, and the
CLUSTER family. CLUSTER INFO/MYID/KEYSLOT/NODES/SLOTS/SHARDS describe a
single virtual node owning slots 0-16383 — the emulation that lets
cluster-aware clients treat the proxy as the whole cluster.
Requests for these return unknown-command; none of them silently misbehaves:
RANDOMKEY samples one random master’s keyspace, not the whole cluster.