The owned keyspace · dive 01 · the grammar
The braced grammar
One shape for every key: emq, the queue in braces, the type. Two productions, one builder.
Every key EchoMQ writes has the same shape — emq:{q}:<type>. This is the
L1 grammar, and it is total: no key is written that
EchoMQ.Keyspace did not compose. This dive reads its two productions on the real code —
queue_key/2 for the per-queue key and job_key/2 for the job row —
and shows why one queue's keys all carry the same {q} hashtag.
A per-queue key · pick a segment
The shape, and why it is total
A protocol's data layer is its agreement about where things live. EchoMQ's agreement is one
string shape: emq:{q}:<type> — the literal emq:, the queue
name wrapped in braces, a colon, then the type. The type is the role of the key within the queue:
pending (the ready set), active (the claimed set),
schedule (the delayed set), job:<id> (a record row).
What makes it a protocol and not a habit is that the grammar is total: there is no path by which
the system writes a key that EchoMQ.Keyspace did not build. The module has exactly two
productions for queue state — queue_key/2 and job_key/2 — and the
job row is itself a per-queue key with a validated id appended. So one queue named orders
owns a whole family of keys that all share the {orders} hashtag.
- emq:{orders}:pending
- the ready set — ids waiting to be claimed.
- emq:{orders}:active
- the claimed set — ids a worker holds.
- emq:{orders}:schedule
- the delayed set — ids parked until a due time.
- emq:{orders}:job:<id>
- the record row — one job's hash.
Every key in that family carries {orders}, so every key routes to one slot —
the precondition for the atomic multi-key Lua of The Lua layer.
queue_key/2 — the per-queue production
queue_key/2 composes the per-queue key as iodata, then flattens it to a binary in one
pass. The guard fixes both arguments as binaries, so a non-string queue or type fails at the boundary rather than
producing a malformed key.
# echo_mq — EchoMQ.Keyspace # Per-queue keys are emq:{q}:<type>. The braces are not decoration — # they are the cluster hashtag, and they pin every key of one queue to # one slot. Built as iodata, then flattened once. def queue_key(queue, type) when is_binary(queue) and is_binary(type), do: IO.iodata_to_binary(["emq:{", queue, "}:", type])
The iodata list ["emq:{", queue, "}:", type] is the grammar made literal: the brace
opens immediately after emq: and closes immediately before the type colon, so the queue
name is always the hashtag and nothing else is. There is no string interpolation that a caller could subvert —
the structure is in the list, not in a format string.
job_key/2 — the job row, gated
The job row is a per-queue key whose type is job: with a validated
branded id appended. It is the one production that refuses its input: an id that is not a valid branded id is not
turned into a key — the function raises.
# echo_mq — EchoMQ.Keyspace # The job row is a per-queue key whose type is "job:" plus the branded id. # The id is GATED first: the keyspace refuses to address an unbranded row, # so a malformed id never becomes a key. def job_key(queue, branded) when is_binary(branded) do if EchoData.BrandedId.valid?(branded) do queue_key(queue, "job:") <> branded else raise ArgumentError, "job_key requires a valid branded id" end end
So job_key("orders", "JOB0Nb1VTbfnu4") yields
emq:{orders}:job:JOB0Nb1VTbfnu4. The branded id is the long part of the key by design — 14
bytes, a 3-byte namespace plus an 11-byte Base62 payload — and valid?/1 is the first
place the wire enforces it. The id is gated a second time inside the Lua, on its kind prefix; that is
Immutability & branded ids. Here the point is the
grammar: the row's address is always a per-queue key, so it co-locates with the queue's sets.
Build a key — read off the grammar
The grammar is mechanical: give a queue name and a type and the key is determined. Type a queue and pick a type
below. The page composes the key exactly as queue_key/2 does — and, for the
job: type, appends a sample branded id the way job_key/2 does —
then reads back the hashtag (always the queue name) so you can see the brace is the routing key.
queue_key / job_key · compose a key
A queue name and a type determine the key — and the key always carries the queue as its hashtag. That is the whole data-layer agreement: one shape, one builder, one routing key per queue.
Pattern → implementation
A key naming convention becomes a contract when every writer agrees on the exact string shape — the prefix, the separators, the field positions — and on the hashtag that co-locates a group. Redis Patterns Applied R2.05 teaches it.
EchoMQ.Keyspace.queue_key/2 and job_key/2 make
emq:{q}:<type> the only way a key is built, so the convention is total — the
grammar is the protocol, not a style guide, and the brace makes every queue single-slot.
The grammar is the data-layer half of the protocol. Build every key through one module to one braced shape, and "where things live" stops being a convention and becomes the wire.
Recap — one shape, two productions
The braced grammar is emq:{q}:<type>. queue_key/2 builds
the per-queue key as iodata; job_key/2 builds the job row from a per-queue key plus a
validated branded id, raising on an invalid one. The grammar is total — no key escapes the builder — and every
key of one queue shares the {q} hashtag, so they co-locate on one slot. The next dive
reads what that hashtag computes: the cluster slot.
Next in this module: The hashtag & the slot — the brace as the cluster hashtag,
and slot/1 = CRC16-XMODEM % 16384.
References
Where the grammar and the hashtag convention are documented, and where the record it addresses is read.
Sources
- Redis — Keyspace & cluster hash tags — the
{...}hashtag convention the braces use. - Valkey — Documentation — the store the grammar addresses; the substrate of record.
- Valkey — HSET — the hash write that fills a job row.
Related in this course
- The owned keyspace — the module hub.
- The hashtag & the slot — the slot the brace computes.
- The reserve — the cross-queue reserve.
- The record hash — what the job row holds.
- redis-patterns R2.05 · Hash-tag co-location — the near side of the door.