# EVAL + Lua `EVAL script numkeys key1 ... keyN arg1 ... argM` runs `script` server-side as one atomic operation. the script sees: - `KEYS[1..N]` — the named keys (1-indexed) - `ARGV[1..M]` — the named args - `redis.call(cmd, ...)` — invokes another Redis command from inside the script; raises on error - `redis.pcall(cmd, ...)` — same but returns `{err = "..."}` instead of raising a script either runs to completion or errors; either way, no other client sees any intermediate state. ## why it's lua 5.1 forever redis embeds [Lua 5.1](https://www.lua.org/manual/5.1/) and uses the included sandbox restrictions. 5.1 has been frozen for ~17 years. that's a feature, not a bug — every redis 5.1 script you've ever written keeps working unchanged forever. it also means: - `unpack(table)` is global (in 5.2+ it moved to `table.unpack`) - `#tbl` returns array length but has nil-hole edge cases - no `goto`, no integer type (everything's a double until 5.3) - `pcall`/`xpcall` exist; `error()` works the obvious way if you're writing a redis-compatible engine, embed Lua 5.1 specifically. `mlua` (rust) and `ziglua` (zig) both let you pick the version explicitly — `lang = .lua51` in build.zig.zon. ## script return values redis maps Lua return values back to RESP this way: | lua type | RESP reply | |--------------------|--------------------------| | `nil` | nil | | `true` | integer `1` | | `false` | nil (!) | | number | integer (truncated) | | string | bulk string | | `{ok = "OK"}` | simple string `"+OK"` | | `{err = "msg"}` | error `"-msg"` | | table (sequential) | array (recursive) | `{ok = ...}` and `{err = ...}` are the *only* way for a script to return a status or error reply. otherwise you can only return strings, integers, arrays, or nil. ## composability via redis.call `redis.call` lets the script call any other command. typical pattern: a one-shot "do these N things atomically": ```lua -- schedule.lua if is_immediate == '1' then redis.call('XADD', stream_key, '*', unpack(fields)) else redis.call('HSET', parked_key, unpack(fields)) redis.call('ZADD', queue_key, when, task_key) end redis.call('HSET', runs_key, 'state', 'parked', 'task_key', task_key) return 'SCHEDULED' ``` inside `EVAL`, redis is *still single-threaded*. no other client can observe state between any two of those `redis.call`s. that's the entire reason EVAL exists. ## error semantics - `redis.call('NOPE')` — raises a Lua error → script terminates → redis returns error reply. - `redis.pcall('NOPE')` — returns `{err = "..."}` → script continues, can decide what to do. real redis does *not* roll back side effects on error. if your script did `XADD` and then errored, the XADD stays. matters because anyone parked on that stream (XREADGROUP BLOCK) needs to be woken up regardless of whether the script succeeded — pydocket/burner-redis track this as the `had_xadd` / `had_list_mutation` flags on `EvalOutcome`. ## SCRIPT LOAD + EVALSHA `SCRIPT LOAD ` returns a SHA1; `EVALSHA ...` re-runs it. the redis instance caches scripts in memory and returns `NOSCRIPT` if asked for one it doesn't know — clients are expected to handle `NOSCRIPT` by re-LOADing. EVALSHA saves only bandwidth: for a handful of small scripts, embedding them at build time and sending the body with `EVAL` every call is fine, and there is no `NOSCRIPT` path to handle. ## embedded engines [burner-redis](https://github.com/PrefectHQ/burner-redis) (rust, the original) and [burner-redis-zig](https://tangled.org/zzstoatzz.io/burner-redis) (the port) both embed a real Lua 5.1 interpreter and expose `redis.call` as a C function backed by the in-process store. EVAL scripts run unchanged against either backend — that's what makes "swap redis for an in-process engine in tests" work cleanly. caveats: - the script runs single-threaded against the in-process store, so atomicity is free (mutex around the whole store + a fresh Lua VM per EVAL call). - script-level blocking (XREADGROUP BLOCK inside a script) isn't a thing in real redis either — BLOCK arguments are silently ignored when inside an EVAL. ## sources - [burner-redis (zig port)](https://tangled.org/zzstoatzz.io/burner-redis) — `src/scripting.zig` (embedded Lua 5.1 engine) - [docket](https://tangled.org/zzstoatzz.io/docket) — `src/scripts/` (the three atomic scripts) - [burner-redis (rust, upstream)](https://github.com/PrefectHQ/burner-redis)