use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileId {
Wal,
Snapshot,
SnapshotBak,
Roles,
}
impl FileId {
fn name(self) -> &'static str {
match self {
FileId::Wal => "wal.bin",
FileId::Snapshot => "snapshot.bin",
FileId::SnapshotBak => "snapshot.bin.bak",
FileId::Roles => "roles.json",
}
}
}
pub trait Fs {
fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
fn sync(&mut self, file: FileId) -> std::io::Result<()>;
fn read(&self, file: FileId) -> std::io::Result<Vec<u8>>;
fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
fn snapshot_path(&self) -> Option<std::path::PathBuf> {
None
}
fn wal_path(&self) -> Option<std::path::PathBuf> {
None
}
fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
let mut bytes = self.read(file)?;
bytes.truncate(n);
Ok(bytes)
}
fn try_lock_exclusive(&self) -> std::io::Result<bool> {
Ok(true)
}
fn unlock(&self) -> std::io::Result<()> {
Ok(())
}
fn wal_len(&self) -> std::io::Result<u64> {
Ok(self.read(FileId::Wal)?.len() as u64)
}
fn read_range(&self, file: FileId, from: u64) -> std::io::Result<Vec<u8>> {
let bytes = self.read(file)?;
let from = from.min(bytes.len() as u64) as usize;
Ok(bytes[from..].to_vec())
}
fn snapshot_ident(&self) -> std::io::Result<Option<(u64, u64)>> {
let len = self.read(FileId::Snapshot)?.len() as u64;
Ok(if len == 0 { None } else { Some((len, 0)) })
}
fn list_archives(&self) -> std::io::Result<Vec<u64>> {
Ok(vec![])
}
fn read_archive(&self, _n: u64) -> std::io::Result<Vec<u8>> {
Ok(vec![])
}
fn archive_wal(&mut self, _n: u64) -> std::io::Result<()> {
Err(std::io::Error::other(
"archive_wal not supported by this Fs implementation",
))
}
fn delete_archive(&mut self, _n: u64) -> std::io::Result<()> {
Ok(())
}
fn read_horizon_floor(&self) -> std::io::Result<u64> {
Ok(0)
}
fn write_horizon_floor(&mut self, _floor: u64) -> std::io::Result<()> {
Ok(())
}
fn has_genesis_marker(&self) -> bool {
false
}
fn write_genesis_marker(&mut self) -> std::io::Result<()> {
Ok(())
}
fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub trait FsIntrospect {
fn total_appended(&self) -> usize;
fn sync_count(&self) -> usize {
0
}
}
pub const LOCK_FILE: &str = "LOCK";
#[derive(Debug, Default)]
struct LockState {
file: Option<File>,
held: bool,
}
#[derive(Debug)]
pub struct RealFs {
dir: PathBuf,
lock: std::sync::Mutex<LockState>,
}
impl RealFs {
pub fn new(dir: &std::path::Path) -> std::io::Result<Self> {
std::fs::create_dir_all(dir)?;
Ok(Self {
dir: dir.to_path_buf(),
lock: std::sync::Mutex::new(LockState::default()),
})
}
pub fn dir(&self) -> &std::path::Path {
&self.dir
}
fn path(&self, file: FileId) -> PathBuf {
self.dir.join(file.name())
}
}
impl Fs for RealFs {
fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(self.path(file))?;
f.write_all(data)
}
fn sync(&mut self, file: FileId) -> std::io::Result<()> {
let f = File::open(self.path(file))?;
full_sync(&f)
}
fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
match File::open(self.path(file)) {
Ok(mut f) => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(buf)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
let tmp = self.dir.join(format!("{}.tmp", file.name()));
{
let mut f = File::create(&tmp)?;
f.write_all(data)?;
full_sync(&f)?;
}
std::fs::rename(&tmp, self.path(file))?;
sync_dir(&self.dir)
}
fn snapshot_path(&self) -> Option<std::path::PathBuf> {
Some(self.path(FileId::Snapshot))
}
fn wal_path(&self) -> Option<std::path::PathBuf> {
Some(self.path(FileId::Wal))
}
fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
use std::io::Read as _;
match File::open(self.path(file)) {
Ok(mut f) => {
let mut buf = vec![0u8; n];
let read = f.read(&mut buf)?;
buf.truncate(read);
Ok(buf)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn try_lock_exclusive(&self) -> std::io::Result<bool> {
let mut state = self.lock.lock().unwrap_or_else(|e| e.into_inner());
if state.held {
return Ok(true);
}
if state.file.is_none() {
state.file = Some(
OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(self.dir.join(LOCK_FILE))?,
);
}
let f = state.file.as_ref().expect("lock file just opened");
match f.try_lock() {
Ok(()) => {
state.held = true;
Ok(true)
}
Err(std::fs::TryLockError::WouldBlock) => Ok(false),
Err(std::fs::TryLockError::Error(e)) => Err(e),
}
}
fn unlock(&self) -> std::io::Result<()> {
let mut state = self.lock.lock().unwrap_or_else(|e| e.into_inner());
if !state.held {
return Ok(());
}
state.held = false;
match state.file.as_ref() {
Some(f) => f.unlock(),
None => Ok(()),
}
}
fn wal_len(&self) -> std::io::Result<u64> {
match std::fs::metadata(self.path(FileId::Wal)) {
Ok(m) => Ok(m.len()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
Err(e) => Err(e),
}
}
fn read_range(&self, file: FileId, from: u64) -> std::io::Result<Vec<u8>> {
use std::io::{Read as _, Seek as _, SeekFrom};
let mut f = match File::open(self.path(file)) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
};
let len = f.metadata()?.len();
if from >= len {
return Ok(Vec::new());
}
f.seek(SeekFrom::Start(from))?;
let mut buf = Vec::with_capacity((len - from) as usize);
f.read_to_end(&mut buf)?;
Ok(buf)
}
fn snapshot_ident(&self) -> std::io::Result<Option<(u64, u64)>> {
let m = match std::fs::metadata(self.path(FileId::Snapshot)) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
let mtime_nanos = m
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
Ok(Some((m.len(), mtime_nanos)))
}
fn list_archives(&self) -> std::io::Result<Vec<u64>> {
let mut ns = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
let name = entry.file_name();
let s = name.to_string_lossy();
if let Some(mid) = s
.strip_prefix("wal.")
.and_then(|r| r.strip_suffix(".archive"))
{
if let Ok(n) = mid.parse::<u64>() {
ns.push(n);
}
}
}
ns.sort_unstable();
Ok(ns)
}
fn read_archive(&self, n: u64) -> std::io::Result<Vec<u8>> {
let path = self.dir.join(format!("wal.{n}.archive"));
match std::fs::read(&path) {
Ok(b) => Ok(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
Err(e) => Err(e),
}
}
fn archive_wal(&mut self, n: u64) -> std::io::Result<()> {
let wal_path = self.path(FileId::Wal);
let archive_path = self.dir.join(format!("wal.{n}.archive"));
std::fs::rename(&wal_path, &archive_path)?;
sync_dir(&self.dir)
}
fn delete_archive(&mut self, n: u64) -> std::io::Result<()> {
let path = self.dir.join(format!("wal.{n}.archive"));
match std::fs::remove_file(&path) {
Ok(()) => sync_dir(&self.dir),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
fn read_horizon_floor(&self) -> std::io::Result<u64> {
let path = self.dir.join("wal.floor");
match std::fs::read(&path) {
Ok(b) if b.len() >= 8 => Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
])),
Ok(_) => Ok(0),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
Err(e) => Err(e),
}
}
fn write_horizon_floor(&mut self, floor: u64) -> std::io::Result<()> {
let tmp = self.dir.join("wal.floor.tmp");
{
let mut f = File::create(&tmp)?;
f.write_all(&floor.to_le_bytes())?;
full_sync(&f)?;
}
std::fs::rename(&tmp, self.dir.join("wal.floor"))?;
sync_dir(&self.dir)
}
fn has_genesis_marker(&self) -> bool {
self.dir.join("wal.genesis").exists()
}
fn write_genesis_marker(&mut self) -> std::io::Result<()> {
let path = self.dir.join("wal.genesis");
{
let mut f = File::create(&path)?;
f.write_all(b"")?;
full_sync(&f)?;
}
sync_dir(&self.dir)
}
fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
match std::fs::remove_file(self.dir.join("wal.genesis")) {
Ok(()) => sync_dir(&self.dir),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
}
fn full_sync(file: &File) -> std::io::Result<()> {
#[cfg(target_os = "macos")]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
if rc == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
{
file.sync_all()
}
}
pub fn sync_wal_at(dir: &std::path::Path) -> std::io::Result<()> {
let path = dir.join(FileId::Wal.name());
let f = match std::fs::File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
full_sync(&f)
}
pub fn truncate_wal_at(dir: &std::path::Path, len: u64) -> std::io::Result<()> {
let path = dir.join(FileId::Wal.name());
let f = match OpenOptions::new().write(true).open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
f.set_len(len)?;
f.sync_all() }
fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
let d = File::open(dir)?;
d.sync_all()
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
#[test]
fn append_read_and_atomic_write() {
let mut fs = RealFs::new(&tmp()).unwrap();
assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
fs.append(FileId::Wal, b"cd").unwrap();
fs.sync(FileId::Wal).unwrap();
assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
}
#[test]
fn write_atomic_replaces_and_still_readable() {
let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
let mut fs = RealFs::new(&d).unwrap();
fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
}
}