use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Barrier(u64);
impl Barrier {
#[must_use]
pub const fn new(seq: u64) -> Self {
Self(seq)
}
#[must_use]
pub const fn seq(self) -> u64 {
self.0
}
}
impl std::fmt::Display for Barrier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{}", self.0)
}
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum SyncKind {
File,
Data,
Directory,
}
impl SyncKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
SyncKind::File => "file",
SyncKind::Data => "data",
SyncKind::Directory => "directory",
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyncRecord {
pub path: PathBuf,
pub kind: SyncKind,
pub at: Barrier,
pub durable_len: Option<u64>,
pub snapshot: Option<PathBuf>,
}
impl SyncRecord {
#[must_use]
pub fn new(
path: impl Into<PathBuf>,
kind: SyncKind,
at: Barrier,
durable_len: Option<u64>,
) -> Self {
Self {
path: path.into(),
kind,
at,
durable_len,
snapshot: None,
}
}
#[must_use]
pub fn with_snapshot(mut self, snapshot: impl Into<PathBuf>) -> Self {
self.snapshot = Some(snapshot.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssumedDurable {
pub path: PathBuf,
pub reason: String,
}
impl AssumedDurable {
#[must_use]
pub fn new(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self {
path: path.into(),
reason: reason.into(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Coverage {
Complete,
Partial {
unobserved: String,
},
}
impl Coverage {
#[must_use]
pub fn partial(unobserved: impl Into<String>) -> Self {
Coverage::Partial {
unobserved: unobserved.into(),
}
}
#[must_use]
pub fn is_complete(&self) -> bool {
matches!(self, Coverage::Complete)
}
}
pub trait DurabilityWitness {
fn synced(&self) -> Vec<SyncRecord>;
fn coverage(&self) -> Coverage;
fn assumed(&self) -> Vec<AssumedDurable> {
Vec::new()
}
}
impl<W: DurabilityWitness + ?Sized> DurabilityWitness for &W {
fn synced(&self) -> Vec<SyncRecord> {
(**self).synced()
}
fn coverage(&self) -> Coverage {
(**self).coverage()
}
fn assumed(&self) -> Vec<AssumedDurable> {
(**self).assumed()
}
}
#[derive(Debug, Default)]
struct JournalInner {
records: Vec<SyncRecord>,
assumed: Vec<AssumedDurable>,
snapshots: Option<crate::ScratchDir>,
}
impl JournalInner {
fn snapshot(&mut self, seq: u64, path: &Path, len: Option<u64>) -> Option<PathBuf> {
if self.snapshots.is_none() {
self.snapshots = crate::ScratchDir::new("synced-content").ok();
}
let dir = self.snapshots.as_ref()?;
let dst = dir.path().join(format!("{seq:012}"));
crate::scratch::copy_file(path, &dst, len).ok()?;
Some(dst)
}
}
#[derive(Clone, Debug)]
pub struct SyncJournal {
inner: Arc<Mutex<JournalInner>>,
next: Arc<AtomicU64>,
coverage: Coverage,
}
impl SyncJournal {
#[must_use]
pub fn complete() -> Self {
Self::with_coverage(Coverage::Complete)
}
#[must_use]
pub fn partial(unobserved: impl Into<String>) -> Self {
Self::with_coverage(Coverage::partial(unobserved))
}
#[must_use]
pub fn with_coverage(coverage: Coverage) -> Self {
Self {
inner: Arc::new(Mutex::new(JournalInner::default())),
next: Arc::new(AtomicU64::new(1)),
coverage,
}
}
fn next_barrier(&self) -> Barrier {
Barrier(self.next.fetch_add(1, Ordering::SeqCst))
}
fn push(&self, record: SyncRecord) {
let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
guard.records.push(record);
}
pub fn record(
&self,
path: impl AsRef<Path>,
kind: SyncKind,
durable_len: Option<u64>,
) -> Barrier {
let at = self.next_barrier();
let path = canonical(path.as_ref());
let mut record = SyncRecord::new(path.clone(), kind, at, durable_len);
if kind != SyncKind::Directory {
let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if let Some(snapshot) = guard.snapshot(at.seq(), &path, durable_len) {
record.snapshot = Some(snapshot);
}
guard.records.push(record);
return at;
}
self.push(record);
at
}
pub fn sync_all(&self, file: &File, path: impl AsRef<Path>) -> io::Result<Barrier> {
file.sync_all()?;
let len = file.metadata().ok().map(|m| m.len());
Ok(self.record(path, SyncKind::File, len))
}
pub fn sync_data(&self, file: &File, path: impl AsRef<Path>) -> io::Result<Barrier> {
file.sync_data()?;
let len = file.metadata().ok().map(|m| m.len());
Ok(self.record(path, SyncKind::Data, len))
}
pub fn sync_path(&self, path: impl AsRef<Path>) -> io::Result<Barrier> {
let path = path.as_ref();
let file = OpenOptions::new().read(true).write(true).open(path)?;
self.sync_all(&file, path)
}
pub fn sync_path_if_present(&self, path: impl AsRef<Path>) -> io::Result<Option<Barrier>> {
match self.sync_path(path) {
Ok(barrier) => Ok(Some(barrier)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn sync_dir(&self, dir: impl AsRef<Path>) -> io::Result<Option<Barrier>> {
let dir = dir.as_ref();
#[cfg(unix)]
{
File::open(dir)?.sync_all()?;
Ok(Some(self.record(dir, SyncKind::Directory, None)))
}
#[cfg(not(unix))]
{
let _ = dir;
Ok(None)
}
}
pub fn assume(&self, path: impl AsRef<Path>, reason: impl Into<String>) {
let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
guard
.assumed
.push(AssumedDurable::new(canonical(path.as_ref()), reason));
}
}
impl DurabilityWitness for SyncJournal {
fn synced(&self) -> Vec<SyncRecord> {
let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
guard.records.clone()
}
fn coverage(&self) -> Coverage {
self.coverage.clone()
}
fn assumed(&self) -> Vec<AssumedDurable> {
let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
guard.assumed.clone()
}
}
pub(crate) fn canonical(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_failed_sync_records_nothing() {
let journal = SyncJournal::complete();
let err = journal.sync_path("/definitely/not/a/real/path/app.db");
assert!(err.is_err());
assert!(
journal.synced().is_empty(),
"the journal must mean `this is on stable storage`, never `we tried`"
);
}
#[test]
fn barriers_are_monotonic_across_clones() {
let journal = SyncJournal::complete();
let clone = journal.clone();
let a = journal.record("a", SyncKind::File, None);
let b = clone.record("b", SyncKind::File, None);
assert!(a < b);
assert_eq!(journal.synced().len(), 2, "clones share one journal");
}
}