From 0a8aaa2c249f0aa647ed7e738fbc7a2eb5de7813 Mon Sep 17 00:00:00 2001 From: Patrick Insinger Date: Wed, 29 Sep 2021 16:07:03 -0700 Subject: [PATCH] zenith_utils - add crashsafe_dir Utility for creating directories and directory trees in a crash safe manor. Minimizes calls to fsync for trees. --- Cargo.lock | 1 + zenith_utils/Cargo.toml | 1 + zenith_utils/src/crashsafe_dir.rs | 125 ++++++++++++++++++++++++++++++ zenith_utils/src/lib.rs | 3 + 4 files changed, 130 insertions(+) create mode 100644 zenith_utils/src/crashsafe_dir.rs diff --git a/Cargo.lock b/Cargo.lock index 47493c6efd..b168bf8333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2606,6 +2606,7 @@ dependencies = [ "slog-scope", "slog-stdlog", "slog-term", + "tempfile", "thiserror", "tokio", "webpki", diff --git a/zenith_utils/Cargo.toml b/zenith_utils/Cargo.toml index f315a473af..22c1c9bab6 100644 --- a/zenith_utils/Cargo.toml +++ b/zenith_utils/Cargo.toml @@ -38,3 +38,4 @@ rustls-split = "0.2.1" hex-literal = "0.3" bytes = "1.0" webpki = "0.21" +tempfile = "3.2" diff --git a/zenith_utils/src/crashsafe_dir.rs b/zenith_utils/src/crashsafe_dir.rs new file mode 100644 index 0000000000..fcb4ddcce3 --- /dev/null +++ b/zenith_utils/src/crashsafe_dir.rs @@ -0,0 +1,125 @@ +use std::{ + fs::{self, File}, + io, + path::Path, +}; + +/// Similar to [`std::fs::create_dir`], except we fsync the +/// created directory and its parent. +pub fn create_dir(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + + fs::create_dir(path)?; + File::open(path)?.sync_all()?; + + if let Some(parent) = path.parent() { + File::open(parent)?.sync_all() + } else { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "can't find parent", + )) + } +} + +/// Similar to [`std::fs::create_dir_all`], except we fsync all +/// newly created directories and the pre-existing parent. +pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { + let mut path = path.as_ref(); + + let mut dirs_to_create = Vec::new(); + + // Figure out which directories we need to create. + loop { + match path.metadata() { + Ok(metadata) if metadata.is_dir() => break, + Ok(_) => { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("non-directory found in path: {:?}", path), + )); + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + + dirs_to_create.push(path); + + match path.parent() { + Some(parent) => path = parent, + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "can't find parent", + )) + } + } + } + + // Create directories from parent to child. + for &path in dirs_to_create.iter().rev() { + fs::create_dir(path)?; + } + + // Fsync the created directories from child to parent. + for &path in dirs_to_create.iter() { + File::open(path)?.sync_all()?; + } + + // If we created any new directories, fsync the parent. + if !dirs_to_create.is_empty() { + File::open(path)?.sync_all()?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_create_dir_fsyncd() { + let dir = tempdir().unwrap(); + + let existing_dir_path = dir.path(); + let err = create_dir(existing_dir_path).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); + + let child_dir = existing_dir_path.join("child"); + create_dir(child_dir).unwrap(); + + let nested_child_dir = existing_dir_path.join("child1").join("child2"); + let err = create_dir(nested_child_dir).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } + + #[test] + fn test_create_dir_all_fsyncd() { + let dir = tempdir().unwrap(); + + let existing_dir_path = dir.path(); + create_dir_all(existing_dir_path).unwrap(); + + let child_dir = existing_dir_path.join("child"); + assert!(!child_dir.exists()); + create_dir_all(&child_dir).unwrap(); + assert!(child_dir.exists()); + + let nested_child_dir = existing_dir_path.join("child1").join("child2"); + assert!(!nested_child_dir.exists()); + create_dir_all(&nested_child_dir).unwrap(); + assert!(nested_child_dir.exists()); + + let file_path = existing_dir_path.join("file"); + std::fs::write(&file_path, b"").unwrap(); + + let err = create_dir_all(&file_path).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::AlreadyExists); + + let invalid_dir_path = file_path.join("folder"); + create_dir_all(&invalid_dir_path).unwrap_err(); + } +} diff --git a/zenith_utils/src/lib.rs b/zenith_utils/src/lib.rs index 142c65e6f1..ca26be5df2 100644 --- a/zenith_utils/src/lib.rs +++ b/zenith_utils/src/lib.rs @@ -18,6 +18,9 @@ pub mod pq_proto; // dealing with connstring parsing and handy access to it's parts pub mod connstring; +// helper functions for creating and fsyncing directories/trees +pub mod crashsafe_dir; + // common authentication routines pub mod auth;