The Lua layer · dive 02
Declared keys
The host builds every key; the script touches only KEYS[]. Values ride ARGV. That is the law.
Every key a script touches is declared in KEYS[]; the
script constructs none from data. EchoMQ.Script's own contract states it — every key a
script touches is declared in KEYS; ARGV carries values only.
It is the discipline that keeps atomicity sound on a cluster, keeps the keyspace the single owner of where data
lives, and keeps the wire portable to a thread-per-shard engine. Classify a key on the right, then read the
two beats below.
Declared or constructed? · classify a key
The host builds, the script declares
Two beats again: the handle that builds the keys, and the body that touches only what it was handed. The difference this dive draws is which things cross as keys and which cross as values.
Beat one — the host builds the keys. EchoMQ.Jobs.enqueue/4 builds both
keys through EchoMQ.Keyspace — the single owner of where data lives — then hands them to
the script as KEYS. The values go in ARGV. The script never
sees the queue name, the braces, or the slot; it sees two opaque keys and two opaque values.
# echo_mq — EchoMQ.Jobs # The host builds KEYS through the keyspace, the single owner of where data # lives. The script receives [row, pending] declared and [id, payload] as # values. The script does not know the queue name, the braces, or the slot — # only the two keys it was handed. def enqueue(conn, queue, job_id, payload) when is_binary(job_id) and is_binary(payload) do keys = [Keyspace.job_key(queue, job_id), Keyspace.queue_key(queue, "pending")] Connector.eval(conn, @enqueue, keys, [job_id, payload]) # ... verdict mapping omitted; see scripts-are-the-protocol end
Beat two — the script touches only declared keys. @enqueue references
KEYS[1] and KEYS[2] and nothing else. It never builds a key by
concatenating ARGV. The branded id rides ARGV[1] as a
value — the member written into the set, never spliced into a key name.
-- the @enqueue script — KEYS[1] = the job row, KEYS[2] = the pending set -- Both keys arrive declared; the script constructs neither. ARGV[1] (the id) -- is a VALUE — the member written into KEYS[2], never concatenated into a key. if string.sub(ARGV[1], 1, 3) ~= 'JOB' then return redis.error_reply('EMQKIND job id must be JOB-namespaced') end if redis.call('EXISTS', KEYS[1]) == 1 then return 0 end redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2]) redis.call('ZADD', KEYS[2], 0, ARGV[1]) return 1
The id appears inside the script only as the third argument to ZADD — a member, a value.
It is never used to build the row key; that key was built host-side and arrived as KEYS[1].
The rule for where the job lives stays in EchoMQ.Keyspace, where it belongs.
Keys are handed in; values ride ARGV. A script that builds a key from data has moved the address rule out of the keyspace and broken the law.
Why declared keys is the law — three reasons
The discipline is not a style choice. Three things depend on it.
One — atomicity needs co-location. A Valkey cluster runs one script's keys on one node only if
they hash to one slot. The per-queue hashtag {q} puts every key of a queue on one slot;
declaring those keys is how the engine verifies the script stays on its slot. A key constructed in-script is not
on the declared set, so the engine cannot guarantee it is co-resident.
Two — the keyspace stays the single owner of address. If a script built a key from data, the
rule for where a job lives would be split between EchoMQ.Keyspace and the script body.
Declaring keys keeps that rule in one place. There is exactly one answer to "where does this job's row live",
and it is the keyspace's.
Three — thread-per-shard placement. A multithreaded engine like Dragonfly places a script on a
thread by its declared key set (--lock_on_hashtags). A key constructed inside the script
is invisible to that placement — so an undeclared key is not portable to a thread-per-shard engine. Conformance
is phrased against Valkey; the declared-key discipline is what makes the multithreaded placement reachable.
Co-location, on a slot
One enqueue touches the row and the pending set. Both are declared, both carry the {q}
hashtag, so both hash to one slot and the script runs on one node — atomic. Add an undeclared key (built in the
script) and the guarantee is gone: the key is not in the declared KEYS set, so the
engine cannot place the script where that key lives. Compare the two below.
Declared keys vs an undeclared one · pick a script
Hash-tag co-location and one-slot atomic Lua: keep the keys a script touches on one slot so one script can touch them atomically. Hash-tag co-location and Coordination teach it.
@enqueue declares KEYS[1] and KEYS[2],
both built by EchoMQ.Keyspace on the {q} slot; the script
builds no key from data, so the engine can place it.
Declared keys is the law because it is what atomicity, single ownership of address, and thread-per-shard placement all stand on.
Recap — declared keys, three reasons
The host builds every key; the script touches only KEYS[]; values ride
ARGV. It keeps atomicity sound on a cluster, keeps the keyspace the single owner of
address, and keeps the wire portable to a thread-per-shard engine. The next dive reads how the declared-keys
script is dispatched — loaded once and run by its SHA.
Next in the Lua layer: EVALSHA dispatch — the SHA precomputed at definition, the script run by digest, the source loaded once on a NOSCRIPT miss.
References
Where the declared-keys discipline is grounded, and the engine behaviour it serves.
Sources
- Redis — EVAL — the
KEYS/ARGVcontract: keys declared, values separate. - Redis — Keyspace & hash tags — cluster routing by the hashtag inside
{...}, which the declared keys must share to stay atomic. - Valkey — Documentation — the substrate of record; Redis-semantics Lua and slot routing.
- DragonflyDB — Server flags —
--lock_on_hashtags, the thread-per-shard placement the declared keys enable.
Related in this course
- The Lua layer — the module this dive belongs to.
- Scripts are the protocol — the transition this dive reads the keys of.
- The owned keyspace — the keyspace that builds the declared keys.
- redis-patterns · Hash-tag co-location — keys on one slot, the near side of the door.
- redis-patterns · Coordination — atomic updates and one-slot Lua.