use std::path::Path;
use anyhow::Result;
#[allow(clippy::len_without_is_empty)]
pub trait FileIo
where
Self: Sized + Send + Sync + std::fmt::Debug,
{
fn open(path: &Path) -> Result<Self>;
fn open_existing(path: &Path) -> Result<Self>;
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()>;
fn write_at(&mut self, offset: u64, data: &[u8]) -> Result<()>;
fn sync(&mut self) -> Result<()>;
fn len(&self) -> Result<u64>;
fn set_len(&mut self, len: u64) -> Result<()>;
}
pub mod simple_file;
pub(crate) fn lock_dir(dir: &Path) -> Result<std::fs::File> {
use crate::write_ahead::WriteAheadError;
let lock = std::fs::File::open(dir)?;
match lock.try_lock() {
Ok(()) => Ok(lock),
Err(std::fs::TryLockError::WouldBlock) => Err(WriteAheadError::DirectoryLocked.into()),
Err(std::fs::TryLockError::Error(e)) => Err(e.into()),
}
}
pub(crate) fn sync_parents(dir: &Path) -> Result<()> {
let absolute = dir.canonicalize()?;
for parent in absolute.ancestors().skip(1) {
sync_dir(parent)?;
}
Ok(())
}
pub(crate) fn sync_dir(dir: &Path) -> Result<()> {
use anyhow::Context;
#[cfg(test)]
tests::before_sync(dir)?;
std::fs::File::open(dir)
.and_then(|f| f.sync_all())
.with_context(|| format!("Failed to fsync directory {}", dir.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SimpleFile, WriteAhead, WriteAheadOptions};
use std::{cell::RefCell, path::PathBuf};
#[derive(Default)]
struct SyncProbe {
calls: Vec<PathBuf>,
fail: Option<PathBuf>,
}
thread_local! {
static PROBE: RefCell<Option<SyncProbe>> = const { RefCell::new(None) };
}
pub(super) fn before_sync(dir: &Path) -> Result<()> {
PROBE.with(|probe| {
if let Some(probe) = probe.borrow_mut().as_mut() {
probe.calls.push(dir.to_path_buf());
if probe.fail.as_deref() == Some(dir) {
probe.fail = None;
return Err(std::io::Error::from_raw_os_error(5).into());
}
}
Ok(())
})
}
#[test]
fn startup_retries_parent_and_wal_directory_barriers() {
for fail_on_parent in [false, true] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().canonicalize().unwrap().join("new-parent/wal");
let fail = if fail_on_parent {
path.parent().unwrap().to_path_buf()
} else {
path.clone()
};
PROBE.with(|p| {
*p.borrow_mut() = Some(SyncProbe {
fail: Some(fail.clone()),
..Default::default()
})
});
let mut wal = WriteAhead::<SimpleFile>::with_options(WriteAheadOptions {
log_dir: path.clone(),
preallocation_chunk_size: None,
..Default::default()
});
assert!(wal.start().is_err());
assert!(wal.writer().is_err());
let file = path.join("0000000000.log");
if fail_on_parent {
assert!(!file.exists());
} else {
assert_eq!(
std::fs::metadata(&file).unwrap().len(),
crate::logfile::FILE_HEADER_SIZE
);
}
wal.start().unwrap();
let calls = PROBE.with(|p| p.borrow_mut().take().unwrap().calls);
let parents: Vec<_> = path
.canonicalize()
.unwrap()
.ancestors()
.skip(1)
.map(Path::to_path_buf)
.collect();
let retry_start = calls.iter().position(|p| p == &fail).unwrap() + 1;
assert_eq!(&calls[retry_start..calls.len() - 1], parents);
assert_eq!(calls.last().unwrap(), &path);
let id =
futures::executor::block_on(wal.write_batch(vec![b"durable".to_vec()])).unwrap()[0];
drop(wal);
let mut reopened = WriteAhead::<SimpleFile>::with_options(WriteAheadOptions {
log_dir: path,
..Default::default()
});
reopened.start().unwrap();
assert_eq!(
reopened.read(id.file_id, id.file_offset).unwrap(),
b"durable"
);
}
}
}