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 already_exists = spawn_blocking(move || {
|
||||||
let staging = tempfile::TempPath::from_path(publish_staging);
|
let staging = tempfile::TempPath::from_path(publish_staging);
|
||||||
match staging.persist_noclobber(&publish_backup) {
|
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) if error.error.kind() == std::io::ErrorKind::AlreadyExists => Ok(true),
|
||||||
Err(error) => Err(error.error),
|
Err(error) => Err(error.error),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,19 @@ impl Database {
|
||||||
snapshot_path.display()
|
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!(
|
info!(
|
||||||
database = %database_path.display(),
|
database = %database_path.display(),
|
||||||
|
|
@ -228,6 +241,12 @@ pub enum SnapshotStagingError {
|
||||||
#[source]
|
#[source]
|
||||||
source: std::io::Error,
|
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`.
|
/// 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(),
|
path: staging_path.to_path_buf(),
|
||||||
source,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue