use std::fs;
use std::io::Write;
use std::path::Path;
use crate::error::{Result, WalError};
pub fn fsync_directory(dir: &Path) -> Result<()> {
nodedb_types::fail_point_err!("wal::fsync_directory", |detail: String| WalError::Io(
std::io::Error::other(format!("failpoint wal::fsync_directory: {detail}"))
));
let dir_file = fs::File::open(dir).map_err(WalError::Io)?;
dir_file.sync_all().map_err(WalError::Io)?;
Ok(())
}
fn invalid_input(detail: String) -> WalError {
WalError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
detail,
))
}
fn checked_name(op: &str, label: &str, name: &str) -> Result<()> {
if nodedb_types::is_plain_path_component(name) {
return Ok(());
}
Err(invalid_input(format!(
"{op}: {label} must be one plain name — no separator, no '..', no drive \
letter, no leading or trailing dot or space: {name:?}"
)))
}
fn tmp_name(name: &str) -> String {
format!("{name}.tmp")
}
pub fn atomic_write_fsync(dir: &Path, name: &str, bytes: &[u8]) -> Result<()> {
checked_name("atomic_write_fsync", "name", name)?;
let dst = dir.join(name);
let tmp = dir.join(tmp_name(name));
{
let mut f = fs::File::create(&tmp).map_err(WalError::Io)?;
f.write_all(bytes).map_err(WalError::Io)?;
f.sync_data().map_err(WalError::Io)?;
}
fs::rename(&tmp, &dst).map_err(WalError::Io)?;
fsync_directory(dir)?;
Ok(())
}
pub fn atomic_swap_dirs_fsync(parent: &Path, live: &str, backup: &str, staged: &str) -> Result<()> {
checked_name("atomic_swap_dirs_fsync", "live", live)?;
checked_name("atomic_swap_dirs_fsync", "backup", backup)?;
checked_name("atomic_swap_dirs_fsync", "staged", staged)?;
let live = parent.join(live);
let backup = parent.join(backup);
let staged = parent.join(staged);
fs::rename(&live, &backup).map_err(WalError::Io)?;
fs::rename(&staged, &live).map_err(WalError::Io)?;
fsync_directory(parent)?;
Ok(())
}
pub fn read_checkpoint_dontneed(path: &Path) -> Result<Vec<u8>> {
let file = fs::File::open(path).map_err(WalError::Io)?;
let len = file.metadata().map_err(WalError::Io)?.len();
let bytes = fs::read(path).map_err(WalError::Io)?;
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd as _;
let ret = unsafe {
libc::posix_fadvise(
file.as_raw_fd(),
0,
len as libc::off_t,
libc::POSIX_FADV_DONTNEED,
)
};
if ret != 0 {
tracing::debug!(
path = %path.display(),
ret,
"posix_fadvise(DONTNEED) returned nonzero — checkpoint bytes may stay in page cache"
);
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = (file, len);
}
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn is_invalid_input(err: &WalError) -> bool {
matches!(err, WalError::Io(e) if e.kind() == std::io::ErrorKind::InvalidInput)
}
const BAD_NAMES: [&str; 5] = ["sub/escaped.ckpt", "..", "/etc/passwd", "C:evil", "."];
#[test]
fn atomic_write_fsync_roundtrip() {
let dir = tempfile::tempdir().unwrap();
atomic_write_fsync(dir.path(), "payload.ckpt", b"hello world").unwrap();
assert!(
!dir.path().join("payload.ckpt.tmp").exists(),
"tmp must be renamed away"
);
assert_eq!(
fs::read(dir.path().join("payload.ckpt")).unwrap(),
b"hello world"
);
}
#[test]
fn atomic_write_fsync_overwrites() {
let dir = tempfile::tempdir().unwrap();
atomic_write_fsync(dir.path(), "payload.ckpt", b"v1").unwrap();
atomic_write_fsync(dir.path(), "payload.ckpt", b"v2").unwrap();
assert_eq!(fs::read(dir.path().join("payload.ckpt")).unwrap(), b"v2");
}
#[test]
fn tmp_name_keeps_the_full_destination_name() {
assert_eq!(tmp_name("a.col"), "a.col.tmp");
assert_ne!(tmp_name("a.col"), tmp_name("a.idx"));
}
#[test]
fn atomic_swap_dirs_fsync_swaps() {
let dir = tempfile::tempdir().unwrap();
let live = dir.path().join("live");
let backup = dir.path().join("backup");
let staged = dir.path().join("staged");
fs::create_dir(&live).unwrap();
fs::write(live.join("marker"), b"old").unwrap();
fs::create_dir(&staged).unwrap();
fs::write(staged.join("marker"), b"new").unwrap();
atomic_swap_dirs_fsync(dir.path(), "live", "backup", "staged").unwrap();
assert_eq!(fs::read(live.join("marker")).unwrap(), b"new");
assert_eq!(fs::read(backup.join("marker")).unwrap(), b"old");
assert!(!staged.exists());
}
#[test]
fn atomic_write_fsync_rejects_a_name_that_is_not_one_component() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
for name in BAD_NAMES {
let err = atomic_write_fsync(dir.path(), name, b"x").unwrap_err();
assert!(
is_invalid_input(&err),
"{name:?} must be InvalidInput, got {err:?}"
);
}
assert!(
fs::read_dir(dir.path().join("sub"))
.unwrap()
.next()
.is_none(),
"nothing may be written before the check"
);
}
#[test]
fn atomic_swap_dirs_fsync_rejects_a_bad_name_in_each_position() {
for position in 0..3 {
for name in BAD_NAMES {
let dir = tempfile::tempdir().unwrap();
let live = dir.path().join("live");
fs::create_dir(&live).unwrap();
fs::write(live.join("marker"), b"old").unwrap();
fs::create_dir(dir.path().join("staged")).unwrap();
let mut names = ["live", "backup", "staged"];
names[position] = name;
let err =
atomic_swap_dirs_fsync(dir.path(), names[0], names[1], names[2]).unwrap_err();
assert!(
is_invalid_input(&err),
"{name:?} at position {position} must be InvalidInput, got {err:?}"
);
assert_eq!(
fs::read(live.join("marker")).unwrap(),
b"old",
"live must be untouched when the check fails"
);
}
}
}
#[test]
fn read_checkpoint_dontneed_returns_bytes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ckpt");
fs::write(&path, b"checkpoint bytes").unwrap();
let bytes = read_checkpoint_dontneed(&path).unwrap();
assert_eq!(bytes, b"checkpoint bytes");
}
}