Skip to content

whispem/asm.kvstore

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kvstore

Single-node, in-memory key/value store over TCP, written in x86-64 assembly (NASM). No libc, raw Linux syscalls only. Statically linked, ~13 KB binary, ~3 MB RSS.

Build & run

make
./kvstore [port]        # default 4242

Requires Linux x86-64, nasm, ld.

$ printf 'SET user:1 hello\nGET user:1\nGET nope\n' | nc localhost 4242
OK
hello
(nil)

Tests (need python3):

./kvstore 4747 &
python3 smoke_test.py 4747     # protocol + storage regression suite
python3 killer.py 4747         # concurrency: mid-line client + 200 simultaneous clients

Docker (optional)

Not required for anything below — kept as a convenient way to ship or try the server. The binary is fully static, so the runtime image is FROM scratch: a single ~13 KB file.

docker build -t kvstore .
docker run --rm -p 4242:4242 kvstore        # override the port: ... kvstore 5000

On ARM64 hosts (Apple Silicon), force the target arch; Docker Desktop runs it through Rosetta or QEMU:

docker build --platform linux/amd64 -t kvstore .
docker run --rm --platform linux/amd64 -p 4242:4242 kvstore

Developing on macOS (no Docker)

The binary targets Linux x86-64, so on a Mac it builds and runs inside a lightweight Linux VM. With OrbStack:

orb create -a amd64 ubuntu kv     # once: amd64 Ubuntu machine (Rosetta-backed)
orb -m kv                         # shell in; the macOS home is mounted at the same path
sudo apt update && sudo apt install -y nasm make binutils netcat-openbsd
cd ~/path/to/kvstore
make && ./kvstore 4242

Edit on the macOS side, make in the VM — same files, no syncing. From macOS the server is reachable at kv.orb.local:4242. Inside the VM, poke it with nc -N (-N makes netcat exit once its input ends; without it nc lingers — harmless to the server, annoying for you). Any Linux VM (Lima, UTM, WSL2 on Windows) works the same way.

Protocol

Plain text, one command per \n-terminated line (trailing \r stripped). Keys cannot contain spaces or newlines; values may contain spaces.

Command Reply
SET <key> <value> OK
GET <key> <value> | (nil)
DEL <key> OK | (nil)
PING PONG
QUIT BYE, then closes

Errors: ERR usage: ..., ERR unknown command, ERR store full, ERR arena exhausted, ERR line too long (line > 64 KB, connection closed).

Design

  • FNV-1a 64-bit hashing, open addressing with linear probing, tombstones on delete
  • 65,536 slots of 32 bytes (2 MB table), insert cap at 49,152 occupied slots
  • 256 MB anonymous mmap arena, bump allocation, key+value stored contiguously
  • epoll event loop, non-blocking sockets; per-connection 64 KB read buffer (fd-indexed state table), partial lines carried across reads
  • Replies via sendto + MSG_NOSIGNAL: a dead peer cannot SIGPIPE the process
  • Measured ~47k ops/s pipelined over loopback (round-trip through a Python client)

Done

  • TCP server: socket / bind / listen / accept4, SO_REUSEADDR, port from argv
  • Commands: SET, GET, DEL, PING, QUIT
  • FNV-1a hash table with linear probing and tombstones
  • Tombstone reuse on insert (delete/insert cycles don't degrade the table)
  • mmap bump-allocator arena, single allocation per SET
  • Buffered reads, pipelining, partial lines across read calls
  • CRLF tolerance, values with spaces, empty values
  • Oversized line rejection (error reply, then close)
  • Capacity guards: ERR store full, ERR arena exhausted
  • Partial-write-safe send loop, SIGPIPE-proof (MSG_NOSIGNAL)
  • Smoke test suite: 20k keys, 30 KB values, tombstone cycles, pipelining, cross-connection reads
  • Multi-stage Dockerfile, FROM scratch runtime (binary verified self-contained via chroot into an empty root)
  • epoll event loop: non-blocking sockets, per-connection read state, 200+ simultaneous clients

Roadmap

  • Write buffering for slow readers: queue replies per connection, arm EPOLLOUT (currently a slow reader whose socket buffer fills up is dropped)
  • RESP protocol compatibility (talk to redis-cli directly)
  • Persistence: append-only file, replay on boot, periodic fsync
  • Key expiry: EXPIRE / TTL, lazy eviction on access
  • More commands: EXISTS, INCR, KEYS / SCAN, STATS
  • Arena reclamation: free old blobs on overwrite (free list or compaction)
  • Table resize + rehash instead of the fixed 64k slots
  • Backward-shift deletion (drop tombstones entirely)
  • Binary-safe values: length-prefixed protocol, allow \n in payloads
  • Graceful shutdown on SIGINT / SIGTERM
  • Flags: bind address, table size, arena size

Known limits

  • Linux x86-64 only
  • A slow reader whose socket buffer fills up gets disconnected (write buffering not yet implemented — first item on the roadmap)
  • Up to 1024 simultaneous connections (fd-indexed state table)
  • No persistence: data dies with the process
  • Command line must fit in 64 KB
  • Overwriting a key leaks the old blob in the arena (bump allocator, by design for now)

About

A single-node key/value store over TCP, written in pure x86-64 assembly. No libc — syscalls only.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages