Immutability · dive 03 · how
The version fence
A connection is checked against the wire before it can touch the store.
The immutable line protects the data; the branded-id gate protects identity. One thing is left:
the connection. A connector that does not speak this wire must not run against this store. The
fence is one reserved key — {emq}:version — carrying the wire's identity, and a
claim-or-verify the connector runs before its first command: a match runs, an absent key is
claimed atomically with SET … NX and re-read, a mismatch is refused. Pick a store state
on the right and read the connector's verdict.
The fence at connect · pick the store state
The connection is the last thing left to guard
The keys and the Lua are fixed; identity is gated. But a connector that does not speak this wire could still
attach to the store and write rows its readers cannot understand. The fence closes that gap. It is one reserved
key, {emq}:version, carrying the wire's identity string, and a check the connector runs
before its first command.
The check is a claim-or-verify, and the simulator above walks its three cases. The connector
reads {emq}:version: if the value matches the wire it speaks, it proceeds. If the key
is absent, it claims it — SET … NX, then a confirming read — so two connectors racing to
claim an empty store cannot both win. If the value is present and disagrees, the connection is refused: a
version_fence error, and on a reconnect that error is fatal — the connection is not
retried against a store it does not match.
The fence, on real code
The reserved key is built by the keyspace. {emq}: is the core's own space — cross-queue
keys live there, disjoint from any queue's emq:{q}: keys — and the version key is one of
them.
# echo_mq — EchoMQ.Keyspace # {emq}: is the core's reserved space — cross-queue keys live here, disjoint # from any queue's emq:{q}: keys. The version key is one of them. @reserve "{emq}:" @version_key @reserve <> "version" def version_key, do: @version_key
The check is run by the connector. @wire_version is the identity string this connector
speaks — a code constant, not a label. The fence reads the version key once and branches: a match runs, an absent
key is claimed with SET … NX and confirmed, a present-but-different value refuses the
connection.
# echo_wire — EchoMQ.Connector # @wire_version is the identity string this connector speaks; it is a code # constant. The fence reads {emq}:version once: a match runs, an absent key # is claimed with SET NX then re-read to confirm, and a different value # refuses the connection. @wire_version "echomq:2.0.0" defp fence(sock, buf) do vkey = Keyspace.version_key() with {:ok, current, buf2} <- sync(sock, ["GET", vkey], buf) do case current do @wire_version -> :ok nil -> with {:ok, _, buf3} <- sync(sock, ["SET", vkey, @wire_version, "NX"], buf2), {:ok, @wire_version, _} <- sync(sock, ["GET", vkey], buf3) do :ok else {:ok, got, _} -> {:error, {:version_fence, got}} err -> err end got -> {:error, {:version_fence, got}} end end end
The SET … NX is the race guard: NX writes only if the key is
absent, so of two connectors claiming an empty store exactly one succeeds, and the confirming
GET makes the loser read the winner's value — which either matches (it runs) or does not
(it is refused). The fence runs inside the connect sequence, before the connection answers its first caller; on a
dropped-and-reconnected socket, a version_fence mismatch is treated as fatal rather than
retried.
One reserved key and a claim-or-verify before the first command: match runs, absent is claimed atomically, mismatch is refused.
The reserve map — where does this key live?
Why does the version key belong in {emq}: and not in a queue's space? Because it is not
a queue's data — it describes the whole wire. The reserve is first-byte-disjoint from any
queue's emq:{q}: keys, so the cross-queue version record is never confused with a
queue's pending set or job row. Pick a key and read which space it lands in and why.
Queue space or reserve · pick a key
A single reserved key, claimed atomically, can guard a whole connection — SET … NX is
the atomic claim, and an atomic check-then-act is what makes the guard safe under a race.
Coordination teaches that atomic claim.
EchoMQ.Connector.fence/2 reads Keyspace.version_key/0 →
{emq}:version at connect; absent → SET … NX + confirm, match →
run, mismatch → {:error, {:version_fence, got}} (fatal on reconnect).
The connection is checked against the wire before it can touch it, so a connector that does not match the wire never writes to it.
Recap — the third guard
The fence is one reserved key, {emq}:version
(EchoMQ.Keyspace.version_key/0), and a claim-or-verify
(EchoMQ.Connector.fence/2) the connector runs before its first command: a match runs, an
absent key is claimed atomically with SET … NX and confirmed, a mismatch is refused with
{:version_fence, got} and is fatal on reconnect. With the immutable line, the branded-id
gate, and the version fence in hand, the Protocol chapter is complete — the keyspace, the record, the Lua layer,
and the discipline that holds them together. Close it in the workshop.
Next: the Protocol workshop — decode a real key, then trace one EVALSHA to its Lua source.
References
Where the atomic claim and the read the fence is built from are documented.
Sources
- Valkey — SET — the
NXatomic claim the fence uses to win an empty-store race uncontested. - Valkey — GET — the read the fence opens and confirms with.
- Valkey — Documentation — the BSD-licensed store EchoMQ is backed by; the substrate of record.
Related in this course
- The owned keyspace — the keyspace and the
{emq}:reserve in depth. - The branded-id gate — the previous dive: identity gated in Lua.
- The Protocol — the chapter landing.
- redis-patterns R2 · Coordination — the atomic claim, the near side of the door.