use std::{io, path::Path};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LocalPublishBoundary {
BeforeTemporaryWrite,
DuringTemporaryWrite,
BeforeTemporarySync,
AfterTemporaryDurable,
AfterInstall,
BeforeParentSync,
AfterParentDurable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LocalPublishFault {
Interrupted,
OutOfSpace,
InputOutput,
PartialWrite,
SyncFailure,
}
#[cfg(not(any(test, feature = "test-fault-injection")))]
#[allow(clippy::unnecessary_wraps)]
pub(crate) const fn local_publish_failpoint(
_path: &Path,
_boundary: LocalPublishBoundary,
) -> io::Result<()> {
Ok(())
}
#[cfg(not(any(test, feature = "test-fault-injection")))]
pub(crate) const fn local_publish_partial_write_len(
_path: &Path,
_requested: usize,
) -> Option<usize> {
None
}
#[cfg(any(test, feature = "test-fault-injection"))]
pub(crate) fn local_publish_failpoint(
path: &Path,
boundary: LocalPublishBoundary,
) -> io::Result<()> {
enabled::hit(path, boundary)
}
#[cfg(any(test, feature = "test-fault-injection"))]
pub(crate) fn local_publish_partial_write_len(path: &Path, requested: usize) -> Option<usize> {
enabled::partial_write_len(path, requested)
}
#[cfg(any(test, feature = "test-fault-injection"))]
pub use enabled::{LocalPublishFailpointGuard, arm, arm_fault};
#[cfg(any(test, feature = "test-fault-injection"))]
mod enabled {
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
};
use super::{LocalPublishBoundary, LocalPublishFault, io};
#[derive(Debug, Clone, Copy)]
struct ArmedFault {
boundary: LocalPublishBoundary,
fault: LocalPublishFault,
registration_id: u64,
}
static NEXT_REGISTRATION_ID: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
static FAILPOINTS: LazyLock<Mutex<HashMap<PathBuf, ArmedFault>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub struct LocalPublishFailpointGuard {
path: PathBuf,
registration_id: u64,
}
impl Drop for LocalPublishFailpointGuard {
fn drop(&mut self) {
let mut failpoints = FAILPOINTS.lock().unwrap_or_else(|error| error.into_inner());
if failpoints
.get(&self.path)
.is_some_and(|armed| armed.registration_id == self.registration_id)
{
failpoints.remove(&self.path);
}
}
}
#[must_use]
pub fn arm(path: PathBuf, boundary: LocalPublishBoundary) -> LocalPublishFailpointGuard {
arm_fault(path, boundary, LocalPublishFault::Interrupted)
}
#[must_use]
pub fn arm_fault(
path: PathBuf,
boundary: LocalPublishBoundary,
fault: LocalPublishFault,
) -> LocalPublishFailpointGuard {
let registration_id =
NEXT_REGISTRATION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
FAILPOINTS
.lock()
.unwrap_or_else(|error| error.into_inner())
.insert(
path.clone(),
ArmedFault {
boundary,
fault,
registration_id,
},
);
LocalPublishFailpointGuard {
path,
registration_id,
}
}
pub(super) fn hit(path: &Path, boundary: LocalPublishBoundary) -> io::Result<()> {
let fault = FAILPOINTS
.lock()
.unwrap_or_else(|error| error.into_inner())
.get(path)
.filter(|armed| armed.boundary == boundary)
.map(|armed| armed.fault);
match fault {
None => Ok(()),
Some(LocalPublishFault::Interrupted) => Err(io::Error::new(
io::ErrorKind::Interrupted,
format!("injected local publication interruption at {boundary:?}"),
)),
Some(LocalPublishFault::OutOfSpace) => Err(io::Error::from_raw_os_error(libc::ENOSPC)),
Some(LocalPublishFault::InputOutput | LocalPublishFault::SyncFailure) => {
Err(io::Error::from_raw_os_error(libc::EIO))
}
Some(LocalPublishFault::PartialWrite) => Err(io::Error::new(
io::ErrorKind::WriteZero,
"injected partial local publication write",
)),
}
}
pub(super) fn partial_write_len(path: &Path, requested: usize) -> Option<usize> {
FAILPOINTS
.lock()
.unwrap_or_else(|error| error.into_inner())
.get(path)
.filter(|armed| {
armed.boundary == LocalPublishBoundary::DuringTemporaryWrite
&& armed.fault == LocalPublishFault::PartialWrite
})
.map(|_armed| requested / 2)
}
}