The Lua layer · dive 03
EVALSHA dispatch
The source crosses the wire once. After that, every call is a digest.
A script's source crosses the wire at most once per connection.
EchoMQ.Script.new/2 precomputes each script's SHA1 when the module loads;
EchoMQ.Connector.eval/5 runs the script with EVALSHA — sending only the
digest, the key count, the keys, and the values. On a cold cache (NOSCRIPT) the
connector loads the source once, confirms the returned SHA matches, and re-runs by SHA. Run the dispatch on the
right, then read the two code blocks below.
EVALSHA dispatch · warm cache or cold
Precompute the SHA, at definition
EchoMQ.Script.new/2 is called once per script when the module loads. It hashes the
source to a lowercase SHA1 and keeps the name, the source, and the digest together. The source is retained only
for the cold-cache load; the digest is what the hot path sends.
# echo_wire — EchoMQ.Script # A server-side script with its SHA1 precomputed, so the connector can run # EVALSHA-first with a load-on-NOSCRIPT fallback. Every key a script touches # is declared in KEYS — the law; ARGV carries values only. def new(name, source) when is_atom(name) and is_binary(source) do # the SHA1 of the source is what EVALSHA addresses; lowercase hex matches # the digest SCRIPT LOAD returns, so the two can be compared byte for byte. sha = :crypto.hash(:sha, source) |> Base.encode16(case: :lower) %__MODULE__{name: name, source: source, sha: sha} end
So @enqueue = Script.new(:enqueue, "...") is computed once. Every later
enqueue/4 reuses the same struct — the SHA is never recomputed on the hot path. The
struct carries three fields: the name, the source (for the cold
load), and the sha (for the dispatch).
Run by SHA, load on NOSCRIPT
EchoMQ.Connector.eval/5 builds the EVALSHA command from the
precomputed SHA, the key count, the keys, and the values. On a NOSCRIPT reply it loads
the source once, verifies the SHA, and re-runs; any other server error is mapped to a typed failure.
# echo_wire — EchoMQ.Connector # EVALSHA-first execution of a declared-keys script. The wire carries the # 40-char digest, the key count, then KEYS then ARGV — never the source on the # hot path. def eval(conn, %Script{} = s, keys, argv, timeout \\ 5_000) do parts = ["EVALSHA", s.sha, Integer.to_string(length(keys))] ++ keys ++ argv case command(conn, parts, timeout) do # cold cache: the server has never seen this script. Load it ONCE, confirm # the returned digest equals the precomputed one, then re-run by SHA. {:ok, {:error_reply, "NOSCRIPT" <> _}} -> GenServer.call(conn, {:bump, 5}) # count the script_load (slot 5) case command(conn, ["SCRIPT", "LOAD", s.source], timeout) do {:ok, sha} when sha == s.sha -> command(conn, parts, timeout) |> map_script_reply() {:ok, other} -> {:error, {:sha_mismatch, other}} {:error, _} = err -> err end # any other server error is a verdict — mapped to a typed {:server, msg}. {:ok, {:error_reply, msg}} -> {:error, {:server, msg}} other -> other end end
The hot path is the first command(conn, parts, …): EVALSHA, the
digest, the key count, then the keys, then the values. The source appears only in the
NOSCRIPT branch, run at most once per script per connection. The connector keeps
counters — slot 5 script_loads, slot 6 evalsha_calls — so a
healthy connection shows one load per script and many EVALSHA calls after it.
EVALSHA sends a digest, not source. The source crosses once, on a cold cache; every call after that is forty characters and the arguments.
Verify, never trust — two details
Two details carry the dispatch's correctness, and both are in the branch above.
The SHA is verified, not trusted. SCRIPT LOAD returns the server's own
digest of the source; the connector accepts the re-run only when that digest equals the precomputed
s.sha. A mismatch ({:sha_mismatch, other}) means the source the
server hashed is not the source the struct holds — a hard fault, not a silent retry.
A load-and-retry error maps exactly as the first attempt. A script can fail on either attempt;
map_script_reply turns the re-run's error reply into the same
{:server, msg} shape a first-attempt error gets, so a caller cannot tell a cold cache
from a warm one. The connector found this on a cold script cache during conformance.
After SCRIPT LOAD · what the connector does with the returned digest
Load a script once and run it by SHA with EVALSHA, reloading on a NOSCRIPT miss — scripting is how a pattern becomes a protocol. Patterns become protocol and Coordination teach it.
EchoMQ.Script.new/2 precomputes the SHA1; EchoMQ.Connector.eval/5
runs it EVALSHA-first, loads the source once on a NOSCRIPT miss, verifies the returned digest, and maps a
re-run error exactly as a first-attempt one.
The dispatch trusts nothing it can verify: it confirms the loaded digest, and it makes a cold cache indistinguishable from a warm one.
Recap — the digest, not the source
EVALSHA dispatch sends a digest, not source. EchoMQ.Script.new/2 precomputes the SHA1;
EchoMQ.Connector.eval/5 runs the script by SHA, loads the source once on a NOSCRIPT
miss, verifies the returned digest, and maps a re-run error exactly as a first-attempt error. The scripts are
the protocol; this is how they run. That closes the Lua layer — the next module reads what holds the wire
together.
Next module: Immutability & branded ids — why the keys and the Lua are held fixed, the 14-byte branded-id gate, and the version fence.
References
Where the EVALSHA dispatch is grounded, and the load path it falls back to.
Sources
- Redis — EVALSHA — run a cached script by its SHA1 digest.
- Valkey — EVALSHA — the substrate-of-record form of the same dispatch.
- Valkey — SCRIPT LOAD — cache the source and return its digest, the cold-cache path.
- Valkey — Documentation — the store the connector speaks to over RESP.
Related in this course
- The Lua layer — the module this dive belongs to.
- Scripts are the protocol — the script this dispatch runs.
- Declared keys — the keys the dispatch passes after the SHA.
- redis-patterns · Patterns become protocol — scripting as protocol, the near side of the door.
- redis-patterns · Coordination — atomic updates and one-slot Lua.