jonnifyechomq

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

connect boot fence GET {emq}:version run value == @wire_version → :ok → first command the version key lives in the reserve {emq}: — never a queue's key

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

per-queue spaceemq:{q}:*pending · active · job:<id> core reserve{emq}:*version (cross-queue) first-byte-disjoint: a queue key starts "emq:", a reserve key starts "{emq}"
the pattern (Redis Patterns Applied)

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.

the implementation (echo_wire)

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

Related in this course