jonnifyechomq

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

HASH key emq:{orders}:job:JOB0f3k… state "pending" — lifecycle position attempts "0" — fencing token payload "<caller bytes>" — opaque cargo added by the native module · absent at birth lease_epoch "<run_id>" — lease epoch failures "<count>" — retry budget
state — the lifecycle position. Born: "pending". Scripts: @enqueue writes "pending"; @claim2 "active"; @retry "scheduled" or "dead"; @promote "pending". Native module: the same values from EMQ.ENQUEUE, EMQ.CLAIM, EMQ.FAIL and EMQ.PROMOTE. Each transition that keeps the row rewrites it in the same atomic step that moves the job between sets.

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).

the pattern (Redis Patterns Applied)

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.

the implementation (echo_mq + trafikey)

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.

built page ready to read

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

Related in this course