{{ message }}
feat(storage): add inode-stable ASTVOL volume-root publication - #1801
Draft
joshuajbouw wants to merge 6 commits into
Draft
feat(storage): add inode-stable ASTVOL volume-root publication#1801joshuajbouw wants to merge 6 commits into
joshuajbouw wants to merge 6 commits into
Conversation
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds crash-oriented, inode-stable ASTVOL1 volume-root publication and same-inode reclaim support.
Changes:
- Adds dual-copy checksummed root recovery.
- Implements same-inode replacement, relocation, and truncation.
- Adds format documentation, changelog, and crash-stage tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
Suppressed comments (5)
crates/astrid-storage/src/volume/hosted/mod.rs:209
- Root updates always start with copy 0, even when recovery left copy 0 as the only valid authority (for example, a crash after the first root write during generation-0 conversion). Rewriting that sole copy first reintroduces a window where a torn write leaves neither copy recoverable. Write the slot opposite
state.root_slotfirst, sync it, then update the selected slot.
if state.generation > 0 {
recover::write_root_pointer(
&mut state.file,
state.generation,
state.root_base,
state.durable_len,
false,
crates/astrid-storage/src/volume/hosted/reclaim.rs:334
- When the source is generation 0,
finish_imageputs the staged replacement's fully valid footer at physical EOF before any root exists.recover_containerthen takes the generation-0 EOF-footer fallback and admits this replacement at theReplacementDurablecrash cut, so the old generation is not kept current until the root flip as required. The staged image needs an encoding/location that cannot be selected by generation-0 EOF recovery before publication.
let replacement_end = finish_image(&mut replacement)?;
run_stage(&volume.path, ReclaimStage::ReplacementDurable)?;
crates/astrid-storage/src/volume/hosted/reclaim.rs:330
- The first replacement starts at the current EOF, which can be below
ROOT_BYTESfor a small generation-0 container (an empty volume is only 8 bytes). Publishing the fixed slots at bytes 8–135 then overwrites the just-synced replacement commit/image before pointing at it, making the intermediate root unrecoverable. Reserve the complete fixed-root area before choosing the replacement start and handle inputs too small to relocate safely.
let mut replacement = build_image(
&mut state,
physical_len,
replacement_generation,
super::ROOT_BYTES as u64,
crates/astrid-storage/src/volume/hosted/reclaim.rs:368
- This bound check happens only after
build_imageandfinish_imagehave written the final image. Becausecandidate_baseis also where the currently selected replacement begins, an image that does not fit has already overwritten that selected generation before this error is returned; an already-compact or empty volume can therefore be left corrupt. Preflight the complete encoded size or enforce the bound before every record/footer write so crossing it cannot mutate the selected image.
let final_end = finish_image(&mut finalized)?;
if final_end > candidate_base {
return Err(io::Error::other("volume reclaim did not shrink the image"));
crates/astrid-storage/src/volume/hosted/reclaim.rs:340
- This publishes
ROOT_BYTESas the replacement's canonical base even though the replacement was built atreplacement_base. Besides writing incorrect on-media metadata, repeat reclaims misclassify this first flip asFinalRootPublishedbecause its generation is greater than 1. Publish the actual replacement base here.
replacement_generation,
super::ROOT_BYTES as u64,
replacement_end
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+142
to
+144
| if state.physical_tail == PhysicalTail::TruncateToValid { | ||
| state.file.set_len(state.valid_len)?; | ||
| } |
Comment on lines
+274
to
+280
| let first_slot = state.root_slot == RootSlot::Second; | ||
| super::recover::write_root_pointer( | ||
| &mut state.file, | ||
| generation, | ||
| root_base, | ||
| footer_offset, | ||
| first_slot, |
Comment on lines
+398
to
+407
| #[test] | ||
| fn receipt_region_survives_all_same_inode_reclaim_stages() { | ||
| let temporary = tempfile::tempdir().unwrap(); | ||
| let path = temporary.path().join("astrid.volume"); | ||
| let volume = seeded_volume(&path); | ||
| let receipt = VolumeRegion::new("system/gc-outbox/receipt.ready").unwrap(); | ||
| volume.create_region(&receipt, true).unwrap(); | ||
| volume.write_region_at(&receipt, 0, b"proof").unwrap(); | ||
| volume.sync().unwrap(); | ||
| volume.reclaim_same_inode().unwrap(); |
Comment on lines
+427
to
+433
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
3 tasks
Publish the opposite root slot before the selected copy so an interrupted selected-slot write cannot invalidate the last recoverable generation. Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
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.

Linked Issue
Closes #1797. Tracking #1780.
Summary
Adds inode-stable ASTVOL volume-root publication for hosted volume recovery.
The current head also publishes the sibling root slot before the selected copy so a torn selected-slot rewrite cannot destroy the last recoverable generation. This remains a reviewed candidate, not a release gate. It does not activate User storage, complete every reclaim path, or authorize a release by itself.
Changes
Verification
068fbde80842b832f2edb6c859593d1c4c02e7eb.astrid-storage782 passed, 7 ignored; root tests 17/17.AstridVolume::reclaimstill uses the generic namespace swap; same-inode reclaim remains a test/setup path.AI / Tool Assistance
Assisted-by: Codex:GLM-5.3 Flash
AI assistance was used for implementation and tests. The maintainer reviewed the complete diff and owns the landing decision.
Checklist