mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Make snapshot and backup publication durable across power loss
Neither the pre-activation backup nor the pre-migration snapshot fsynced the staged file contents or the parent directory around the publishing rename. A crash after the import committed could lose the retained '.pre-blob-activation.bak' (whose directory entry was never made durable), and the next activation would then write a new backup that already contains the imported blobs, silently breaking the documented pre-activation rollback boundary; a torn staging file could likewise wedge later boots in backup validation. write_snapshot_to_staging now syncs the staged file before handing it to the caller, and both publishers sync the destination's parent directory after their rename (fabro-db on a blocking task, activation inside its existing blocking publication task). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e067de9382
commit
5629dcd7d0
2 changed files with 59 additions and 1 deletions
|
|
@ -292,7 +292,13 @@ async fn create_backup(
|
|||
let already_exists = spawn_blocking(move || {
|
||||
let staging = tempfile::TempPath::from_path(publish_staging);
|
||||
match staging.persist_noclobber(&publish_backup) {
|
||||
Ok(()) => Ok(false),
|
||||
Ok(()) => {
|
||||
// Make the rename's directory entry durable: the retained
|
||||
// backup is the documented rollback artifact, so it must not
|
||||
// vanish in a crash after the import has already committed.
|
||||
fabro_db::sync_parent_directory(&publish_backup)?;
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => Ok(true),
|
||||
Err(error) => Err(error.error),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,19 @@ impl Database {
|
|||
snapshot_path.display()
|
||||
)
|
||||
})?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let published_path = snapshot_path.clone();
|
||||
spawn_blocking(move || sync_parent_directory(&published_path))
|
||||
.await
|
||||
.context("joining the snapshot directory sync task")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"syncing the directory of pre-migration snapshot {}",
|
||||
snapshot_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
info!(
|
||||
database = %database_path.display(),
|
||||
|
|
@ -228,6 +241,12 @@ pub enum SnapshotStagingError {
|
|||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("flushing snapshot staging file {path} to disk")]
|
||||
Sync {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// Writes a consistent single-file copy of the live pool to `staging_path`.
|
||||
|
|
@ -266,6 +285,39 @@ pub async fn write_snapshot_to_staging(
|
|||
path: staging_path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
// The staging file must be durable before the caller renames it into a
|
||||
// path that later recovery logic treats as a complete snapshot.
|
||||
let sync_result = match fs::File::open(staging_path).await {
|
||||
Ok(file) => file.sync_all().await,
|
||||
Err(source) => Err(source),
|
||||
};
|
||||
sync_result.map_err(|source| SnapshotStagingError::Sync {
|
||||
path: staging_path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flushes the directory entry metadata for `path`'s parent so a rename into
|
||||
/// that directory survives power loss. No-op off Unix, where a directory
|
||||
/// cannot be opened for syncing.
|
||||
#[cfg(unix)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "directory fds have no async open; callers run this on a blocking thread"
|
||||
)]
|
||||
pub fn sync_parent_directory(path: &Path) -> std::io::Result<()> {
|
||||
let Some(parent) = path.parent() else {
|
||||
return Ok(());
|
||||
};
|
||||
std::fs::File::open(parent)?.sync_all()
|
||||
}
|
||||
|
||||
/// Flushes the directory entry metadata for `path`'s parent so a rename into
|
||||
/// that directory survives power loss. No-op off Unix, where a directory
|
||||
/// cannot be opened for syncing.
|
||||
#[cfg(not(unix))]
|
||||
pub fn sync_parent_directory(_path: &Path) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue