Repository navigation
storage: toyos-blockdev, with fat32, Served and every consumer async over it - #1025
Conversation
Review of #1025, round 1 (head fe3ecb5, base 18a3873)ArchitectureThe layering matches design §2.4 and B1's row. The crash model meets §3.4 for B1:
Accepted note 4 (finality) is consistent with the client's contract. Once Size, approximately, from numstat:
These amounts are proportionate to the stage. FindingsBLOCKER 1: the cache's concurrency claim is false under two flushes
The header says "Several tasks may use it at once", and the PR presents the version and overtake checks as mechanism for C2. Two concurrent flushes break it. That happens with a sync beside a write over
B1's callers are one task at a time, so none of this happens today. The module still claims it, though, and C2 is told it can rely on it. Fix, one of:
BLOCKER 2: update's held volume now reads the partition for every block it already holds
fat32 now does every partial write as read-modify-write through this device (
Each The header's premise is also false now. It says holding avoids "a read and a write of one block per 512 bytes"; under the new read path it avoids only the write. Fix: read from the disk only the blocks that are not held, or skip the disk entirely when the whole range is held. One host test covers it: a counting disk under BLOCKER 3: the design's
|
…over it Everything below the file protocol becomes asynchronous through one crate, so the bcachefs engine and the node core can have several requests on a device at once, and FAT is awaited directly rather than blocked on. toyos-blockdev (lib/blockdev, no_std, no SDK) holds: - BlockDev: blocks(), read(lba, &mut [Block]), write(lba, &[Block]) and flush(), all on &self. A transfer is whole 4 KiB blocks by its type. - A single-threaded executor: spawn, run_until_stalled, and block_on, which calls a caller-supplied wait when nothing can go on (a device that answers only when its owner reads its session), and block_on_ready for devices that answer as they are asked. - Bytes, a device over memory, for the build and the host tests. - Behind the `model` feature, the crash model: completed writes held in a volatile cache until a flush; a crash keeps any subset of unflushed writes' atomic units, the unit being the device's and not the format's. Small state sets are enumerated, large ones sampled from a recorded seed. Its own control: a record and a marker with no flush between them is found torn, and with a flush is not. toyos-fat32 runs over BlockDev and every I/O function is async. Its private bridge (src/device.rs) turns a byte range into one whole-block request, reading a part block before writing it. BlockAccess and IoError are gone. diskserver's Served is an async BlockDev over a blockring session. Answers reach the waiting task at progress(), which the owner calls (or wait(), as block_on's wait). A request on the wire when the session ends is asked again after the reconnect, as writes already were; reads are now too. A failed reconnect makes the disk gone for good: requests queued for the next session would otherwise go out under ones asked since. Session's synchronous run/read/write/flush and its answer stash are deleted. fileserver's Cache is an async BlockDev over its disk, holding no borrow across a disk request. A block written while a flush carries an older copy stays dirty (per-slot versions), and a fetch that a write overtook is not kept. The FAT and DATA volumes stay synchronous at the Volume boundary and drive each request with Cache::block_on, waiting on the disk; bcachefs's synchronous BlockIO does the same. A fileserver Disk trait names the wait. update's install is async and generic over BlockDev, driven by block_on over the three Served disks. Its held volume (Cached) is a holding BlockDev whose flush writes the held runs and then flushes the partition, so the volume's sync makes it durable. The supervisor reads the slot table through Served the same way. lib/update's slots::read and NotRead, which took a synchronous fill, are replaced at both callers by a read and slots::current. The build writes and reads its FAT volumes through Bytes and block_on_ready; imgstat likewise. toyos-disk (lib/disk) is deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…y what it does not hold fileserver's Cache said several tasks may use it at once, and two flushes broke that. A second flush could send a block's newer copy while the first still had its older copy on the wire. The block service's client forbids two requests for one block in flight when either is a write, because requests in flight together are unordered. A snapshotted block could also be cleaned and evicted under the second flush, which then panicked. Now one flush runs at a time: a second waits for the first and takes its own dirty set. A flush also writes no block that a fetch is reading. A fetch reads only blocks the cache does not hold, and a flush's blocks stay held until it has written them, so a fetch never runs under a flush's write. With both rules a fetch can no longer land a stale copy, so the `overtaken` mark is gone. Two tests over a disk that counts overlapping requests in flight hold this; each is red without its rule. update's held volume read the partition for every block, held or not. fat32 reads a block before it writes part of one, so each FAT entry and cluster write became a round trip to the block service: 5654 partition reads for a 256 KiB file, 5644 of them for held blocks. A held block is now served from the hold, and only runs of the others are read: 10 reads. A host test fails on any partition read of a held block. Another holds the install's flush order: each partition is flushed after its last write, both before the table's one write, and the table's flush comes last. issues/the-storage-programs-are-never-linted.md records that no clippy shape lints fileserver or diskserver, so await_holding_refcell_ref never runs where most of the RefCell-and-await code is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
|
Round 1 response. The head is f863887, rebased onto 777c8c5; the only conflict was BLOCKER 1, fixed with option (a).
BLOCKER 2, fixed.
BLOCKER 3, recorded in
SUGGESTION 1, taken. SUGGESTION 2, taken. I fixed Reruns at f863887: On the queued T14 row (fe3ecb5): in B1, every |
Review of #1025, round 2 (head f863887, base 777c8c5)Scope: round 1's three blockers, plus the new code in f863887 (+364/−61: about +190 production across Architecture. Option (a) is the right fix. It uses one Round 1 blockersB1 (two concurrent flushes): resolved. I traced the invariants in
The two B2 (held volume reads held blocks): resolved. B3 (lint gap unrecorded): resolved.
S1 and S2 were taken. The flush-order test asserts the right order, and its control reds it. T14 reuseThe argument for reusing fe3ecb5's
Inspection settles this, and the QEMU rows at f863887 ( FindingsSUGGESTION 1
SUGGESTION 2
Evidence
These cover every new claim. For the orchestrator: LAND |
Problem
Block I/O below the file protocol was synchronous at every layer:
toyos_disk::Disk, diskserver'sServed, andtoyos-fat32's byte-addressedBlockAccess. The bcachefs engine and the node core need several requests in flight on one device, and FAT needs to be awaited rather than blocked on. This is stage B1 of the storage design: one crate for asynchronous block I/O, with every existing user moved onto it.Resulting behaviour
toyos-blockdev(lib/blockdev,no_std, no SDK):BlockDevhasblocks(),read(lba, &mut [Block]),write(lba, &[Block])andflush(), all on&self. Because a transfer is[Block]([u8; 4096]), a whole-block transfer is guaranteed by the type.spawn,run_until_stalledandblock_on. When no task can go on,block_oncalls a wait the caller supplies;block_on_readyis for devices that answer as they are asked.Bytesis a device over memory.modelfeature (details below).model): a completed write stays in a volatile cache until a flush covers it. At a crash, the medium holds every flushed write plus any subset of the unflushed writes' atomic units. The unit comes from the device (a 512-byte LBA by default), never from the format. Small state sets are enumerated; large ones are sampled from a recorded seed, and a red run names that seed.toyos-fat32runs overBlockDev, and every function that touches I/O isasync. A private bridge (src/device.rs) turns a byte range into one whole-block request, reading any partial block before writing it.BlockAccessandIoErrorare gone.Servedis an asyncBlockDevover a blockring session:progress(), or atwait()when it isblock_on's wait. A session end is found there and reconnected.Goneat once.MAX_RECONNECTSends answersGone, as before.Session::run/read/write/flushand its answer stash are deleted.Cacheis an asyncBlockDevover its disk. Several tasks may use it at once, and it holds no borrow across a disk request. The disk never has two requests for one block in flight when either is a write, which the block service's client forbids:Volumeboundary until C2. They drive each request withCache::block_on, which waits on the disk; bcachefs's synchronousBlockIOdoes the same.Disk: BlockDevtrait names that wait. OnlyServedwaits;Ramand the test devices never do.update's install isasyncand generic overBlockDev. It runs underblock_onover its threeServeddisks; it asks one request at a time, so the wait is the pending disk's.Cached) is a holdingBlockDev. A held block is read from the hold and never from the partition, which matters because fat32 reads a block before it writes part of one. Itsflushnow writes the held runs and then flushes the partition, as the trait requires, so the volume'ssyncmakes it durable and the install's separate boot flush is gone.Servedwithblock_on.lib/update'sslots::readandNotReadtook a synchronous fill and had only these two callers; both now read the two copies and callslots::current.image.rs,fatformat.rs) andimgstatread and write FAT throughBytesandblock_on_ready.toyos-disk(lib/disk) is deleted.Decisions
The orchestrator accepted these four:
tests/metalandlib/fat32/checkare nottoyos-fat32consumers: they depend only on the checker, a pure byte judge. They are unchanged, and so the checker is untouched. The supervisor's slot read does useServed, so it moves here.&[Block]/&mut [Block]. An arena-backed buffer is decided when W2 or C2 needs one.AdapterCache) no longer exists. Every test's claim is unchanged:active_fatcovers only the blocks FAT 0 holds alone.Servedbehaviour changes above.Also in this PR:
toyos-fat32'ssrc/device.rs, where that bridge now lives. One test was added for writes across part blocks.toyos-blockdev'smodelfeature joinsNOT_A_CONTROLinbuild/build.rs, because it is a capability rather than a negative control. The--all-targetsclippy shape compiles the model undercfg(test), so no separate clippy shape is needed.await_holding_refcell_refneeds nobuild/clippy.rschange, but clippy only enforces it on host crates.Cache::flushis refused by plaincargo clippy -- -D warnings(control below). The gate's clippy therefore coverstoyos-blockdevandtoyos-fat32.main. update's install is linted, throughupdate-copies. The gap is recorded inissues/the-storage-programs-are-never-linted.md, with its evidence, an owner (the storage track, before C2) and an exit.Checks
Review round 1 (head f863887, rebased onto 777c8c5)
Evidence:
r2/.cargo run -- --ci host: exit 0, 101 of 101 steps green (log).cargo run -- --build-only: exit 0 (log).the_log_lands_on_the_boot_stickandnvme_disk_boots: 2 of 2 pass (log). These were rerun because the cache changed.Cached::flushwithout the partition's flush.Round 0 (head fe3ecb5)
cargo run -- --ci hostat c8ef90a (log): 92 of 93 steps green. The one red wasbuild::tests::every_model_control_is_run, which counted the newmodelfeature as an undeclared control. After theNOT_A_CONTROLentry,cargo test -p toyos-build --libpassed (367 tests, log).toyos-blockdev: 11 tests.toyos-fat32: lib 21,host_read10,host_write21,hostile33,refused_writes20,replace_rename2.toyos-fat32-check: untouched.toyos-update: 52.update-copies: 3.Servedtests. fileserver: 70.cargo run -- --ci hostat the final commit fe3ecb5: exit 0, all 93 steps green (log).cargo run -- --build-onlyexit 0 at 8a8e15c (log). Its tree equals fe3ecb5's. The first attempt (log) caught a temporary stdin lock inupdate'smain.rs.cargo test --test toyos-build -- the_log_lands_on_the_boot_stick nvme_disk_boots, 2 of 2 pass at 8a8e15c (log).nvme_disk_bootsserves LOG, BOOT and DATA off the NVMe driver's sessions.the_log_lands_on_the_boot_stickwrites the boot's log through fileserver's FAT overServedand usbd. Its 1.47 MB is read back off the image on the host.Served::waitover a real session, which no host test reaches.a_block_written_while_a_flush_carries_its_older_copy_stays_dirtyonly.overtakenignored: red ona_fetch_a_write_overtook_is_not_keptonly.RefCellborrow held acrossdisk.flush().await: clippy refuses it (await_holding_refcell_ref).Served, a refused request not asked again: red on the three re-ask/gone tests.Served, a failed reconnect not made final: red ona_refused_reconnect_makes_the_disk_gone_for_every_requestonly.Metal:
usbd_serves_the_boot_stickon the T14 is prepared with--metal-readback, and the run is requested from the orchestrator. It rides the sharedtestcasesboot, and its judge reads the log fileserver wrote overServedoff the stick. QEMU already establishes order and contents on this path; the metal row confirms the same path over the T14's own xHC and stick.Risks
update's install (nor the supervisor's slot grant). It is host-tested overBytes. The only guest-only part ismain.rs's choice of which disk to wait on.Volumeboundary until C2. Throughput and latency are as before: one request at a time.Evidence: ToyOSOrg/evidence@0e64fb2,
b1/.🤖 Generated with Claude Code
https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q