Skip to content

M1: the record plane and the .yo file #2

Description

@tamnd

The record plane and the file, fronted because this is the thing the lineage has never built. aki spent two years on data types and never started its file, and the durability story was still a document when the project moved on. Building the file first means every type after this is written against a real record plane.

Scope

  • The hybrid log with mutable, read only and stable regions, and per shard thread local epochs
  • Group commit, with the four durability modes: none, os, group, sync
  • Recovery from the last good superblock plus a log replay
  • F2 style lookup based compaction, quiesced, with dead byte accounting driving when it runs
  • The .yo format: dual superblock at offset 0 and 16384, the atomic root flip, log page headers, the collection catalogue
  • yo check
  • The independent minimal reader, which shares no code with the engine on purpose

Exit gate

  • 200k or more durable commits per second in group mode on NVMe with SQPoll
  • A 10 GB file opens in under 100 ms
  • Zero silent corruptions over 100k injected faults in the crash injection harness
  • The independent reader agrees with the engine on every file the fuzzer produces

Notes

The io_uring ladder from the VLDB 2026 study is the reason the commit target is set where it is. The measured rungs go 16.5, 19.0, 183.0 with async exec, 216.0, 238.0 with registered buffers, 300.0, 376.0 with IOPOLL and 546.5 with SQPoll, in thousands of transactions per second. Wrapping io_uring in Tokio erases most of that, which is why there is no async in the engine.

The independent reader is not a nice to have. It is the only test that can catch the format drifting away from what is written down, and it is what makes a yanked release still readable.

Spec: 16-implementation-plan.md M1, 06-record-plane.md, 07-yo-file-format.md.

Activity

  1. added
    milestoneMilestone tracking issue
    gateBlocks a milestone exit
    storageRecord plane, hybrid log, compaction, tiering
    formatThe .yo on-disk format and its stability
    on Aug 25, 2026
  2. reopened this on Aug 28, 2026
  3. tamnd commented on Aug 28, 2026

    @tamnd
    OwnerAuthor

    Reopened to audit this checklist against the code rather than against memory. Ten of the eleven boxes are ticked now, two of them because #32 closed them and eight because the code was already there and nobody had come back to mark it.

    The seven scope boxes, and where each one lives

    The hybrid log with three regions and per shard epochs. Region in crates/yo-record/src/log.rs with Stable, ReadOnly and Mutable, mutable_fraction and region_of deciding which is which. The epoch is per shard on the log itself and is stamped into every page header. The arena side reclamation epochs are separate and are in crates/yo-shard/src/epoch.rs, one cache line per shard, one relaxed store per batch rather than per operation.

    Group commit and the four durability modes. Durability in crates/yo-record/src/log.rs with None, Os, Group and Sync. Group is the default.

    Recovery from the last good superblock plus a log replay. Yo::open reads both slots and pick chooses the good one, which is the whole point of the dual superblock. Replay is crates/yo-record/src/replay.rs, and it is short because the append path did the work: the length is written last, the four bytes past the tail are always zero, and every record carries its own checksum, so the three ways a log can end all look the same from the reader's side.

    F2 style compaction with dead byte accounting. crates/yo-record/src/compact.rs. page_wants_compaction reads the page header's own dead fraction against COMPACT_TRIGGER, and compact_upto runs against a byte budget rather than to completion.

    The format. Two superblock slots at 0 and 16384, SUPERBLOCK_LEN being 16384 throughout crates/yo-format/src/superblock.rs. The root flip and its ordering is crates/yo-file/src/file.rs, and the ordering is the guarantee: data, then the slot, then the slot's own sync, so a checkpoint can be behind the truth and never ahead of it. Page headers are crates/yo-format/src/page.rs. The collection catalogue is crates/yo-format/src/catalog.rs.

    yo check. crates/yo-cli, built on the independent reader and not on the engine, which is the only way a checker can tell you the engine is wrong.

    The independent minimal reader. crates/yo-reader, 1420 lines, written from 06 and 07 rather than from the engine. Its tests are the one place in the tree where depending on the engine is allowed, because the agreement between the two is the deliverable.

    The exit gate

    The 10 GB open row is measured, not asserted. cargo bench -p yo-file --bench open gives 263 microseconds at 320 regions, which is ten gigabytes, against a budget of 100 milliseconds. It is not close and it should not be. Open reads two superblock slots and 320 region headers of 32 bytes each and nothing else, so what the number reflects is the design rather than the disk. The fixture is sparse, which is honest for what is being measured, because open never touches a record.

    The 100k fault row now runs in CI. ci.yml runs twenty thousand on every push, which is the right number for a per push job and was never the gate. The gate number runs nightly in deep across five shapes with seeds that do not overlap, so a failure names one run and one seed and reproduces from the command line. The run on #32 was a hundred thousand trials in 44.4 seconds, all five shapes clean.

    The reader agreement row was a claim nobody had run, because the gate says every file the fuzzer produces and there was no fuzzer producing files. fuzz/fuzz_targets/file.rs is that fuzzer. It builds a real .yo file on disk across up to four shards from fuzzer chosen keys, values, TTLs and tombstones, and checks three separate claims: the engine's own replay sees what went in, the independent reader sees what went in, and the two agree on the superblock and the checkpoint entries field for field. Three claims rather than one, so a failure says which side is wrong instead of leaving you with a disagreement and no suspect.

    The one row that stays open

    200k or more durable commits per second in group mode on NVMe with SQPoll. This cannot be attempted yet and will not be ticked on a substitute. SQPoll is io_uring, there is no io_uring sink in the tree, and no machine we have access to meets the gate box definition in bench/00 section 7. The reactor in M2 is where the io_uring path arrives, and the number gets measured then, on a box that qualifies.

    That is the only thing left in M1. Leaving it open on a milestone whose other ten boxes are done is deliberate: a performance row that gets ticked on a machine that does not qualify is exactly the kind of number the confound registry exists to stop, and aki's four wrong numbers all came from methodology rather than from code.

  4. tamnd commented on Aug 28, 2026

    @tamnd
    OwnerAuthor

    #34 is merged, so the ring is no longer a crate nobody writes through. LogFile::use_ring puts a shard's log on it, the shard hands its bytes over instead of stopping for a pwrite and an fdatasync, and a commit waiting on durability parks on an address instead of on a syscall.

    Three rules had to hold before any of that is safe, and each has a test with its name on it. Bytes have to outlive the call, so every write is staged into a pooled buffer the writer owns. Two writes to one page cannot be out at once, because io_uring does not order submissions and every flush rewrites the page header, so the older header landing last is a page that lies about how many records it has. A sync means nothing until the writes it covers have landed, because a queued fsync runs in parallel with the writes queued ahead of it, so sync records the address it has to cover and the fsync goes out from poll once the last write is back.

    Two durability modes were quietly wrong under an asynchronous sink and are fixed in the same PR. sync mode used to sync and reply, which is a reply before the fsync when the sink is asynchronous, and it now parks the caller on the address instead. os mode promised the operating system had the bytes while they sat in the submission queue, and a page boundary now submits as well as writes.

    The 200 thousand durable commits row stays open, and this is the first update where the reason is hardware rather than missing code. The path now runs end to end on real io_uring, checked on server3 with kernel 6.8, and it lands the same bytes as the synchronous path. What it does not have is a device worth quoting. server1 and server3 are both QEMU virtio disks on shared VPS hosts, and a benchmark row there comes back with error bars around plus or minus 30 percent, wide enough that the ring and pwrite columns cannot be told apart in either direction. gamingpc has the fastest disk of the four machines and runs Windows, where there is no io_uring at all.

    So the row needs the box described in bench/00 section 7, which is NVMe with SQPoll and nothing else running on it, and none of server1, server2, server3 or gamingpc is that box. The number will come from a run on one, not from an argument here. Everything else in M1 is ticked.

  5. added a commit that references this issue on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    formatThe .yo on-disk format and its stabilitygateBlocks a milestone exitmilestoneMilestone tracking issuestorageRecord plane, hybrid log, compaction, tiering

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions