use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::error::{Result, WalError};
fn base_options() -> OpenOptions {
let mut opts = OpenOptions::new();
opts.create(true).write(true).append(false);
opts
}
#[cfg(target_os = "linux")]
fn segment_options(use_direct_io: bool) -> OpenOptions {
use std::os::unix::fs::OpenOptionsExt as _;
let mut opts = base_options();
if use_direct_io {
opts.custom_flags(libc::O_DIRECT);
}
opts
}
#[cfg(not(target_os = "linux"))]
fn segment_options(_use_direct_io: bool) -> OpenOptions {
base_options()
}
pub(crate) fn open_segment_file(path: &Path, use_direct_io: bool) -> Result<File> {
direct_io_open_injection(path, use_direct_io)?;
match segment_options(use_direct_io).open(path) {
Ok(file) => Ok(file),
Err(err) => Err(classify_open_error(path, use_direct_io, err)),
}
}
#[cfg(feature = "failpoints")]
fn direct_io_open_injection(path: &Path, use_direct_io: bool) -> Result<()> {
if !use_direct_io {
return Ok(());
}
nodedb_types::fail_point_err!("wal::direct_io_unsupported", |_: String| {
WalError::DirectIoUnsupported {
path: path.display().to_string(),
}
});
Ok(())
}
#[cfg(not(feature = "failpoints"))]
fn direct_io_open_injection(_path: &Path, _use_direct_io: bool) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
fn classify_open_error(path: &Path, use_direct_io: bool, err: std::io::Error) -> WalError {
if use_direct_io && err.raw_os_error() == Some(libc::EINVAL) && buffered_open_works(path) {
return WalError::DirectIoUnsupported {
path: path.display().to_string(),
};
}
WalError::Io(err)
}
#[cfg(target_os = "linux")]
fn buffered_open_works(path: &Path) -> bool {
let Some(dir) = path.parent() else {
return false;
};
let probe = dir.join(".nodedb-direct-io-probe");
let opened = base_options().open(&probe).is_ok();
let _ = std::fs::remove_file(&probe);
opened
}
#[cfg(not(target_os = "linux"))]
fn classify_open_error(_path: &Path, _use_direct_io: bool, err: std::io::Error) -> WalError {
WalError::Io(err)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buffered_open_creates_the_segment() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("seg.wal");
let file = open_segment_file(&path, false).unwrap();
drop(file);
assert!(path.exists());
}
#[cfg(target_os = "linux")]
#[test]
fn classifying_a_direct_io_refusal_leaves_no_files_behind() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("seg.wal");
let err = std::io::Error::from_raw_os_error(libc::EINVAL);
let verdict = classify_open_error(&path, true, err);
assert!(
matches!(verdict, WalError::DirectIoUnsupported { .. }),
"expected a direct-I/O verdict, got {verdict:?}"
);
assert!(
!path.exists(),
"the diagnostic created the segment file it was only supposed to ask about"
);
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
assert!(
leftovers.is_empty(),
"diagnosing the failure left files in the WAL directory: {leftovers:?}"
);
}
#[test]
fn unrelated_open_failure_is_not_reported_as_direct_io_unsupported() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("absent").join("seg.wal");
let err = open_segment_file(&path, true).unwrap_err();
assert!(
matches!(err, WalError::Io(_)),
"expected a plain I/O error, got {err:?}"
);
}
}