Skip to content

Latest commit

 

History

History
73 lines (52 loc) · 6.47 KB

File metadata and controls

73 lines (52 loc) · 6.47 KB

CLAUDE.md

This file provides guidance to Claude Code when working in this repository.

Project Overview

zigkv — an in-memory, Redis-compatible key-value store written in pure Zig. Zero dependencies, single statically-linked binary, RESP2 protocol compatible (works with redis-cli and redis-benchmark).

Key facts:

  • Requires Zig >= 0.15.2 (declared in build.zig.zon).
  • Linux-only: the server is built on epoll (src/server.zig). It will not compile or run on Windows/macOS.
  • No external packages — .dependencies = .{} in build.zig.zon.

Build, Run, Test

zig build                          # debug build → ./zig-out/bin/zigkv
zig build -Doptimize=ReleaseFast   # release build (use for server/perf work)
zig build run -- server 6379       # build and run server on a port
zig build run -- ping              # send a PING to 127.0.0.1:6379
zig build test                     # compile + run unit tests
./bench.sh [requests] [connections]  # benchmark zigkv vs real Redis (needs redis-server + redis-benchmark)

CLI usage of the built binary: zigkv server [port] (default port 6379) and zigkv ping.

Architecture / Source Layout

All code lives in src/. Import graph: main.zig → server.zig → (store.zig, resp.zig, commands.zig). src/root.zig re-exports the same modules as an embeddable zigkv module (b.addModule("zigkv", ...) in build.zig) for in-process use (no socket/Redis needed).

File Role
src/main.zig Entry point. Parses args, picks the allocator, dispatches server / ping.
src/server.zig Single-threaded, non-blocking epoll (edge-triggered) event loop. Owns connections, does read → parse → execute → flush; reads drain to EAGAIN every wakeup.
src/resp.zig RESP2 protocol parser. Non-allocating scan-then-parse, supports pipelining.
src/commands.zig Command dispatch: parses the ParsedCommand, mutates Store, returns an owned RESP response.
src/store.zig In-memory data structures: StringHashMap-backed strings (each entry is one allocation holding key+value), lists, hashes, sets.

Key Design Decisions & Gotchas

  • Allocator choice (main.zig): debug builds use GeneralPurposeAllocator (slow safety bookkeeping, catches leaks); release builds use std.heap.smp_allocator. Don't change this split casually.
  • Per-connection arena (server.zig): each Connection has an ArenaAllocator that is reset after every fully-drained batch of pipelined commands. Per-request allocations (parsed args, plus owned buffers from LPOP/HGET/HGETALL/LRANGE/SMEMBERS/GETSET) live in the arena and must not outlive the reset. Since the speed pass, commands.execute appends its RESP reply directly into the connection's long-lived write_buf (grown only with the connection allocator) and returns void — no arena response slice, no appendSlice copy. handleReadable records write_buf.items.len before each command and rolls back to it on error, appending -ERR command error.
  • RESP parser (resp.zig): two-phase design — scanComplete finds the byte length of one complete command without allocating (safe to call repeatedly on a growing non-blocking read buffer); parseComplete then allocates args. tryParse returns consumed, the number of bytes belonging to that command — the caller must shift/drop exactly those bytes off read_buf (see the copyForwards/shrinkRetainingCapacity in server.zig). Never assume one recv() == one full command.
  • epoll is edge-triggered (server.zig): EPOLLET cuts kernel wakeups, but every wakeup must drain the socket to EAGAIN (the read loop has no early-out) or the next edge is lost.
  • Command dispatch (commands.zig): a first-byte switch on the lowercased command name narrows to a group, then the existing std.ascii.eqlIgnoreCase(cmd, "...") + arity checks, ordered with hot commands (SET, GET, INCR, PING) first. Replies go through the file-scope Resp helper (stack-buffer std.fmt.bufPrint, zero heap allocations per reply) appended straight into the caller's write buffer; the entry point is execute(scratch_alloc, buf_alloc, out, store, parsed) !void. Add new commands by following the pattern inside the matching group. Command names are matched case-insensitively; args are not.
  • TTL is stored per-key as ?i64 expiry (ms precision); EXPIRE/TTL/PERSIST/SET ... EX N are implemented. TTL is applied lazily on access, not via a background sweeper.
  • Response format: simple strings (+OK\r\n), bulk strings ($<len>\r\n...\r\n), integers (:<n>\r\n), errors (-ERR ...\r\n), arrays (*<n>\r\n).

Supported Commands

String: SET (with EX TTL), MSET, GET, MGET, GETSET, APPEND, EXISTS, INCR, INCRBY, DECR, DECRBY, DEL, EXPIRE, TTL, PERSIST, DBSIZE, TYPE, FLUSHALL, CONFIG, PING List: LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE Hash: HSET, HGET, HGETALL, HDEL, HEXISTS Set: SADD, SREM, SISMEMBER, SCARD, SMEMBERS

Known Discrepancies (README vs. Code)

The README advertises features that are not implemented in the current source. Don't assume they exist:

  • Batch mode (zigkv batch, NDJSON input) — NOT implemented; main.zig only handles server and ping.
  • Persistence / snapshots (zigkv save dump.json / zigkv load dump.json) — NOT implemented. No JSON read/write code exists (only the PERSIST command, which is unrelated).
  • Built-in benchmark (zigkv bench) — NOT implemented; benchmarking is done via the shell script bench.sh.
  • README's benchmark table numbers are real measured runs but hardware-dependent/noisy (see README note); re-run bench.sh rather than trusting them.

Testing

zig build test compiles and runs tests declared in src/. Currently no test "..." blocks exist in the codebase — the test build step succeeds but runs nothing. When adding tests, use the standard test block syntax and std.testing (e.g. std.testing.allocator, std.testing.expectEqualStrings). The resp.zig parser and store.zig operations are the most natural candidates for unit tests.

Code Style

  • Match the existing conventions: snake_case names, structs with explicit field types, std.* imports grouped at top, doc comments on non-obvious public functions (//! module docs, /// for functions).
  • The code favors dense explanatory comments — keep them when editing.
  • No formatting step configured; the code follows zigfmt by hand. Run zig fmt on any file you touch.