use std::fs::File;
use crate::error::{Result, WalError};
pub(crate) enum DurabilityState {
Synced,
FlushedUnsynced,
Poisoned { detail: String },
}
impl DurabilityState {
pub(crate) fn new() -> Self {
Self::Synced
}
pub(crate) fn check(&self) -> Result<()> {
match self {
Self::Poisoned { detail } => Err(WalError::DurabilityLost {
detail: detail.clone(),
}),
Self::Synced | Self::FlushedUnsynced => Ok(()),
}
}
pub(crate) fn needs_fsync(&self) -> bool {
matches!(self, Self::FlushedUnsynced)
}
pub(crate) fn should_skip_sync(&self, buffer_empty: bool) -> bool {
buffer_empty && !self.needs_fsync()
}
pub(crate) fn record_flush(&mut self) {
if matches!(self, Self::Synced) {
*self = Self::FlushedUnsynced;
}
}
pub(crate) fn record_sync_ok(&mut self) {
if !matches!(self, Self::Poisoned { .. }) {
*self = Self::Synced;
}
}
pub(crate) fn poison(&mut self, detail: String) -> WalError {
let err = WalError::DurabilityLost {
detail: detail.clone(),
};
crate::diag::durability_lost(&err, &detail);
*self = Self::Poisoned { detail };
err
}
}
pub(crate) fn fsync_and_track(file: &File, state: &mut DurabilityState) -> Result<()> {
#[cfg(feature = "failpoints")]
if let Some(detail) = nodedb_types::fail_point::eval_fail(
nodedb_types::fail_point::FailScope::Any,
"wal::fsync_failure",
) {
return Err(state.poison(format!(
"fsync failed: failpoint wal::fsync_failure: {detail}"
)));
}
if let Err(e) = file.sync_all() {
return Err(state.poison(format!("fsync failed: {e}")));
}
state.record_sync_ok();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flush_marks_an_outstanding_fsync() {
let mut state = DurabilityState::new();
assert!(!state.needs_fsync());
state.record_flush();
assert!(state.needs_fsync());
}
#[test]
fn successful_fsync_clears_the_marker() {
let dir = tempfile::tempdir().unwrap();
let file = File::create(dir.path().join("f")).unwrap();
let mut state = DurabilityState::new();
state.record_flush();
fsync_and_track(&file, &mut state).unwrap();
assert!(!state.needs_fsync());
state.check().unwrap();
}
#[test]
fn poison_is_permanent() {
let mut state = DurabilityState::new();
let err = state.poison("EIO".to_string());
assert!(matches!(err, WalError::DurabilityLost { .. }));
assert!(matches!(
state.check(),
Err(WalError::DurabilityLost { .. })
));
state.record_flush();
assert!(matches!(
state.check(),
Err(WalError::DurabilityLost { .. })
));
}
}