Repository navigation
perf(core): carry an applied frame on the next intent's sync - #233
Merged
Merged
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every mode change the seal WAL records costs two syncs: the intent's before the change and the applied frame's after it. A staging, an undo and a rollback each make one change per directory. With the WAL's syncs skipped, an npm-sized staging took about half as long and an undo about two fifths, so with the scans #228 removed gone, these syncs are about half of what a large tree's staging still costs.
An applied frame no longer gets a sync of its own when its directory was reopened for that one change, which is every directory below the root in a staging and every directory an undo or a rollback restores. The frame is written, and the next intent's sync, which comes before the next mode change, carries it.
SealWal::with_deferred_appliedbounds where this happens. Inside it nothing changes on disk but those mode changes and the frames before them, and when it ends it syncs what is left before anything else runs, whether the loop finished or failed. A staging's seals of the source parent and of the root, which it goes on holding, are synced as before.What a power loss leaves. A sync carries every byte written before it, so the WAL a power loss leaves ends at an intent whose mode changed with no applied frame, or past it. That is the state a crash between
fchmodand the applied frame's own sync left before, and recovery settles it the same way, from the directory's mode. A process that dies while the machine stays up loses nothing, since the bytes are already in the file, and the next process's lease syncs the WAL before its replay reads it, as it already did.What is wider. The applied frame and the next intent are written before one sync. A filesystem that keeps the later bytes and loses the earlier ones leaves a frame that is fully present but invalid inside the WAL, and replay refuses that rather than repairing it, as it already refuses a lone frame torn the same way. The store then fails closed instead of recovering by itself. This is the one outcome the change adds. It needs a power loss inside one sync's window on a filesystem that writes a file's pages out of order.
Errors. A failed sync poisons the WAL as before, including the one a scope ends with. A forward failure's disposition depends on whether its transaction exists and can be recovered, not on which error stopped it, so a failed sync still blocks recovery. What changes is which error names it: a failed intent sync now also leaves the applied frame before it uncertain, and a failed settle is reported as a WAL error rather than as the last applied frame's. When a directory fails after the previous directory's applied frame was written, and the settle fails too, the error carries both, since that frame is now uncertain as well. When the directory's failure was its own intent's sync, the WAL is already poisoned and the scope has no sync left to try, so the intent's failure is reported alone.
Measured with #228's harness on the same macOS APFS machine, old and new back to back: a debug build, each step a fresh process under
ulimit -n 128, on trees built to the sizes #162 reported. The machine was busier than when #228 was measured, so both columns run slower than its table, and it is the ratio this change sets.clean, staging itundoclean --purge, staging and purging itA staging and an undo take a quarter less at both sizes and a rollback two fifths less. A purge changes no modes, so only its staging gains. These were measured on the first commit, whose replay also synced once in every process; the second commit leaves that to the lease again. The WAL holds the same bytes as before, 0.74 + 0.82 MiB committed and reserved for the npm-sized tree and 1.51 + 1.65 MiB for the Cargo-sized one, since no frame changes. The change keeps nothing per directory; peak RSS was not compared.
Validation
Merged with main at 0310e9c,
cargo fmt --all --check,cargo clippy --workspace --all-targets --locked -- -D warnings(also forx86_64-unknown-linux-musl),RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked --document-private-itemsandcargo test --workspace --lockedare clean, the suite at 1820 passed / 0 failed / 1 ignored.deferred_applied_syncs: inside a scope, two mutations cost one sync each and the scope's end one more. Cut at the second mutation's last sync, the WAL replays the first as applied and the second as an intent of unknown outcome. Outside a scope the same call syncs both frames, and an empty scope syncs nothing.settle_failure_poisons: the sync a scope ends with fails, and the WAL takes no further frame.lease_syncs_once: taking the lease syncs the WAL once, and a replay with no tail to repair adds no sync.seals_sync_once,restores_sync_once: twenty more directories cost a staging, an undo and a rollback twenty more syncs, where they cost forty.seal_failures_reported: the second directory below the root is refused before its intent and the sync the seal pass ends with fails with EIO; the staging reports both. When the EIO is that directory's intent sync instead, it reports the intent's failure alone.restore_failures_reported: the same for a rollback, and both cases for an undo.lost_applied_frame: a staging stops after its first, its third and its last seal, and the WAL is cut back to its last sync, as a power loss would leave it. After the first and the third, the last directory's outcome is unknown and its mode is changed; after the root's, nothing was waiting. Each time the next open rolls the tree back to its modes.Seventeen mutations, all caught. Syncing every applied frame again in the staging, the rollback or the undo loop fails
seals_sync_onceandlost_applied_frame, orrestores_sync_once. Ending a scope without its sync, or with a settle that does nothing, failsdeferred_applied_syncsandsettle_failure_poisons. Not recording how far a sync reached failsdeferred_applied_syncsandlost_applied_frame. Deferring outside a scope failsdeferred_applied_syncs. Joining a pass's error with its settle's as the first commit did, in either error type's helper or at the rollback's or the undo's call, failsseal_failures_reportedorrestore_failures_reported, as does reporting only the settle's error or a poisoned settle beside the pass's. Dropping the lease's sync, or syncing again in every replay, failslease_syncs_once.Testing this found that a staging killed after some seals and before the root's cannot be rolled back when the tree's directories are group-writable, whether or not an applied frame was lost: recovery requires every ancestor of a sealed directory to be exclusive, and the forward pass has not sealed them yet.
lost_applied_frametherefore keeps the root at 0o755. That is a separate issue.