Immutability · dive 01 · why
The immutable line
An agreement only holds when both sides hold it byte-for-byte. Below the line, both sides do, for as long as the wire lasts.
Below the language line are two layers — L1, the keys and field names, and
L2, the transitions, each one atomic server-side step with a fixed contract. They are fixed
for a wire, and not by policy: every speaker reads the same key at the same address and the
same named fields out of the same row. Rename one of them in one speaker and that speaker writes a row no other
speaker can find, while the fence passes it. Make the change as a new wire, with a new identity in
{emq}:version, and the fence refuses the mismatch instead — which is what the native
module did. This dive deepens the line on the real record — the key
EchoMQ.Keyspace.queue_key/2 builds, and the three fields both executors write — and
shows exactly who breaks for each thing you change.
Change one thing · who can still read the job?
An agreement is only as fixed as both sides keep it
The three pillars interoperate and any runtime can be a peer for one reason: the agreement is the keys and the
transitions. But an agreement only holds if both sides hold it byte-for-byte. That is what "the line is fixed
for a wire" means — not that the code cannot be changed, but that changing the keys, the field names or a
transition's contract in one speaker desyncs that speaker from every other. The data is not destroyed;
it becomes unfindable (a moved key) or unreadable (a renamed field) across the
wire. And the fence does not catch it: the renamer still presents the same identity in
{emq}:version, and the fence compares nothing else.
So a change below the line is made one way only: as a new wire, with a new identity. Then
nothing desyncs silently — a speaker of the old wire is refused by the fence before its first write. The
demonstrator above makes each case exact. Three speakers — the canonical Elixir and two peer runtimes — all read
one row at emq:{q}:job:<id> with the fields state,
attempts, payload, through a transition whose
KEYS are the row and the pending set. Make one change and read how many of the three
can still find and read the job. A change below the line made in one speaker breaks the changer; the same kind
of change made as a new wire is fenced; a change above the line — the L4 verb — touches nothing on the
wire.
The line, on the real record
The address is L1, and it is built by one function. EchoMQ.Keyspace.queue_key/2
composes the exact bytes every speaker must agree on: the literal emq:, the queue name
in braces, the type suffix.
# echo_mq — EchoMQ.Keyspace # The literal "emq:", the queue name wrapped in braces, then the type # suffix. Every speaker composes the exact same bytes for the same # {queue, type} — that byte-for-byte agreement is the line. def queue_key(queue, type) when is_binary(queue) and is_binary(type), do: IO.iodata_to_binary(["emq:{", queue, "}:", type])
The field names are L1 too, and they are written by the @enqueue script (L2) in one
atomic HSET. Three names, fixed: state is the lifecycle
position, attempts the fencing token (the claim increments it, and a completion that
presents another value is refused with EMQSTALE), payload the
opaque caller bytes. A reader anywhere on the wire reads exactly these three.
-- the @enqueue script (L2, scripted) — the three-field row, written atomically -- These three names ARE the contract. Every speaker reads exactly -- state, attempts, payload — no more, no fewer, by these names. redis.call('HSET', KEYS[1], 'state', 'pending', 'attempts', '0', 'payload', ARGV[2])
The native module writes the same row. EMQ.ENQUEUE, once every check has passed, issues
the same HSET with the same three names and the same initial values, then the same
score-0 pending entry. A row the scripts wrote and a row the native command wrote are the same row.
// trafikey — EMQ.ENQUEUE (L2, native), the writes after every check has passed // The same three names and the same initial values, byte for byte, then // the same score-0 pending entry. w.call( cmd::HSET, &[ row, b"state", b"pending", b"attempts", b"0", b"payload", payload, ], )?; w.call(cmd::ZADD, &[pending, b"0", id])?;
And it adds two fields. The native claim stamps lease_epoch with the server's
run_id beside the scripts' state=active (so a lease that
expired across a server restart is recovered without counting a stall), and EMQ.FAIL
caps retries on a failures field, so attempts stays the fencing
token. That is a change to field and transition contracts — so the native module is a new wire, with its own
identity in {emq}:version: echomq:6.0.0, where the scripts'
is echomq:4.0.0, the brand a default wire 4 connector holds. Each executor's fence
refuses the other's value.
// trafikey — EMQ.CLAIM, the lease write // The scripts' claim writes state=active; the native claim also stamps // lease_epoch with the server's run_id. A new field is a new contract: // this is why the native module carries its own identity. w.call( cmd::HSET, &[&row, b"state", b"active", b"lease_epoch", run_id], )?;
That is the evidence the line held: a second executor was built against the same keys and the same field
names, with the same replies on the ordinary path (where a reply differs, the native verb refuses an edge the
script let through), without renaming one of them, and what it added arrived under a new identity. Now the
consequence of the other path is concrete. Change the literal emq: in one runtime and
its rows land at an address no other speaker scans — the jobs are not lost, they are unfindable. Rename
payload to data in that runtime's
@enqueue and it writes a field nobody else reads — a claim returns the row, but every
other speaker reads payload and finds nothing. The same logic applies to the
KEYS contract: drop the pending set from the declared keys and the row is written but
never enters the queue, so nothing ever claims it. In every case the renamer still presents the old identity,
so every fence passes it.
The L4 verb is the mirror image. EchoMQ.Jobs.enqueue/4 is a name above the
line; rename it to push and nothing on the wire moves, because the keys it builds and
the transition it runs are unchanged. The L3 executor is above the line too: the scripted connector sends
EVALSHA by SHA, a native command arrives by name, and the keys and fields are the
same. That asymmetry — fixed below, free above — is the whole reason one wire serves three pillars in any
language.
Below the line is the contract; above it is the code. A field rename made in one speaker makes the row invisible to every other speaker; the same change made as a new wire is refused at the fence; an L4 rename changes nothing on the wire.
The layer ledger — below or above, fixed or free
Read EchoMQ as five layers and mark each one. The two below the line are fixed for a wire, on both executors; the two above it are each runtime's own; the engine underneath is external. Pick a layer to read where it sits and what "fixed for a wire" or "free" means for it in practice.
The layers · pick one to read its standing
The full layer model — and the stack drawn end to end — is the orientation dive The protocol below the line. This dive reads the two fixed layers on the real record.
Pattern & implementation
A Redis convention becomes a protocol when the agreement — the keys plus the atomic transitions — is pushed below the language line and held fixed. Coordination teaches the atomic, one-slot move that this fixity rests on.
EchoMQ.Keyspace.queue_key/2 fixes the address
emq:{q}:<type>; the @enqueue script and
EMQ.ENQUEUE write the same three named fields; the native module's two added fields
come with its own identity in {emq}:version. L1 and L2, fixed for a wire.
Push the agreement below the language line and hold it byte-for-byte, and a Redis convention becomes a protocol any runtime can be a peer of — and a second executor can join it without moving a key.
Recap — the line, and who keeps it
The line is the keys and the transitions, and it is fixed for a wire because an agreement only holds when both
sides hold it byte-for-byte. EchoMQ.Keyspace.queue_key/2 builds the address
emq:{q}:<type>; the @enqueue script and
EMQ.ENQUEUE write the three named fields state,
attempts, payload. A change below the line made in one speaker
makes the row unfindable or unreadable to every other speaker, and the fence passes it; the same change made as
a new wire is refused at the fence. The native module is that case made real: the same keys and fields, the
same replies on the ordinary path, two fields more, its own identity. The L4 verb above the line is free to change. The next dive reads
the second guard — how identity is gated before a row is even written.
Next in this module: The branded-id gate — the 14-byte branded id the host
checks, and the 3-byte EMQKIND gate both executors run.
References
Where the line is grounded, and where the keyspace and record layers are documented.
Sources
- Redis — Keyspace & hash tags — the L1 key routing the address relies on, by the hashtag inside
{...}. - Valkey — HSET — the hash write that lays down the three fixed fields.
- Valkey — Modules Introduction — how a module registers commands; the native module re-implements the core job transitions this way.
- Valkey — Documentation — the BSD-licensed store EchoMQ is backed by; the substrate of record.
Related in this course
- The protocol below the line — the line drawn on the full layer stack.
- The record hash — the record and its fields in depth.
- The branded-id gate — the next dive: identity gated in the transition.
- The version fence — the identity that marks a new wire.
- redis-patterns R2 · Coordination — atomicity and co-location, the near side of the door.