jonnifyechomq

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?

below the line
as a new wire
above the line
three speakers, one row Elixircanonical a Go peera runtime a Node peera runtime emq:{q}:job:<id> state · attempts · payload
Change (above the line): rename the L4 verb enqueue → push. Wire HOLDS. Readers: 3 of 3 — the wire is untouched; every speaker reads the same row written the same way. Why: the verb is above the line; the keys and the transition it runs are unchanged.

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

L4 API · EchoMQ.Jobs.enqueue L3 executor · EVALSHA, or EMQ.* by name — fixed for a wire below this line — L2 transitions · atomic, Lua or EMQ.* L1 keys & fields · emq:{q}:<type>, state/attempts/payload L0 Valkey · the engine (external)
L2 transitions — scope: one atomic server-side step each: an inline Lua script, or, for a core job transition, one EMQ.* command. FIXED FOR A WIRE — below the line, the same for every speaker of that wire, on either executor. the contract (declared keys, arguments, field writes, reply) is fixed for a wire; the native module keeps the scripts’ keys and arguments, and their replies on the ordinary path, adds two field writes, and so carries its own identity in {emq}:version

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

the pattern (Redis Patterns Applied)

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.

the implementation (echo_mq + trafikey)

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

Related in this course