From fda0622ea8e2b1781645368d49b98beca50fecba Mon Sep 17 00:00:00 2001 From: Paulo Cabral Sanz Date: Fri, 14 Aug 2026 17:37:58 -0300 Subject: [PATCH 1/3] test: compact must fsync the parent directory artifact::rename_into_place already fsyncs the parent after rename. compact_to_file is the fold's durable publish and currently does not. --- src/snapshot.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/snapshot.rs b/src/snapshot.rs index bd43f8c..d239e4a 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1630,4 +1630,25 @@ mod tests { ); assert_eq!(snap.entries["node.a"].value, b"survives"); } + + /// Compact is the fold's durable publish: inode `sync_all` then rename. + /// Without a parent-directory fsync the new dirent can vanish on power + /// loss — the same hole `artifact::rename_into_place` already closes. + #[test] + fn compact_to_file_fsyncs_parent_dirent() { + let src = include_str!("snapshot.rs"); + let compact = src + .split("fn compact_to_file(") + .nth(1) + .expect("compact_to_file must exist"); + let compact = compact + .split("\n// ---------------------------------------------------------------------------") + .next() + .unwrap(); + assert!( + compact.contains("fsync_dir"), + "compact_to_file must fsync the parent directory after persist, \ + matching artifact::rename_into_place (Pedra F17)" + ); + } } From 3b1fc5609d4fd68a2bf359e05337676ba546a08e Mon Sep 17 00:00:00 2001 From: Paulo Cabral Sanz Date: Fri, 14 Aug 2026 17:37:58 -0300 Subject: [PATCH 2/3] fix: fsync parent directory after compact rename persist() is rename(2). The inode is durable; the new dirent is not until the parent is fsynced. Reuse artifact::fsync_dir. --- src/artifact.rs | 2 +- src/snapshot.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/artifact.rs b/src/artifact.rs index 3df2677..06ec4ac 100644 --- a/src/artifact.rs +++ b/src/artifact.rs @@ -360,7 +360,7 @@ pub(crate) fn hash_payload(root: &Path) -> Result, SnapshotErr Ok(files) } -fn fsync_dir(path: &Path) -> Result<(), SnapshotError> { +pub(crate) fn fsync_dir(path: &Path) -> Result<(), SnapshotError> { File::open(path)?.sync_all()?; Ok(()) } diff --git a/src/snapshot.rs b/src/snapshot.rs index d239e4a..3d5f012 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1119,6 +1119,10 @@ fn compact_to_file( tmp.as_file().sync_all()?; tmp.persist(path).map_err(|e| SnapshotError::Io(e.error))?; + // persist() is rename(2). The inode is durable; the directory entry is + // not until the parent is fsynced. artifact::rename_into_place already + // does this — compact is the fold's only other rename-publish. + crate::artifact::fsync_dir(dir)?; Ok(()) } From 0fb2ec70ca7e1e077014f44bf8eba6bed40d208e Mon Sep 17 00:00:00 2001 From: Paulo Cabral Sanz Date: Fri, 14 Aug 2026 18:07:28 -0300 Subject: [PATCH 3/3] style: rustfmt compact_to_file_fsyncs_parent_dirent --- src/snapshot.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/snapshot.rs b/src/snapshot.rs index 3d5f012..e031d86 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1646,7 +1646,9 @@ mod tests { .nth(1) .expect("compact_to_file must exist"); let compact = compact - .split("\n// ---------------------------------------------------------------------------") + .split( + "\n// ---------------------------------------------------------------------------", + ) .next() .unwrap(); assert!(