This manual is the detailed technical reference for the project.
It is the companion to the concise, GitHub-facing README.md:
-
README.md— project identity, status, and quick start. -
docs/(this manual) — architecture, interfaces, verification, and implementation detail in engineering-manual style. -
docs/architecture/,docs/adr/,docs/correctness/,docs/testing/,docs/benchmarks/— prose notes, decision records, and measured results, linked from the chapters below rather than duplicated.
Conventions used throughout:
-
monospacemarks commands, file paths, identifiers, and literals. -
Unmeasured values are written as
TBD (unmeasured)— never estimated. -
When prose and code disagree, the code plus its tests win until the documentation is updated in the same milestone.
Start with Introduction for scope and reading order,
then Architecture for the system overview. Chapters are assembled
with include:: directives below: to add, delete, rename, or reorder a
chapter, edit one line in this file and add or remove the matching file
under sections/.
Keep the blank lines between directives: a chapter ending in a list would
otherwise swallow the next chapter’s title.
|
1. Introduction
This chapter states what the system is, what this manual covers, and how to navigate the rest of the documentation.
1.1. Purpose and scope
The system under documentation is an ordered, persistent key-value store with crash-safe durability semantics. This manual describes its architecture, component design, external interfaces, verification strategy, build process, measured performance, and known limitations.
This manual describes the system as checked in. Statements about future work (replication, transactions, sharding) are marked as such and are not part of the implemented behavior. See Known Limitations for the explicit list of what is not claimed.
1.2. How to read this manual
-
Engineers new to the project: read Introduction and Architecture first, then the chapter relevant to your task.
-
Operators: Interfaces, Implementation, and Known Limitations are the most relevant chapters.
-
Contributors: Verification defines what "proven correct" means here, and Implementation defines the local build and test gates.
The recommended reading order is:
-
Start with the architecture overview (Architecture).
-
Drill into component design (Design) as needed.
-
Consult interfaces (Interfaces) and the programming model (Programming Model) when writing client code.
-
Check verification (Verification) before changing storage logic.
1.3. Conventions
Key terms used throughout this manual:
-
Acknowledged — a write the system promises to survive a crash.
-
Prefix — the initial contiguous run of a log; recovery replays a prefix.
-
Litter — files left behind by a crash that recovery must ignore or reap.
| When prose and code disagree, the code plus its tests win until the documentation is updated in the same milestone. |
1.4. Related documents
The Markdown files already in this repository remain the canonical prose record. This manual summarizes them and links to them:
-
architecture/overview.md— system topology and write lifecycle -
architecture/status.md— what is actually landed, crate by crate -
architecture/durability.md— acknowledgement rule and crash windows -
architecture/consistency.md— isolation target and validation -
adr/— architecture decision records -
correctness/strategy.md— reference models and testing strategy -
testing/strategy.md— simulator, chaos, soak, and CI split -
testing/inventory.md— what each test file proves -
benchmarks/— measured results only
2. Architecture
This chapter describes the system topology, the implemented components, and the write lifecycle vocabulary used everywhere else in this manual.
2.1. System topology
The target topology is a sharded, replicated store. What exists today is the bottom of the storage engine running as a single-node process (the layers in Implemented components and their crates.), plus Raft consensus through durability: a deterministic core, a seeded simulation harness, and crash-safe persistence — not yet wired to the server (R3). See ADR-006 for the R1/R2/R3 split.
CLIENTS -> RPC/API -> Router -> Txn Coordinator -> Shard Directory -> SHARD A/B/C -> Raft Group (N1 N2 N3) -> Storage Engine (WAL -> Memtable -> SSTables)
2.2. Components
| Component | Crate | Responsibility |
|---|---|---|
Ordered KV model |
|
In-memory |
Write-ahead log |
|
Framed, checksummed records; segments; replay; crash recovery |
Storage engine |
|
WAL-backed memtable; flush; manifest publishing; compaction; merged reads |
Immutable tables |
|
Checksummed blocks, index, footer, Bloom filters, tombstones |
Server and CLI |
|
TCP JSON-lines API; snapshot reads; background compaction timer |
Raft consensus (unwired) |
|
Deterministic core; seeded sim; crash-safe |
2.3. Write lifecycle vocabulary
The following terms are never conflated in this manual:
received -> replicated -> committed -> persisted -> applied -> acknowledged
There is no replication yet, so the only acknowledgement point is
Wal::sync(): everything at or before the last sync survives a crash.
The exact rule is stated in architecture/durability.md and summarized
in Design.
3. Design
This chapter records the per-component design: what each layer stores, what it promises, and where the crash windows are.
3.1. Write-ahead log
The log is a sequence of framed records carrying dense sequence numbers. Each frame carries a checksum. Replay reads a prefix:
-
a clean prefix replays fully,
-
a torn tail (short read) is truncated and never replays,
-
a corrupt frame stops replay; the suffix is truncated.
Wal::open(&opts) -> (Wal, Vec<WalEntry>, RecoveryReport)
wal.append_put(key: Vec<u8>, value: Vec<u8>) -> u64 // dense seqno
wal.append_delete(key: Vec<u8>) -> u64
wal.sync() -> () // acknowledgement point
wal.synced_seq() -> Option<u64>
Segment::replay(path) -> (ReplayOut, u64)
| Sequence numbers stay dense across reopen and rotation, so no write is ever replayed twice or skipped. |
3.2. Memtable engine
The engine applies PUT to the WAL first, then to the in-memory table.
Restart replays the log into a fresh table. Rejected writes leave no trace.
3.2.1. Flush and manifest publishing
The live table set is defined by tables/MANIFEST (version 1), not the
directory listing. Flush publishes in order: write sst-000007.sst.tmp
(zero-padded 6-digit ids), sync, rename, directory fsync, update MANIFEST
(MANIFEST.tmp, sync, rename, directory fsync), then delete WAL segments.
3.2.2. Crash windows
-
Crash before rename: only
*.tmplitter, deleted at open. -
Crash after rename, before manifest: unlisted orphan
.sst, deleted at open; the WAL still holds the data. -
Crash after manifest, before WAL deletion: table and WAL overlap as harmless duplicates; the next flush clears the WAL.
Never treat the directory listing as the live set. An unlisted
.sst file is either litter or an orphan and must be reaped, never merged
into reads.
|
3.3. SSTables and compaction
Tables are immutable: checksummed blocks, an index, a footer, per-table Bloom filters, and tombstones for deletes. When 8 tables accumulate, the oldest batch of 8 is merged (size-tiered oldest-batch policy) with the same publish protocol as flush, so compaction crash windows reduce to the flush cases above.
Tombstone garbage collection runs only on full merges with an empty memtable, where no older layer can exist below a dropped tombstone, so a dropped tombstone can never resurrect a shadowed key.
3.4. Raft consensus
The core (fig-raft::Node) is a pure state machine: no I/O, no threads,
no clock. Inputs are messages plus timer events; outputs are outbound
messages plus committed entries. Elections, log matching
(truncate-then-append, no holes), current-term commit advancement, and
exactly-once in-order application all live here (R1; see ADR-006).
Durability is explicit (R2). The core reports what changed via
take_dirty; persist::Store writes current_term, voted_for, and the
log to raft-state.json atomically (tmp, sync, rename, directory fsync)
and replays it into Node::restore on restart. Heartbeats mark nothing
dirty, so steady-state leadership costs no disk write. An uncommitted
suffix may be overwritten by a new leader — it is never applied before
commit.
A seeded simulation harness (virtual ticks, partitions, drops) proves single-leader election, ordered replication, failover preserving the committed prefix, and partition healing deterministically on fixed seeds.
The core is not yet wired to the server. Client writes still ack on
Wal::sync() without replicating; majority-ack replication is R3.
|
4. Interfaces
This chapter specifies the external interfaces: the wire protocol and the configuration limits. It is the contract between the server and its clients.
4.1. Wire protocol
The server speaks newline-delimited JSON over TCP: one request line gets
one response line. Keys and values are arbitrary bytes, so they travel
base64-encoded (standard alphabet) in *_b64 fields. Every request
carries a seq number for pipelining; every response echoes that seq
with ok, and failures add a stable code (plus a human message) so
clients branch on codes, never on text.
{"seq":1,"op":"put","key_b64":"ay4=","value_b64":"dg=="}
{"seq":1,"ok":true}
{"seq":2,"op":"get","key_b64":"ay4="}
{"seq":2,"ok":true,"value_b64":"dg=="}
| Request | Fields | Effect |
|---|---|---|
|
|
Stage write; acknowledged at the next |
|
|
Snapshot read; missing keys return |
|
|
Stage tombstone; response reports |
|
|
Ordered range read; |
|
(none) |
Persist the staged prefix; defines the acknowledgement point |
|
(none) |
Force memtable flush into a manifest-published table |
|
(none) |
Force a merge of live tables; response reports |
|
(none) |
Return |
Unknown operations and malformed lines return INVALID_ARGUMENT and the
connection stays up; see Verification for the wire gate tests.
Batch many put requests behind a single sync. Acknowledgement is
a prefix property, so one sync covers every write before it.
|
4.2. Configuration limits
Keys and values are arbitrary bytes subject to fig-core::Config::Limits,
validated at the API boundary before they reach the log:
-
maximum key length: 64 KiB (
max_key_bytes), -
maximum value length: 4 MiB (
max_value_bytes), -
maximum decoded request line: 32 MiB (
max_request_bytes).
Oversized inputs are rejected with INVALID_ARGUMENT.
5. Programming Model
This chapter shows how client code uses the interfaces from Interfaces: the basic write/read cycle, error handling, and the durability contract in practice.
5.1. Basic write/read cycle
The fastest path is fig-cli, which takes raw UTF-8 strings and handles
the base64 framing itself (arbitrary binary keys remain a client-library
concern, not a shell concern):
fig-cli --addr 127.0.0.1:7001 put k1 v1
fig-cli get k1
fig-cli scan a z 100
fig-cli stats
The equivalent raw session over TCP (fig-server defaults to
127.0.0.1:7001), one JSON object per line:
{"seq":1,"op":"put","key_b64":"azE=","value_b64":"djE="}
{"seq":2,"op":"sync"}
{"seq":3,"op":"get","key_b64":"azE="}
In application terms the cycle is:
-
Connect to the server over TCP.
-
Send
putrequests, each tagged with a client-chosenseq. -
Send
syncto acknowledge the staged prefix. -
Send
getorscanto read from a snapshot; match responses to requests by the echoedseq.
5.2. Errors
All failures use shared error kinds (fig-core::Error) surfaced on the
wire as stable code strings: bad input is INVALID_ARGUMENT, a missing
key is NOT_FOUND. Corruption is fail-safe: replay stops at the first
corrupt frame and truncates the suffix rather than surfacing torn data.
5.3. Durability contract
sync is the only acknowledgement point. In application terms:
-
writes before the last successful
syncresponse survive a crash, -
writes after it may be lost,
-
reads always observe a consistent snapshot of the acknowledged prefix.
6. Verification
This chapter defines what "proven correct" means: reference models, differential tests on randomized operation streams, crash gates, and the continuous gates that enforce them.
6.1. Strategy
Reference models are compared against the implementation on seeded random
operation streams: the naive oracle must agree with the real structure on
every get and scan, including tombstone behavior. Crash tests plant
failures at every publish window and assert that reopen yields exactly the
acknowledged state.
| Randomized gates use fixed seeds so failures reproduce deterministically. |
6.2. Test gates
| Location | What it proves |
|---|---|
|
Roundtrip, ordering, scan bounds; oracle agreement up to 10k ops |
|
Frame roundtrip; torn tails read as torn; dense seqnos across reopen |
|
Acked prefix survives seeded crash campaigns; corruption truncates suffix |
|
Replay, flush, tombstone suppression, Bloom behavior, manifest roundtrips |
|
Kill/restart, mid-flush litter, and compaction windows reopen clean |
|
Wire roundtrip, garbage-input survival, restart durability, concurrency |
|
One leader per term on seeds; ordered replication; solo self-election; minority isolation; failover preserving the committed prefix |
|
Vote + entries survive reopen; heartbeats write nothing; tmp litter
reaped, corrupt state is |
The local gate runs everything:
./scripts/check.sh # fmt --check + clippy -D warnings + full test suite
Two operational gates run outside cargo test: fig-bench --verify-only
recomputes the oracle for a load run with zero shared state, and
scripts/soak.sh repeats load/sync/SIGKILL/restart cycles, re-verifying
every earlier cycle’s keys after each restart.
| Storage logic changes must update the corresponding gate in the same commit. A passing build with a weakened oracle is a regression, not an improvement. |
7. Implementation
This chapter covers the repository layout, the build, and the continuous integration gates.
7.1. Repository layout
crates/fig-core/ errors, config/limits, KV oracle, tracing
crates/fig-wal/ the log (record / segment / wal) + crash tests
crates/fig-storage/ memtable engine + LSM flush/merge + equivalence gates
crates/fig-sstable/ immutable tables + indexed reads + differential tests
crates/fig-server/ TCP JSON-lines server + fig-cli + wire tests
crates/fig-raft/ deterministic core + sim + crash-safe persistence
docs/ manual source (this system) + existing Markdown notes
scripts/check.sh local gate: fmt + clippy + tests
7.2. Building and testing
cargo build --workspace # debug build of all crates
cargo test --workspace # full test suite
./scripts/check.sh # canonical gate: fmt + clippy + tests
make docs # build this manual into build/docs/
Release builds use link-time optimization ([profile.release], single
codegen unit); see benchmarks/ for measured release-vs-debug comparisons.
Run ./scripts/check.sh before every push. CI mirrors it, plus the
dedicated WAL crash gate.
|
8. Performance
This chapter explains how performance is measured and what the current
numbers show. Measured results live in benchmarks/; this chapter states
the method and the interpretation so numbers are never read out of context.
8.1. Method
All runs use fig-bench (per-operation latency in microseconds, p50/p99
over the full run) against a local server with a stated durability policy
(for example, an explicit sync every 500 puts, so every measured put is
acknowledged). Hardware, compiler, dataset, and durability policy are
recorded with each run because throughput is meaningless without them.
Throughput and latency are related by the offered load. For a single
sequential client over n operations with mean latency \(E[L\)]:
With concurrent clients the wall-clock form applies instead
(\(T = n / T_{wall}\)), which is why single-client and multi-client
figures are reported separately in benchmarks/.
8.2. Reading the results
-
GETthroughput is typically round-trip bound; compiler optimization level moves it little. -
PUTthroughput is execution bound (flush and compaction rewrites), so release optimization and moving merges off the write path dominate. -
Foreground merges stall writes visibly in p99/max; background compaction trades that tail for steady throughput.
| Concurrency figures are loopback-only unless stated otherwise, and no power-loss testing is claimed. See Known Limitations. |
9. Known Limitations
This chapter lists what the system explicitly does not do yet. It exists so no reader infers guarantees from the chapters above.
-
No replication: a disk loss is not survived, only a process crash. The Raft core through durability (deterministic elections, log matching, crash-safe
Store) is landed and tested, but it is not wired to the server yet — writes ack onWal::sync()without a majority round. -
Single node only: no shards, no Raft groups serving traffic, no distributed transactions.
-
No power-loss testing:
kill -9exercises page-cache-survives crashes; unsynced-tail loss is covered by truncation simulation, not by pulling the plug. -
No fsync-per-write figures: published runs batch syncs.
-
Checksums exist at the WAL frame level; there are no checksums above it.
-
Benchmarks are loopback-only with a small client count; there are no multi-host latency claims.
| Do not deploy this system where disk loss or multi-node consistency is required. Server replication wiring (R3) is next; it is not here. |
When a limitation above is fixed, update this chapter, the affected design chapter, and the corresponding decision record in the same commit.