Liking cljdoc? Tell your friends :D

The durable-storage kernel — a reusable substrate for new persistent layers

Navigation: ↑ Persistence architecture · Lock-free overlay · Durability & recovery · Storage backends

The persistent-ARTrie family is one application of a smaller, general engine: the unit-agnostic code under src/persistent_artrie/core/. That engine — the durable-storage kernel — knows nothing about dictionaries, radix tries, or Unicode. It knows how to make an in-memory structure durable, crash-safe, and lock-free over a block device. This document presents core/ as a substrate: what it gives you, which seams you implement, and how to build a new persistent file layer on it that is not a dictionary at all.

Thesis. To make a new structure durable, you implement two small seam traits — a block-storage backend and a key/record model — and, if your structure is a copy-on-write tree, a handful of Template-Method hooks. In exchange you inherit the data-loss-critical machinery: the write-ahead log, the Order-A durability protocol, the committed-watermark checkpoint bound, crash recovery, the buffer pool, epoch-based reclamation, and memory-pressure eviction — each already machine-checked.


Terms of art (defined before first use)

TermDefinition
seamA trait whose implementation you supply so generic kernel code can call into your structure. BlockStorage and KeyEncoding are seams.
Template MethodA design pattern (Gamma et al. 1994) where an invariant algorithm skeleton is fixed in a base and only named steps vary. The kernel fixes the Order-A write ordering and lets you vary the node-building step.
Order-AThe kernel's fixed durable-write ordering: append+sync WAL $\to$ publish via CAS $\to$ commit-rank + mark watermark. Its inverse, "publish then log" (Order-B), is rejected because it can expose a visible-but-not-durable write.
LSNLog-Sequence Number — a WAL record's monotone position.
watermarkThe largest LSN $L$ with every $\ell \in 1..=L$ committed — the only safe checkpoint bound under out-of-order commit.
arc-swapA single-word atomic cell holding an Arc<T> that readers load without a lock and writers replace by CAS.

The substrate at a glance

How a new layer rides the kernel — a dashed grey box ④ Your new persistent layer connects to the durable-storage kernel (core/)'s three service groups:

How a new persistent layer rides the durable-storage kernel: a dashed grey box ④ 'Your new persistent layer' connects to the kernel (core/)'s three service groups — ① extension seams you IMPLEMENT (example: the ARTrie family's overlay hooks), ② durability services you INHERIT (example: the suffix-graph family using the WAL), and ③ storage + concurrency services you INHERIT (buffer manager, epoch + MVCC, eviction).

The kernel's three groups in detail — the modules inside each group:

Inside the durable-storage kernel (core/): the three service groups broken into their modules — ① extension seams you implement (BlockStorage for 256 KB blocks, the KeyEncoding key/record model, and the LockFreeOverlay → DurableOverlayWrite → OverlayCheckpoint Template-Method hooks), ② durability services you inherit (the WAL, DurabilityPolicy, CommittedWatermark, RecoveryManager), and ③ storage + concurrency services you inherit (BufferManager, EpochManager + MVCC, eviction).

Seam 1 — BlockStorage: the storage backend

BlockStorage (core/block_storage.rs) is the interface every layer above is generic over (S: BlockStorage). It abstracts a file as an array of fixed-size blocks:

  • Block $0$ holds a 64-byte FileHeader (magic, version, root pointer, entry count, free-list head, checksum, and the image-coverage frontier image_checkpoint_lsn).
  • Blocks $1..N$ each hold BLOCK_SIZE = $256\text{ KB}$ of data.
  • Block IDs are 24-bit (MAX_BLOCK_COUNT = 2^{24}), so a single file addresses up to $2^{24}$ blocks $= 4\text{ TB}$. (A swizzled child pointer packs block_id in only 23 bits, so nodes reached through swizzled links occupy the first $2^{23} = 8\text{ M}$ blocks, $2\text{ TB}$ — see storage-backends.md.)

The trait is Send + Sync + 'static (backends own their resources and carry no borrowed lifetime, so a whole structure can be erased behind a dyn object). Its surface:

pub trait BlockStorage: Send + Sync + 'static {
    fn read_block(&self, block_id: u32, buf: &mut [u8; BLOCK_SIZE]) -> Result<()>;
    fn write_block(&self, block_id: u32, buf: &[u8; BLOCK_SIZE]) -> Result<()>;
    fn read_bytes(&self, block_id: u32, offset: usize, buf: &mut [u8]) -> Result<()>;
    fn write_bytes(&self, block_id: u32, offset: usize, data: &[u8]) -> Result<()>;
    fn allocate_block(&self) -> Result<u32>;      // must be safe to call concurrently
    fn free_block(&self, block_id: u32) -> Result<()>;
    fn read_header(&self) -> Result<FileHeader>;
    fn write_header(&self, header: &FileHeader) -> Result<()>;
    fn root_ptr(&self) -> Result<u64>;            // + set_root_ptr
    fn entry_count(&self) -> Result<u64>;         // + set_entry_count
    fn image_checkpoint_lsn(&self) -> Result<u64>;// + set_* (the #48 double-apply guard)
    // …read_header_bytes / write_header_bytes for custom (e.g. 96-byte) headers
}

Two backends ship — MmapDiskManager (default; page-cache, best for single-block I/O) and IoUringDiskManager (O_DIRECT + ring pool, best for batch I/O and true durability). I/O buffers are AlignedBlocks (#[repr(C, align(4096))]), so the same buffer pool serves both mmap and O_DIRECT. To bring your own storage (an encrypted file, a network block device, an in-memory mock for tests), implement these ~14 methods; everything above is generic over the result. See storage-backends.md.


Seam 2 — the key / record model

For a trie-shaped layer, KeyEncoding (core/key_encoding.rs) is the model seam: it fixes the storage Unit, the public traversal Token, the reconstructed Term, the on-disk magics, and the path-compression cap, so one generic node type serves every alphabet. The three shipped markers are ByteKey, CharKey, U64Key<PREFIX>; the design is detailed in abstractions.md.

For a non-trie layer, you do not need KeyEncoding at all — you define your own record model and serialize it into blocks yourself, exactly as the suffix-graph family does with its native graph representation. The kernel's durability services (below) operate on opaque WalRecord payloads and Lsns, not on trie keys.


Seam 3 — the overlay Template-Method hooks (for copy-on-write trees)

If your structure is an immutable, copy-on-write tree published through an atomic root (the ARTrie shape), you implement a short trait tower in core/overlay/ and inherit the entire durable-write control flow:

Trait (file)You supply (the seam hooks)You inherit (the fixed skeleton)
LockFreeOverlay (flip.rs)the atomic-root accessor, the read enginerouting, snapshot loads
DurableOverlayWrite (durable_write.rs)the WAL-record builder, the value domain, the node-building CAS publish, the present-hoistthe Order-A skeleton, the durability-policy gate, the commit-rank + watermark tail
OverlayCheckpoint (checkpoint.rs)how to serialize your node into arena slotscapture the immutable snapshot, publish the image, advance the watermark

The Order-A skeleton is data-loss-critical and lives in exactly one place. In literate pseudocode, the durable write the kernel performs for you is:

durable_write(op):                              # DurableOverlayWrite default method
    require policy ∈ {Immediate, GroupCommit}   # gate: "acknowledged ⟹ durable"
    if present_hoist(op) is a no-op:            # NON-FAULTING on the membership hot path
        return Unchanged                        # do NOT burn an LSN / punch a watermark hole

    data_lsn ← append_durable_wal(op)           # STEP 1: WAL append + fsync, BEFORE visibility
                                                #         (one append covers every CAS retry)
    loop:                                        # STEP 2: publish via the atomic-root CAS
        root  ← atomic_root.load()               #   read the current immutable root
        root' ← path_copy(root, op)              #   your seam: build the new version
        if atomic_root.compare_exchange(root, root'):  break   # the LINEARIZATION point
                                                #   on conflict, re-read and retry
    rank_lsn ← append_commit_rank(data_lsn)     # STEP 3: bind commit generation, durable
    watermark.mark_committed(data_lsn)          #   advance the contiguous committed prefix
    watermark.mark_committed(rank_lsn)          #   …covering BOTH LSNs so it never stalls
    return Applied

Order-B (CAS then log) is rejected: it can expose a visible-but-not-durable write. The single WAL append is never re-appended across retries (that would burn LSNs and punch holes in the watermark). A refused write (insert-once on an already-present key, a failed compare-and-swap) is burned for watermark liveness — mark_committed_burned(data_lsn) — but never ranked, so replay treats it as a no-op.

If your structure is not a copy-on-write tree, skip this seam and drive the durability services directly, as in Recipe B below.


What you inherit — the ready-made services

ServiceModuleWhat it gives you
Write-ahead logcore/wal/append-only, fsync-durable, CRC-checked records (WalRecord + Lsn); a concurrent append path with pending-segment handoff during sync.
Durability policycore/durability.rsDurabilityPolicy::{Immediate, GroupCommit, Periodic, None} — the fsync-frequency vs. throughput dial, backed by an AtomicEnumCell for lock-free reads.
Committed watermarkcore/committed_watermark.rsthe contiguous committed-prefix tracker that yields the only safe checkpoint_lsn under out-of-order lock-free commit.
Recoverycore/recovery.rsload the checkpoint image, replay the WAL tail above the image-coverage frontier, reconcile per-record commit order, stop fail-closed at a torn record.
Buffer managercore/buffer_manager.rsa frame pool over BlockStorage with fault-in, pinning, and batched dirty-page flush.
Epoch reclamation + MVCCcore/concurrency.rs, core/mvcc.rsEpochManager (safe memory reclamation under lock-free reads) and ReadTransaction (epoch-pinned snapshot reads).
Evictioncore/eviction/memory-pressure-driven unswizzling of cold subtrees to disk and read-fault-back.

The guarantees these services enforce (and prove)

\text{visible}(x) \;\implies\; \text{WAL-durable}(x)\ \text{at}\ \mathrm{lsn}(x) \le \mathrm{syncedLsn}
\qquad(\textbf{acknowledged} \implies \textbf{durable})
\text{checkpoint\_lsn} \;=\; \max\{\,L : \forall\,\ell \in 1..=L,\ \text{committed}(\ell)\,\}
\qquad(\text{a committed contiguous prefix})
\text{Recovered} \;=\; \text{durableCheckpoint} \,\cup\, \text{WAL-tail} \;=\; \text{visible}
\qquad(\text{no lost writes, no invented state})

These hold for any client of the kernel, because the machinery that enforces them is generic. They are model-checked in TLA⁺ (LockFreeDurableCheckpoint.tla, SharedPersistentConcurrency.tla, StorageSyscallOutcome.tla, …) and proved in Rocq (Spec/PublicDurabilityPolicySpec.v, Spec/PersistentWalAtomicitySpec.v, Spec/PersistentRecoveryReplayCompletenessSpec.v, …); the full cross-reference is in formal-verification-map.md.


Building a new persistent file layer — two recipes

Recipe A — a copy-on-write tree (the ARTrie shape)

  1. Storage. Reuse MmapDiskManager, or implement BlockStorage for your medium.
  2. Model. Define a KeyEncoding marker (or reuse ByteKey/CharKey/U64Key) fixing your unit width, term reconstruction, and on-disk magics.
  3. Node. Represent your tree as immutable nodes published through an AtomicNodePtr (arc-swap root); path-copy on mutation.
  4. Hooks. Implement LockFreeOverlay + DurableOverlayWrite + OverlayCheckpoint, supplying only the node-building CAS publish, the WalRecord builder, and the node-serialization step.
  5. Done. You now have lock-free reads, Order-A durable writes, watermark-bounded checkpoints, and crash recovery — verified — with no bespoke durability code.

The persistent ARTrie is this recipe; the entire byte/char/u64/vocab family shares one copy of steps 3–4 via the generic OverlayNode<K, V>.

Recipe B — a non-tree snapshot layer (the suffix-graph shape)

  1. Storage. As above — implement or reuse BlockStorage.
  2. Snapshot. Hold your whole structure as an immutable Arc<T> behind an arc_swap::ArcSwapOption root.
  3. Write. Follow Order-A by hand: append a durable prepared operation segment, rebuild a candidate Arc<T> off to the side, publish it by pointer-identity CAS (Arc::ptr_eq), then append a durable commit segment before acknowledging.
  4. Recover. On reopen, load the checkpoint image and replay the WAL tail, applying only prepared records whose commit marker is durable.

The suffix-graph family (PersistentSuffixAutomaton, PersistentSuffixTree, PersistentScdawg) is this recipe — durable substring indexes with no trie overlay, reusing the same WAL, checkpoint, watermark, and recovery services. See families.md and ../algorithms/persistent-suffix-graphs.md.


What is dictionary-specific vs. reusable

Reusable (the kernel — core/)Dictionary-specific (a client)
BlockStorage, AlignedBlock, FileHeaderthe ART Node4/16/48/256 layouts, CharBucket
WAL, DurabilityPolicy, CommittedWatermark, recoverythe KeyEncoding alphabets, term reconstruction
the Order-A DurableOverlayWrite skeletonthe per-op WalRecord payloads (Insert, Increment, …)
BufferManager, EpochManager, MVCC, evictionthe Dictionary/MappedDictionary trait surface
the overlay AtomicNodePtr + CAS-publish patternpath compression, adaptive edge storage

A new layer swaps the right-hand column and keeps the left. That separation — enforced by the grep-verified layering invariant — is what makes core/ a substrate rather than an implementation detail.

References

  • E. Gamma, R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Template Method, Strategy). Addison-Wesley, 1994.
  • C. Mohan et al. ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking… ACM TODS 17(1), 1992. DOI:10.1145/128765.128770
  • J. Driscoll, N. Sarnak, D. Sleator, R. Tarjan. Making data structures persistent. JCSS 38(1), 1989. DOI:10.1016/0022-0000(89)90034-2

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close