Repository navigation
M1: The file #2
Description
The second milestone of rupg, specified in spec/23-milestones.md section 23.4. M1 builds the single .rupg file and everything that makes a write durable: the format, the header slots, the buffer manager, the per-worker log, recovery and the hot store. It adds a first MVCC with snapshot reads and a Rust key-value API over typed tables. M1 has no SQL and no network.
Why
The file comes before SQL. Every layer stands on the file, and the file format is the hardest thing to change after users have files.
The HLC and the shard field are in M1 because a row id written without a shard field can never get one. This is the meaning of "ready for a cluster" in spec/04 §4.11. The single-node code calls the cluster traits from M1, so the cluster at M10 adds implementations and does not change the stack.
M1 has no gate, but its numbers set the floor for G4 at M3 and G5 at M5 (spec/23 §23.4).
The work
Taken from spec/23 §23.4. The reference in brackets names the spec section that owns the detail. Change the list in the spec and copy it again. Do not edit it here.
- The file format: header slots, pages of 16 KiB, extents, the page table, free space and checksums (spec/08 §8.2, spec/08 §8.3, spec/08 §8.4, spec/08 §8.5, spec/08 §8.9)
- Out-of-place page writes and checkpoints (spec/08 §8.6)
- The buffer manager with the memory limit (spec/08 §8.8, spec/04 §4.6)
- The per-worker log rings, group commit and recovery (spec/11 §11.13, spec/11 §11.14, spec/11 §11.15, spec/11 §11.16)
- The hot store, with row ids of 16 bits of shard and 48 bits of local id (spec/10 §10.2.1, spec/10 §10.3)
- The HLC commit timestamp of 48 bits of milliseconds and 16 logical bits (spec/04 §4.7)
- A first MVCC with snapshot reads (spec/11 §11.3)
- The
rupgfacade with a Rust API that opens a file, creates a table with typed columns, and does insert, update, delete, get and range scan in transactions (spec/17 §17.3) -
rupg checkin therupgbinary (spec/21 §21.9.2) - The platform traits with the operating system and the simulation implementations (spec/21 §21.10)
- Publish the crate names on crates.io (spec/22 §22.13)
- Report the commit latency at one writer, the commits per second at one writer per core on
gpc, the size of an empty file, and the recovery time after 1 GiB of log (spec/23 §23.4) - 1 million crash files with no lost commit (spec/21 §21.9.4)
- 24 CPU hours each on
file_open,page_decodeandlog_replaywith no open find (spec/21 §21.7) - loom tests for the buffer latches and the log ring reservation (spec/21 §21.11)
- dm-log-writes and LazyFS runs on ext4 and XFS (spec/21 §21.9.3)
- Tag the release
0.1.0and match the reproducible build (spec/23 §23.17, spec/23 §23.18) - Crates that first ship:
rupg-common,rupg-types,rupg-platform,rupg-file,rupg-kernels,rupg-buffer,rupg-tree,rupg-log,rupg-hot,rupg-table,rupg-txn,rupgandrupg-cli(spec/23 §23.2)
Exit criterion
- No gate of spec/02 §2.10 applies to M1. The report publishes the commit latency at one writer, the commits per second at one writer per core on
gpc, the size of an empty file, and the recovery time after 1 GiB of log. - 1 million crash files run with no lost commit.
file_open,page_decodeandlog_replayeach run 24 CPU hours with no open find.- The close report answers each line of the checklist in spec/23 §23.18 that applies from M1.
Open questions
These questions in spec/24 close at M1.
- Q30 (spec/24 §24.5): the log ring size, 64 MiB per worker or smaller for an embedded library. Q30: What is the size of the log ring? #43
- Q31 (spec/24 §24.5): the checkpoint trigger,
max_wal_sizefor each ring or for the sum over all rings. Q31: Does max_wal_size apply to each ring or to the sum? #44 - Q38 (spec/24 §24.5): the cost of the hash table fallback on 39-bit aarch64 hosts. Q38: What does the hash table fallback cost on 39-bit aarch64 hosts? #51
- Q40 (spec/24 §24.5): two files during
rupg upgrade. Q40: Are two files allowed during an upgrade? #53 - Q69 (spec/24 §24.9): the thresholds of the performance tests, until the noise is measured. Q69: What are the thresholds of the performance tests? #82
- Q70 (spec/24 §24.9): the crate names, published at M1. Q70: When are the crate names published? #83
Harness work
- Benchmarks: M1: Storage and commit floors rupg-bench#2
Depends on
#1.
Spec
- spec/23-milestones.md §23.4 M1: the file
- spec/23-milestones.md §23.16 Why the order is what it is
- spec/04-architecture.md §4.7 Identifiers
- spec/04-architecture.md §4.11 The cluster is beside the stack, not on top of it
- spec/08-the-file.md
- spec/10-rows-and-columns.md §10.2 Row ids and the segment map
- spec/10-rows-and-columns.md §10.3 The hot store
- spec/11-transactions-and-log.md
- spec/17-embedding.md §17.3 The Rust API
- spec/21-testing.md §21.7 Fuzzing
- spec/21-testing.md §21.9 Crash and power cut
- spec/21-testing.md §21.10 Deterministic simulation
- spec/22-crate-layout.md §22.4 The crates
- spec/24-open-questions.md §24.5 The file, storage and compression
- spec/24-open-questions.md §24.9 The project
Activity
tamnd commentedon Oct 7, 2026
Progress on M1: #91 is merged.
rupg-commonnow hasErrorwith itsSqlState, and the identifiers of spec/04 §4.7:Oid,ShardId,RowId,Hlc,XidandLsn. The widths are now fixed, because the file format stores them. A test checks each SQLSTATE constant against theerrcodes.txtof the pin.No line of the checklist is complete yet. The HLC line completes when the log writes the commit timestamp, and the row id line completes with the hot store.
Next: the platform traits with the operating system and the simulation implementations (spec/21 §21.10). Then the file format in
rupg-file.tamnd commentedon Oct 7, 2026
Progress on M1: #92 is merged.
rupg-platformnow has theIo,File,ClockandEntropytraits with their operating system and simulation implementations.SimIois the disk model of spec/21 §21.9.1. A crash test can list the unsynced operations, give each one a fate (lost, written, torn or misdirected) and an order, and build the file that the power cut leaves. The 1 million crash files of this milestone will use it.The platform line of the checklist stays open until the second half:
Tasks,Net,MemoryPool, the CPU feature detection and the clippy rules that keepstd::fs,std::netandstd::threadout of the other crates.tamnd commentedon Oct 7, 2026
Progress on M1: #93 is merged, and the platform line of the checklist is done.
rupg-platformnow has the five traits of spec/21 §21.10:Io,Clock,Entropy,TasksandNet, each with an operating system and a simulation implementation. It also hasMemoryPoolwith the reclaimer and the Nth failure test mode, and the CPU levels of the kernels.clippy.tomlnow keepsstd::fs,std::netandstd::threadout of the other crates.SimTasksruns each task to its end or to ayield_now. The scheduler of the engine must follow this rule, so a task never blocks on a lock that another task holds. The network faults come with the cluster at M10.Next: the file format in
rupg-file. It starts with the XXH64 checksum lifted from rudb, the identity block, the header slots and the page header.tamnd commentedon Oct 7, 2026
Progress on M1: the first part of the file format is merged in #94.
rupg-filenow has the page checksum lifted from rudb, the identity block, the two header slots with the slot choice rule, the owner record and the 64-byte page header.verifyfinds a bad checksum, a misdirected write and a lost write. Each block has a text form that parses back.The checksum is now in
rupg-fileand not inrupg-kernels, because of the layer order. spec/08 §8.3.2 is changed to match.Next: the page table, the extent classes, the free space map with the arenas, and the ring directory. That PR completes the file format line.
tamnd commentedon Oct 7, 2026
Progress on M1: #96 is merged, and the file format line of the checklist is done.
rupg-filenow has the page table with the 48-bit physical page and the 16-bit write tag, the extent classes E0 to E4, and the free space map with a 1024-bit map for each arena of 16 MiB. Each page kind of this crate has a text form that parses back into the same sealed page.Two spec changes came out of this work. Extents are now aligned inside their arena and not to page 0, because the arenas start at page 3. A page table node and a free space map page hold their physical number in the logical page field, so the misdirected write check also covers them.
Next:
rupg-buffer, the buffer manager with the memory limit and thePageAccesstrait.tamnd commentedon Oct 7, 2026
Progress on M1: #97 is merged. It is the first of two PRs for the buffer manager line, so the line stays open.
rupg-filenow has thePageAccesstrait with the optimistic, shared and exclusive modes.rupg-bufferhasBufferPoolwith the vmcache state word, a clock that writes dirty pages before it evicts them, and a fixed table of frames that it reserves from the memory budget.The thread test found a lost update when two threads wrote back the same page at the same time. Each frame now allows one write of its page at a time, and spec/08 §8.8.2 says why.
Next: the virtual memory window of spec/08 §8.8.1 on Linux and macOS, and the reclaimer that gives clean frames back to the budget. That PR closes the buffer manager line.
tamnd commentedon Oct 7, 2026
Progress on M1: #98 is merged, and the buffer manager line is done.
BufferPoolnow uses the window of spec/08 §8.8.1 on Linux and macOS. The bytes and the state word of logical pagenare at indexn, so a hit needs no lookup. The pool starts atshared_buffers, grows from the memory budget 64 pages at a time, and gives clean pages back when another consumer needs the memory. Windows and Miri use the fixed table of #97.The state numbers changed so that a zero word is an evicted unit. The state word region needs no setup at open.
Next: the out-of-place page writes and the checkpoint of spec/08 §8.6, with the store of the file behind the
Storetrait.21 remaining items
tamnd commentedon Oct 7, 2026
The
rupgfacade is in (#120).Database::openopens or makes a.rupgfile and replays the log when the file was not closed cleanly.create_tablemakes a table with typed columns, and aTransactiondoes insert, update, delete, get and range scan by row id. The catalog is one page that each checkpoint writes. The crash test of the facade runs 30 power cuts over 6 seeds with tables that come, checkpoints that the log size starts and a transaction that is open at the cut, and each acknowledged commit is in the file after each cut. The next PR addsrupg check.tamnd commentedon Oct 7, 2026
rupg check FILEis in (#121), withrupg::checkin the facade. It opens the file to read and does not replay the log. It checks the header slots, the page table, the free space map, the catalog page, the tree and each row of each table, that each logical page in use belongs to the catalog or to one table, and each block of the log after the redo position. The crash test of the facade now runs the check after each power cut. M1 has no segments and no indexes, so the check of those comes with M4 and M6. The next PRs are the larger crash run, the fuzz targets and the loom tests.tamnd commentedon Oct 7, 2026
Release 0.0.9 is tagged as
v0.0.9(#122). It adds therupgfacade with the M1 Rust API (#120) andrupg check(#121). No crate is published. The next PR adds the T8 crash harness intests/crash, which builds crash files at each sync point and counts the distinct ones.tamnd commentedon Oct 7, 2026
The T8 crash test is in (#123), in the new crate
tests/crash. A seeded M1 workload runs through the Rust API on the simulated disk, with commits, rollbacks, a transaction that stays open across other commits, checkpoints, and close and open. Before each sync of the file, the test builds crash files from the writes that are not synced: every subset of 8 writes or fewer, and for more writes the prefixes in both orders, the torn prefixes and a seeded sample. A crash file with the same bytes as an earlier one counts once. Each crash file must open with exactly the acknowledged commits, or those and the one commit that was in progress at the cut, andrupg checkmust report no error.The count for the exit item: 1,021,877 distinct crash files from 436 seeds (seeds 0 to 435), 92,744 sync points and 70,110 commits, with no lost commit, no extra commit and no error from the check. The run took 648 seconds on 10 threads of a Mac at commit bcae64d, which is the tree of #123. The command is
cargo run --release -p rupg-crash -- --files 1000000. A run of 28,538 files on Linux also passed.cargo testruns a short workload on two seeds, and the nightly workflow runs 100,000 files from a new first seed each night.Two speed fixes came with it. The simulated disk now shares chunks of 64 KiB between a fork and a sync, and
Options::window_pageslets a test make a small window, because the default reservation took about 10 ms to make and to free. On Linux, Rust zeroes an aligned allocation with its own writes, so the 8 MiB pool ofrupg checktouched 2,048 new pages on each check. The check now uses 64 frames.The harness has one log ring, so the case of section 21.9.2 where one ring has its last write and another does not comes with more rings. Misdirected writes are not in the plans yet. The next PRs are the fuzz targets and the loom tests.
tamnd commentedon Oct 7, 2026
The three M1 fuzz targets are in (#124), in the separate
fuzz/workspace:file_open,page_decodeandlog_replay. All start from one base file on the simulated disk with 32 commits before a checkpoint and 16 after it, and no close, so each open replays the log.file_openchanges any byte and can cut the file, and an open must give the rows after one of the commits.page_decodechanges a hot page and seals it again, so the decoders see the bad bytes.log_replaychanges the log after the redo position, and an open must give the rows after a commit at or after the checkpoint, orXX001. Each target also runsrupg check.The first runs found two bugs, and #124 fixes both. A file of zero bytes with
createoff opened as a new empty database, so a file that a cut made empty lost its data with no error. It now gives58P01. A leftmost child number in an inner page that pointed to the root made the tree descent loop. Each descent now checks that the level goes down at each step and givesXX001when it does not.After the fixes, each target ran for 5 minutes with no find. The run for the exit item started at 19:56 on a Mac from commit 189cd63, which is the tree of #124: 2 workers for each target for 12 hours, so 24 CPU hours for each target, with
-timeout=1 -rss_limit_mb=2048. The nightly workflow also runs each target for 20 minutes. The next PR is the loom tests for the buffer latches and the log ring reservation.tamnd commentedon Oct 7, 2026
The loom tests are in (#125), and a new CI job
loomruns them on each change. The latch tests includelatch.rsa second time with the atomics of loom, because the pool keeps its state words in the memory of the window, where a loom atomic cannot live. They check that a writer excludes the other latches, that shared latches count their readers, and that an optimistic read that passesstillsaw the page of one version. The ring test runs 2 or 3 threads that each place a block and flush to its end, and each thread must then find every block before the durable position in the store.Each test fails when the code has a bug on purpose: no fence in
stillortry_exclusive, a shared latch on an exclusive frame, or a flush that does not mark itself as running. The last one first passed. Loom reorders only the steps that use one loom object, and the store is not one, so loom never ran another thread between the unlock and the write. The ring now touches anIoMarkbefore each write and sync. It is an atomic under loom and an empty type in other builds. With 3 threads the full search took 527 s, so that case bounds the preemptions at 2.The 24 CPU hour fuzz run is at about 2 hours with no find. The next PR is the runs with dm-log-writes and LazyFS on ext4 and XFS.
tamnd commentedon Oct 7, 2026
Release 0.0.10 is tagged as
v0.0.10(#126). It adds the T8 crash test (#123), the M1 fuzz targets (#124) and the loom tests of the latch and the log ring (#125). It also fixes the two bugs that the fuzz targets found: a file of zero bytes withcreateoff now gives58P01, and a tree descent givesXX001when a child number points up the tree. No crate is published. The next PR is the runs with dm-log-writes and LazyFS on ext4 and XFS.tamnd commentedon Oct 7, 2026
The crash tests on real kernels are merged in #127 (spec/21 section 21.9.3).
tests/realdisk/logwrites.pyruns the M1 workload on ext4 or XFS over dm-log-writes with a mark after each acknowledged change, then replays the log and checks the file at a sample of flush, FUA and mark entries and of the entries between them.tests/realdisk/kill.pykills the workload withSIGKILLat random points, and with--lazyfsit also drops the data that was not synced. With seed 7 and 200 steps, ext4 passed 300 check points of 1746 log entries and XFS passed 300 check points of 839 log entries. 100SIGKILLrounds and 100 LazyFS rounds also passed. A build with nofdatasyncfailed the dm-log-writes and LazyFS tests. The nightly workflow runs all of them in the newrealdiskjob. At M1 theSIGKILLtest uses the key-value workload, because there is no TPC-C yet.tamnd commentedon Oct 7, 2026
Three PRs for the M1 numbers are merged.
rupg checkreportslog bytes, the bytes of the log blocks that an open replays. The M1 benchmark uses it to show the size of the log before it times a recovery.Recovery::for_each_safegives the safe blocks to replay and torupg check.With
rupg-bench m1 --steps recovery --log-mib 512on my laptop (Apple M-series, file in the page cache), the open of 0.5 GiB of log takes 0.48 s, about 1 GiB/s on one thread. It took 0.89 s at #129. The budget of spec/11 is 1 GB/s with 16 cores. Spec/24 Q73 stays open for inserts, deletes, updates that change a length, and a cold disk on Linux.The fuzz run stopped. The disk of the laptop became full at about 01:00, and libFuzzer could not write its files in fork mode. The file system did not have space for 25 minutes of the second part of the run. The one timeout input of
file_openfrom that time runs in 60 ms, so it is not a find. Because #129 and #130 change the code that the targets run, I started the 24 CPU hours again at 238dd51 with the old corpora: each target runs with-fork=2for 12 hours.file_openis faster after #130 (275 inputs per second on the corpus, 129 before). The 7.45 CPU hours of the first part at 189cd63 had no find, but I do not count them.tamnd commentedon Oct 7, 2026
The M1 numbers are in tamnd/rupg-bench#22, from
rupg-bench m1with the facade at 238dd51. Both machines are shared and have no local NVMe, so these are smoke runs and not baselines.The commit numbers follow the
fdatasynctime of the shared disks.gpcis not available, so the commits per second ongpcare not measured. I ticked the line because the driver and the four numbers exist, and the run ongpcis one command when the machine is ready:rupg-bench m1 --dir run --machine gpc --report reports.The empty file is over the budget of spec/17 §17.10. Its length is 3 header pages, one arena of 16 MiB for the pages and the 4 extents of 16 MiB of the log ring. The file is sparse, and 300 KiB is allocated. Spec/17 §17.5 says that the library releases each ring at a clean close and truncates the file after its last used extent, and that is the work of M9. Even then, one arena of 16 MiB stays, so Q30 (#43) must also give the library a smaller extent. I added this to #43.
Recovery: on server1 the open of 1 GiB of log took 15.33 s at aed939c and takes 9.40 s at 238dd51, after #129 and #130.
tamnd commentedon Oct 8, 2026
The fuzz run at 238dd51 is complete. Each target ran with
-fork=2for 12 hours, so each one has 24 CPU hours. No target found an input that crashes, runs out of memory or times out, and the artifact directories are empty. I ticked the line.file_openpage_decodelog_replayThe coverage did not change much in the last hours, so the corpora are close to full for these targets at this commit. The next step for M1 is the release
0.1.0.tamnd commentedon Oct 8, 2026
PR #168 is merged. It adds
cargo xtask lifts, which the close checklist of spec/23 section 23.18 needs. The only lifted file isrupg-file/src/checksum.rs. rudb has 33 commits to its source file after the lift, but none of them changes the checksum functions or their test vectors, so the file records the review. With a full rudb clone the task reports no change that is not reviewed. The next step is the instruction count runner of spec/21 section 21.14, which is also a line of the close checklist. Then comes the release0.1.0.tamnd commentedon Oct 8, 2026
The instruction count runner of spec/21 section 21.14 is in PR #169.
cargo xtask benchruns six cases (insert, get, scan, update, delete and commit) underperf stat -e instructions:uand applies the budgets of 3 percent for one case and 1 percent for the total. The first counts are inxtask/icount.txt, from an AMD EPYC virtual machine with rustc 1.98.0. A second run changed each case by 0.01 percent or less. The total is 2,715,287,883 instructions.tamnd commentedon Oct 8, 2026
PR #170 repairs the
documentationjob of CI. Three doc comments linked to a private item or to a name that is both a function and a macro, socargo docwith-D warningsfailed on main. The other CI jobs passed. From now on the check on the test servers runscargo doctoo, so a broken link does not get to main again.tamnd commentedon Oct 8, 2026
M1 close report
M1 is done except for the publication of the crate names on crates.io, which waits for a decision of the project owner (Q70, #83). Release 0.1.0 is tagged as
v0.1.0from #171 at 573a5a4. No crate is published.The checklist of spec/23 section 23.18
file_open,page_decodeandlog_replayran 24.05 CPU hours each at 238dd51 with no crash, no timeout and no out of memory. See #2 (comment).rupg check. See #2 (comment).kind/bug, 0 withkind/wrong-answerand 0 withkind/security.cargo xtask bench. M1 has no SQL, so the six cases use the Rust API: insert, get, scan, update, delete and commit. The counts are inxtask/icount.txt.cargo xtask liftsreports no rudb change that is not ported or declinedCONTRIBUTING.mdhas the new tasks. #172 records the M1 misses in document 24: Q30, Q31, Q38 and Q40 did not close at M1 and each entry says why, Q69 has the noise that M1 measured, Q70 waits for the owner, and the new Q74 is the size of an empty file. Q73 already had the replay speed.0.M.0is tagged, and the reproducible build matchedv0.1.0at 573a5a4. Two release builds ofrupgin two directories on server1, with rustc 1.98.0, gave the same SHA-256:f90c3eaebabca3c698fdf774d957e19cf8cdc4cb991436aac853f5aafaf89d21.Lines against the budgets of spec/22 section 22.4
Counted at 573a5a4 with the method of spec/22 section 22.14 (
wc -lof the.rsfiles undersrc), less the generated tables of section 22.8.rupg-commonrupg-typesrupg-platformrupg-filerupg-kernelsrupg-bufferrupg-treerupg-logrupg-hotrupg-tablerupg-txnrupgrupg-cliThe M2 crates that have code are also under budget. The largest use is
rupg-wireat 67 percent (5,365 of 8,000) andrupg-serverat 46 percent (3,709 of 8,000).The exit criterion
gpcare not measured, because the machine is not available. The empty file is over its budget and is now Q74.Still open