The Protocol · module 02
The record hash
A job is one Valkey hash, and its field names are part of the wire.
A job is an entity. Its state lives in one Valkey HASH at the key
the keyspace builds — EchoMQ.Keyspace.job_key/2 →
emq:{q}:job:<id> — and admission writes it in one atomic step. Three birth
fields: state, attempts, payload. They
are not implementation detail; they are the protocol, read and written by name on either
executor — the scripted executor (EchoMQ.Connector running the
inline Lua) and the native module (trafikey, whose
EMQ.* commands run the core job transitions inside the server). The native module keeps
every field and adds two: lease_epoch and failures. This module
reads the record — the field set and its writers, the lifecycle field and the fencing token, and the opaque
cargo.
One hash, named fields
The record is the smallest row that carries a job: a hash born with three fields, addressed by the job key. The
names are fixed — state for where the job is in its lifecycle,
attempts for the fencing token, payload for the caller's data.
There is no id field (the id is the key) and no timestamp field; the id itself is
time-ordered.
The names are the same on both executors. The native module keeps every field, writes the same values, and
adds two of its own as its commands run: lease_epoch when it leases a job, and
failures when it records a failed try. Pick a field to read what it holds and which
transitions write it on each executor.
The record at emq:{q}:job:<id> · pick a field
Three birth fields, one hash: where the job is, the token that fences its holder, and what it carries. The native module adds two more. The names are the contract.
The write — one atomic HSET
The record is created by exactly one write, inside the enqueue transition. Beat one is the
named handle: EchoMQ.Jobs.enqueue/4 checks the queue name, builds the two keys —
the job row (job_key/2) and the pending set (queue_key/2)
— and runs the @enqueue script through its dispatch:
EchoMQ.Connector, unless the optional fifth argument, opts,
names another.
# echo_mq — EchoMQ.Jobs: the named handle for admission. # A job is an entity: its row is a hash at the job key; the pending set is a # same-score sorted set keyed by the id itself. The host builds both keys and # hands them in declared; the values (id, payload) ride ARGV. def enqueue(conn, queue, job_id, payload, opts \\ []) when is_binary(job_id) and is_binary(payload) do # A queue name may not carry a brace or a NUL: one queue, one {q} hashtag. Keyspace.valid_queue!(queue) # The dispatch is carried, never inspected: EchoMQ.Connector by default. via = Keyword.get(opts, :via, Connector) keys = [Keyspace.job_key(queue, job_id), Keyspace.queue_key(queue, "pending")] case via.eval(conn, @enqueue, keys, [job_id, payload]) do {:ok, 1} -> {:ok, :enqueued} {:ok, 0} -> {:ok, :duplicate} {:error, {:server, "EMQKIND" <> _}} -> {:error, :kind} other -> other end end
Beat two is the script body. After the kind gate and the idempotency check, the record is one
HSET: the three fields, with their birth values. state is born
"pending", attempts is born "0", and
payload is the caller's ARGV[2], stored verbatim.
-- the @enqueue script: the record write, every key declared in KEYS[] -- The kind gate: the first three bytes of the id must be JOB (EMQKIND). -- The full branded-id check already ran host-side, in job_key/2. if string.sub(ARGV[1], 1, 3) ~= 'JOB' then return redis.error_reply('EMQKIND job id must be JOB-namespaced') end -- Idempotent: a row that already exists is a no-op (a duplicate enqueue). if redis.call('EXISTS', KEYS[1]) == 1 then return 0 end -- THE RECORD: one HSET writes the three named fields with their birth values. -- state 'pending' the lifecycle position -- attempts '0' the fencing token, minted by the first claim -- payload ARGV[2] the caller's opaque cargo, stored verbatim -- The native command EMQ.ENQUEUE writes this same HSET. redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2]) -- The pending set: the id is the member, so byte order is mint order. redis.call('ZADD', KEYS[2], 0, ARGV[1]) return 1
That HSET is the record, literally. The native command
EMQ.ENQUEUE takes the same keys and arguments and writes the same
HSET — the same three names, the same birth values — after it has checked
the key layout, the wire identity and the kind.
Later transitions add a small, fixed set of fields on both executors — last_error
on each failure, stalled from the stall sweep — and on the scripted side the
progress verb writes progress, lanes write group, and flows
write the parent links. The lease is not a field: it is the job's score in
emq:{q}:active. The row a job is born with is these three, and that is what
makes the record portable: a fixed, named shape any speaker reads, small enough that the queue carries no second
index (the id is the pending-set member, so byte order is mint order).
Model an entity as one Valkey hash with a small, fixed field set: the key is the address, the fields are the contract. Data Modeling & Memory (R7) teaches the move in Redis Patterns Applied; Patterns become protocol is the near side of the door.
EchoMQ.Keyspace.job_key/2 addresses the row at
emq:{q}:job:<id>; @enqueue and
EMQ.ENQUEUE write the same field set — state,
attempts, payload — with one
HSET; the native module adds lease_epoch and
failures.
The record is one HSET: three named fields, born with their values, on either executor. The names are protocol; the shape is the contract every speaker reads.
The dives
Three readings of the record, each grounded in the real echo/apps/echo_mq scripts and
the trafikey commands.
The hash and its fields
The birth HSET field by field, then every later field and its writers on each executor, including the two the native module adds.
The state and attempts
state, the lifecycle position every transition rewrites; attempts, the fencing token; the retry budget and the restart exemption.
The payload
The opaque cargo: stored verbatim at admission, returned verbatim by the claim on either executor, never parsed.
Next in The Protocol: the chapter map —
the native module that runs the core job transitions as
EMQ.* commands, and the immutability discipline that holds the field set fixed.
References
Where the record's data type and the writes that build and read it are documented.
Sources
- Valkey — Documentation — the store the record hash lives in; the substrate of record.
- Valkey — HSET — the single write that builds the three-field row.
- Valkey — HGET — how the claim reads the payload back to the worker.
- Valkey — INFO — the
run_idfield the native module stamps aslease_epoch. - Redis — Hashes — the hash data type the record is modeled on.
Related in this course
- The Protocol — the chapter this module belongs to.
- The hash and its fields — dive 1: the field set and its writers.
- The state and attempts — dive 2: the lifecycle field, the token, the budget.
- The payload — dive 3: the opaque cargo.
- The native module — the core job transitions, as
EMQ.*commands inside the server. - The protocol below the line — where the three-field row first appears.
- Data Modeling & Memory (R7) — the data-modeling chapter on the near side.