use crate::page::PAGE_SIZE;
use crate::{Error, Result};
use std::fs::{File, OpenOptions};
use std::path::Path;
#[cfg(unix)]
type FileIdentity = (u64, u64);
#[cfg(not(unix))]
type FileIdentity = ();
#[cfg(unix)]
fn file_identity(file: &File) -> std::io::Result<FileIdentity> {
use std::os::unix::fs::MetadataExt;
let metadata = file.metadata()?;
Ok((metadata.dev(), metadata.ino()))
}
#[cfg(not(unix))]
fn file_identity(_file: &File) -> std::io::Result<FileIdentity> { Ok(()) }
fn writer_paths() -> &'static std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, FileIdentity>> {
static PATHS: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, FileIdentity>>,
> = std::sync::OnceLock::new();
PATHS.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub(crate) fn writer_owned_by_this_process(path: &Path) -> bool {
let Ok(path) = std::fs::canonicalize(path) else { return false };
let paths = writer_paths().lock().unwrap();
let Some(owned) = paths.get(&path) else { return false };
let Ok(file) = File::open(&path) else { return false };
file_identity(&file).is_ok_and(|current| current == *owned)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IoMode { Direct, Buffered }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Barrier { None, Data, Full }
#[derive(Debug, Default)]
pub struct IoStats {
pub writes: std::sync::atomic::AtomicU64,
pub write_bytes: std::sync::atomic::AtomicU64,
pub reads: std::sync::atomic::AtomicU64,
}
impl IoStats {
pub fn take(&self) -> (u64, u64, u64) {
use std::sync::atomic::Ordering::Relaxed;
(self.writes.swap(0, Relaxed), self.write_bytes.swap(0, Relaxed), self.reads.swap(0, Relaxed))
}
}
pub trait FileIo: Send + Sync {
fn manages_free_pages(&self) -> bool { false }
fn pop_free_page(&self) -> Result<Option<u32>> { Ok(None) }
fn push_free_page(&self, _page: u32) -> Result<()> { unreachable!() }
fn stats(&self) -> Option<&IoStats> { None }
fn requires_alignment(&self) -> bool;
fn read_at(&self, buf: &mut [u8], off: u64) -> Result<()>;
fn write_at(&self, buf: &[u8], off: u64) -> Result<()>;
fn sync_data(&self) -> Result<()>;
fn sync_full(&self) -> Result<()>;
fn sync_full_primitive(&self) -> &'static str;
fn sync_dir(&self) -> Result<()>;
fn len(&self) -> Result<u64>;
fn set_len(&self, n: u64) -> Result<()>;
}
struct PosixFile {
f: File,
#[cfg(unix)]
dir: File,
mode: IoMode,
stats: IoStats,
writer_path: Option<std::path::PathBuf>,
writer_identity: Option<FileIdentity>,
}
impl Drop for PosixFile {
fn drop(&mut self) {
if let (Some(path), Some(identity)) =
(self.writer_path.take(), self.writer_identity.take())
{
let _ = unlock(&self.f);
let mut paths = writer_paths().lock().unwrap();
if paths.get(&path) == Some(&identity) { paths.remove(&path); }
}
}
}
impl FileIo for PosixFile {
fn stats(&self) -> Option<&IoStats> { Some(&self.stats) }
fn requires_alignment(&self) -> bool { self.mode == IoMode::Direct }
fn read_at(&self, buf: &mut [u8], off: u64) -> Result<()> {
self.stats.reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if self.requires_alignment() {
debug_assert_eq!(buf.len() % PAGE_SIZE, 0, "unaligned length");
debug_assert_eq!(off as usize % PAGE_SIZE, 0, "unaligned offset");
}
#[cfg(unix)] {
use std::os::unix::fs::FileExt;
self.f.read_exact_at(buf, off)?;
}
#[cfg(windows)] {
use std::os::windows::fs::FileExt;
let mut done = 0usize;
while done < buf.len() {
let n = self.f.seek_read(&mut buf[done..], off + done as u64)?;
if n == 0 { return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "seek_read hit EOF").into()); }
done += n;
}
}
Ok(())
}
fn write_at(&self, buf: &[u8], off: u64) -> Result<()> {
use std::sync::atomic::Ordering::Relaxed;
self.stats.writes.fetch_add(1, Relaxed);
self.stats.write_bytes.fetch_add(buf.len() as u64, Relaxed);
if self.requires_alignment() {
debug_assert_eq!(buf.len() % PAGE_SIZE, 0, "unaligned length");
debug_assert_eq!(off as usize % PAGE_SIZE, 0, "unaligned offset");
}
#[cfg(unix)] {
use std::os::unix::fs::FileExt;
self.f.write_all_at(buf, off)?;
}
#[cfg(windows)] {
use std::os::windows::fs::FileExt;
let mut done = 0usize;
while done < buf.len() {
let n = self.f.seek_write(&buf[done..], off + done as u64)?;
if n == 0 { return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "seek_write wrote 0").into()); }
done += n;
}
}
Ok(())
}
fn sync_data(&self) -> Result<()> { sync_data_raw(&self.f) }
fn sync_full(&self) -> Result<()> { sync_full_raw(&self.f) }
fn sync_full_primitive(&self) -> &'static str { sync_full_primitive_name() }
fn sync_dir(&self) -> Result<()> {
#[cfg(unix)] { self.dir.sync_all()?; }
Ok(())
}
fn len(&self) -> Result<u64> { Ok(self.f.metadata()?.len()) }
fn set_len(&self, n: u64) -> Result<()> { self.f.set_len(n)?; Ok(()) }
}
#[cfg(target_os = "linux")]
fn sync_data_raw(f: &File) -> Result<()> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::fdatasync(f.as_raw_fd()) };
if rc == -1 { return Err(std::io::Error::last_os_error().into()); }
Ok(())
}
#[cfg(target_os = "macos")]
fn sync_data_raw(f: &File) -> Result<()> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::fsync(f.as_raw_fd()) };
if rc == -1 { return Err(std::io::Error::last_os_error().into()); }
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn sync_data_raw(f: &File) -> Result<()> { f.sync_all()?; Ok(()) }
#[cfg(target_os = "macos")]
fn sync_full_raw(f: &File) -> Result<()> {
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_FULLFSYNC) };
if rc == -1 { return Err(std::io::Error::last_os_error().into()); }
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn sync_full_raw(f: &File) -> Result<()> { f.sync_all()?; Ok(()) }
fn sync_full_primitive_name() -> &'static str {
#[cfg(target_os = "macos")] { "F_FULLFSYNC" }
#[cfg(not(target_os = "macos"))] { "fsync (sync_all)" }
}
pub fn open_file(path: &Path, want: IoMode) -> Result<(Box<dyn FileIo>, IoMode)> {
open_file_impl(path, want, false)
}
pub fn open_file_writer(path: &Path, want: IoMode) -> Result<(Box<dyn FileIo>, IoMode)> {
open_file_impl(path, want, true)
}
fn open_file_impl(path: &Path, want: IoMode, writer: bool) -> Result<(Box<dyn FileIo>, IoMode)> {
let parent = path.parent().unwrap_or(Path::new("."));
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
let dir = File::open(parent)?;
let base = || {
let mut o = OpenOptions::new();
o.read(true).write(true).create(true);
o
};
let (f, got) = match want {
IoMode::Direct => match open_unbuffered(base(), path) {
Ok(f) => (f, IoMode::Direct),
Err(_) => (base().open(path)?, IoMode::Buffered),
},
IoMode::Buffered => (base().open(path)?, IoMode::Buffered),
};
let (writer_path, writer_identity) = if writer {
let path = std::fs::canonicalize(path)?;
let identity = file_identity(&f)?;
let mut paths = writer_paths().lock().unwrap();
if paths.get(&path) == Some(&identity) {
#[cfg(feature = "test-support")]
{ (None, None) }
#[cfg(not(feature = "test-support"))]
{ return Err(Error::WriterLocked); }
} else {
if !try_lock_exclusive(&f)? { return Err(Error::WriterLocked); }
paths.insert(path.clone(), identity);
(Some(path), Some(identity))
}
} else {
(None, None)
};
Ok((Box::new(PosixFile {
f,
#[cfg(unix)]
dir,
mode: got,
stats: IoStats::default(),
writer_path,
writer_identity,
}), got))
}
pub fn open_file_readonly(path: &Path) -> Result<Box<dyn FileIo>> {
let parent = path.parent().unwrap_or(Path::new("."));
#[cfg(unix)]
let dir = File::open(parent)?;
let f = OpenOptions::new().read(true).open(path)?;
Ok(Box::new(PosixFile {
f,
#[cfg(unix)]
dir,
mode: IoMode::Buffered,
stats: IoStats::default(),
writer_path: None,
writer_identity: None,
}))
}
pub fn open_recovery_source(path: &Path) -> Result<Box<dyn FileIo>> {
let parent = path.parent().unwrap_or(Path::new("."));
#[cfg(unix)]
let dir = File::open(parent)?;
let f = OpenOptions::new().read(true).open(path)?;
let path = std::fs::canonicalize(path)?;
let identity = file_identity(&f)?;
let mut paths = writer_paths().lock().unwrap();
if paths.get(&path) == Some(&identity) || !try_lock_exclusive(&f)? {
return Err(Error::WriterLocked);
}
paths.insert(path.clone(), identity);
Ok(Box::new(PosixFile {
f,
#[cfg(unix)]
dir,
mode: IoMode::Buffered,
stats: IoStats::default(),
writer_path: Some(path),
writer_identity: Some(identity),
}))
}
pub fn sync_directory(path: &Path) -> Result<()> {
#[cfg(unix)]
{ File::open(path)?.sync_all()?; }
#[cfg(windows)]
{ let _ = path; }
Ok(())
}
#[cfg(target_os = "linux")]
fn open_unbuffered(mut opts: OpenOptions, path: &Path) -> std::io::Result<File> {
use std::os::unix::fs::OpenOptionsExt;
opts.custom_flags(libc::O_DIRECT).open(path)
}
#[cfg(target_os = "macos")]
fn open_unbuffered(_opts: OpenOptions, _path: &Path) -> std::io::Result<File> {
Err(std::io::Error::new(std::io::ErrorKind::Unsupported,
"macOS uncached I/O disabled after failed data-isolation probe"))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn open_unbuffered(_opts: OpenOptions, _path: &Path) -> std::io::Result<File> {
Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "no unbuffered mode"))
}
pub struct AlignedRegion { ptr: *mut u8, len: usize }
unsafe impl Send for AlignedRegion {}
unsafe impl Sync for AlignedRegion {}
impl AlignedRegion {
pub fn new(len: usize) -> Result<Self> {
assert!(len > 0, "an AlignedRegion of zero bytes is a zero-size allocation");
assert_eq!(len % PAGE_SIZE, 0);
let layout = std::alloc::Layout::from_size_align(len, PAGE_SIZE).unwrap();
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
if ptr.is_null() { return Err(Error::OutOfBudget); }
Ok(AlignedRegion { ptr, len })
}
fn frame_start(&self, i: usize) -> usize {
let start = i.checked_mul(PAGE_SIZE).expect("frame index overflow");
let end = start.checked_add(PAGE_SIZE).expect("frame index overflow");
assert!(end <= self.len, "frame {i} is outside the region");
start
}
pub unsafe fn page(&self, i: usize) -> &[u8] {
let start = self.frame_start(i);
unsafe { std::slice::from_raw_parts(self.ptr.add(start), PAGE_SIZE) }
}
#[allow(clippy::mut_from_ref)]
pub unsafe fn page_mut(&self, i: usize) -> &mut [u8] {
let start = self.frame_start(i);
unsafe { std::slice::from_raw_parts_mut(self.ptr.add(start), PAGE_SIZE) }
}
pub unsafe fn prefix(&self, len: usize) -> &[u8] {
assert!(len > 0 && len <= self.len && len % PAGE_SIZE == 0);
unsafe { std::slice::from_raw_parts(self.ptr, len) }
}
}
impl Drop for AlignedRegion {
fn drop(&mut self) {
let layout = std::alloc::Layout::from_size_align(self.len, PAGE_SIZE).unwrap();
unsafe { std::alloc::dealloc(self.ptr, layout) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page::PAGE_SIZE;
#[repr(align(4096))]
struct TestPage([u8; PAGE_SIZE]);
#[test]
fn pages_round_trip_through_the_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let (f, _) = open_file(&path, IoMode::Buffered).unwrap();
let mut w = vec![0u8; PAGE_SIZE];
for (i, b) in w.iter_mut().enumerate() { *b = (i % 251) as u8; }
f.write_at(&w, (PAGE_SIZE * 3) as u64).unwrap();
f.sync_data().unwrap();
let mut r = vec![0u8; PAGE_SIZE];
f.read_at(&mut r, (PAGE_SIZE * 3) as u64).unwrap();
assert_eq!(r, w);
assert_eq!(f.len().unwrap(), (PAGE_SIZE * 4) as u64);
}
#[test]
fn a_read_past_the_end_is_an_error_not_a_short_buffer() {
let dir = tempfile::tempdir().unwrap();
let (f, _) = open_file(&dir.path().join("t.db"), IoMode::Buffered).unwrap();
let mut r = vec![0u8; PAGE_SIZE];
assert!(f.read_at(&mut r, 0).is_err());
}
#[test]
fn requesting_direct_degrades_rather_than_failing() {
let dir = tempfile::tempdir().unwrap();
let (f, got) = open_file(&dir.path().join("t.db"), IoMode::Direct)
.expect("requesting Direct must degrade, never error");
assert_eq!(f.requires_alignment(), got == IoMode::Direct);
#[cfg(target_os = "macos")]
assert_eq!(got, IoMode::Buffered, "unproven uncached mode must stay disabled");
let w = TestPage([0u8; PAGE_SIZE]);
f.write_at(&w.0, 0).unwrap();
f.sync_data().unwrap();
let mut r = TestPage([0u8; PAGE_SIZE]);
f.read_at(&mut r.0, 0).unwrap();
assert_eq!(r.0, w.0);
}
#[test]
fn concurrent_direct_and_buffered_files_keep_their_own_bytes() {
std::thread::scope(|scope| {
for worker in 0..8u8 {
scope.spawn(move || {
for round in 0..32u8 {
let dir = tempfile::tempdir().unwrap();
let mode = if worker % 2 == 0 { IoMode::Direct } else { IoMode::Buffered };
let (f, _) = open_file(&dir.path().join("roundtrip"), mode).unwrap();
let mut w = TestPage([0u8; PAGE_SIZE]);
if round % 2 != 0 {
for (i, byte) in w.0.iter_mut().enumerate() { *byte = worker.wrapping_add(round).wrapping_add(i as u8); }
}
f.write_at(&w.0, 0).unwrap();
f.sync_data().unwrap();
let mut r = TestPage([0u8; PAGE_SIZE]);
f.read_at(&mut r.0, 0).unwrap();
if r.0 != w.0 {
let retained = dir.keep();
std::fs::write(retained.join("expected"), &w.0).unwrap();
std::fs::write(retained.join("observed"), &r.0).unwrap();
panic!("I/O isolation failed: worker {worker}, round {round}, evidence {}", retained.display());
}
}
});
}
});
}
}
pub fn try_lock_exclusive(f: &std::fs::File) -> std::io::Result<bool> {
os_lock::lock(f, true, false)
}
pub fn try_lock_shared(f: &std::fs::File) -> std::io::Result<bool> {
os_lock::lock(f, false, false)
}
pub fn lock_shared(f: &std::fs::File) -> std::io::Result<()> {
os_lock::lock(f, false, true).map(|_| ())
}
pub fn lock_exclusive(f: &std::fs::File) -> std::io::Result<()> {
os_lock::lock(f, true, true).map(|_| ())
}
pub fn unlock(f: &std::fs::File) -> std::io::Result<()> {
os_lock::unlock(f)
}
#[cfg(not(windows))]
mod os_lock {
pub fn lock(f: &std::fs::File, exclusive: bool, wait: bool) -> std::io::Result<bool> {
let outcome = match (exclusive, wait) {
(true, true) => return f.lock().map(|_| true),
(false, true) => return f.lock_shared().map(|_| true),
(true, false) => f.try_lock(),
(false, false) => f.try_lock_shared(),
};
match outcome {
Ok(()) => Ok(true),
Err(std::fs::TryLockError::WouldBlock) => Ok(false),
Err(std::fs::TryLockError::Error(e)) => Err(e),
}
}
pub fn unlock(f: &std::fs::File) -> std::io::Result<()> {
f.unlock()
}
}
#[cfg(windows)]
mod os_lock {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::ERROR_LOCK_VIOLATION;
use windows_sys::Win32::Storage::FileSystem::{
LockFileEx, UnlockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
};
use windows_sys::Win32::System::IO::OVERLAPPED;
const LOCK_BYTE: u64 = 0x7fff_ffff_0000_0000;
fn region() -> OVERLAPPED {
let mut o: OVERLAPPED = unsafe { std::mem::zeroed() };
o.Anonymous.Anonymous.Offset = LOCK_BYTE as u32;
o.Anonymous.Anonymous.OffsetHigh = (LOCK_BYTE >> 32) as u32;
o
}
pub fn lock(f: &std::fs::File, exclusive: bool, wait: bool) -> std::io::Result<bool> {
let mut flags = 0;
if exclusive {
flags |= LOCKFILE_EXCLUSIVE_LOCK;
}
if !wait {
flags |= LOCKFILE_FAIL_IMMEDIATELY;
}
let mut o = region();
let ok = unsafe { LockFileEx(f.as_raw_handle() as _, flags, 0, 1, 0, &mut o) };
if ok != 0 {
return Ok(true);
}
let error = std::io::Error::last_os_error();
if !wait && error.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32) {
return Ok(false);
}
Err(error)
}
pub fn unlock(f: &std::fs::File) -> std::io::Result<()> {
let mut o = region();
let ok = unsafe { UnlockFileEx(f.as_raw_handle() as _, 0, 1, 0, &mut o) };
if ok != 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
}
pub struct Locked(std::fs::File);
impl Locked {
pub fn held(f: std::fs::File) -> Self {
Self(f)
}
}
impl std::ops::Deref for Locked {
type Target = std::fs::File;
fn deref(&self) -> &std::fs::File {
&self.0
}
}
impl Drop for Locked {
fn drop(&mut self) {
let _ = unlock(&self.0);
}
}