use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use super::sync::normalised_parent;
#[derive(Clone, Debug)]
enum EntryKind {
CreateFile,
CreateDir,
ReplaceInstall(Vec<u8>),
}
#[derive(Clone, Debug)]
struct PendingEntry {
child: OsString,
generation: u64,
kind: EntryKind,
}
#[derive(Default)]
struct Journal {
pending: BTreeMap<PathBuf, Vec<PendingEntry>>,
generations: BTreeMap<(PathBuf, OsString), u64>,
sync_counts: BTreeMap<PathBuf, usize>,
lossy: bool,
fail_next: bool,
}
thread_local! {
static JOURNAL: RefCell<Journal> = RefCell::new(Journal::default());
}
fn with_journal<R>(function: impl FnOnce(&mut Journal) -> R) -> R {
JOURNAL.with(|cell| function(&mut cell.borrow_mut()))
}
#[must_use = "a fence reservation must be committed or cancelled"]
pub struct Reservation {
parent: PathBuf,
child: OsString,
generation: u64,
kind: EntryKind,
resolved: bool,
}
impl Reservation {
pub fn commit(mut self) {
let entry = PendingEntry {
child: std::mem::take(&mut self.child),
generation: self.generation,
kind: std::mem::replace(&mut self.kind, EntryKind::CreateFile),
};
let parent = self.parent.clone();
with_journal(|journal| journal.pending.entry(parent).or_default().push(entry));
self.resolved = true;
}
pub fn cancel(mut self) {
self.resolved = true;
}
}
impl Drop for Reservation {
fn drop(&mut self) {
assert!(
self.resolved,
"fence reservation for {}/{} dropped without commit or cancel",
self.parent.display(),
Path::new(&self.child).display(),
);
}
}
fn reserve(parent: &Path, child: OsString, kind: EntryKind) -> Reservation {
let parent = normalised_parent(parent);
let generation = with_journal(|journal| {
let slot = journal
.generations
.entry((parent.clone(), child.clone()))
.or_insert(0);
*slot = slot.saturating_add(1);
*slot
});
Reservation {
parent,
child,
generation,
kind,
resolved: false,
}
}
fn child_of(target: &Path) -> OsString {
target
.file_name()
.map_or_else(|| OsString::from(target), OsString::from)
}
pub fn reserve_create_file(target: &Path) -> Reservation {
let parent = target.parent().unwrap_or_else(|| Path::new("."));
reserve(parent, child_of(target), EntryKind::CreateFile)
}
pub fn reserve_create_dir(target: &Path) -> Reservation {
let parent = target.parent().unwrap_or_else(|| Path::new("."));
reserve(parent, child_of(target), EntryKind::CreateDir)
}
pub fn reserve_install(target: &Path) -> Reservation {
let parent = target.parent().unwrap_or_else(|| Path::new("."));
let kind = match std::fs::read(target) {
Ok(bytes) => EntryKind::ReplaceInstall(bytes),
Err(error) if error.kind() == io::ErrorKind::NotFound => EntryKind::CreateFile,
Err(_other) => EntryKind::ReplaceInstall(Vec::new()),
};
reserve(parent, child_of(target), kind)
}
pub(super) fn record_observed_sync(parent: &Path) {
with_journal(|journal| *journal.sync_counts.entry(parent.to_path_buf()).or_insert(0) += 1);
}
pub(super) fn take_failpoint() -> bool {
with_journal(|journal| std::mem::replace(&mut journal.fail_next, false))
}
pub(super) fn is_lossy() -> bool {
with_journal(|journal| journal.lossy)
}
pub(super) fn snapshot_pending_generations(parent: &Path) -> Vec<u64> {
with_journal(|journal| {
journal
.pending
.get(parent)
.map(|entries| entries.iter().map(|entry| entry.generation).collect())
.unwrap_or_default()
})
}
pub(super) fn clear_pending_generations(parent: &Path, snapshot: &[u64]) {
with_journal(|journal| {
if let Some(entries) = journal.pending.get_mut(parent) {
entries.retain(|entry| !snapshot.contains(&entry.generation));
if entries.is_empty() {
journal.pending.remove(parent);
}
}
});
}
pub fn arm_failpoint() {
with_journal(|journal| journal.fail_next = true);
}
pub fn set_lossy(lossy: bool) {
with_journal(|journal| journal.lossy = lossy);
}
pub fn sync_count(parent: &Path) -> usize {
let parent = normalised_parent(parent);
with_journal(|journal| journal.sync_counts.get(&parent).copied().unwrap_or(0))
}
pub fn total_sync_count() -> usize {
with_journal(|journal| journal.sync_counts.values().copied().sum())
}
pub fn reset_observer() {
with_journal(|journal| journal.sync_counts.clear());
}
pub fn pending_len() -> usize {
with_journal(|journal| journal.pending.values().map(Vec::len).sum())
}
pub fn reset() {
with_journal(|journal| *journal = Journal::default());
}
pub struct TestGuard;
impl Drop for TestGuard {
fn drop(&mut self) {
reset();
}
}
pub fn test_guard() -> TestGuard {
reset();
TestGuard
}
pub fn cut() -> io::Result<()> {
let pending = with_journal(|journal| std::mem::take(&mut journal.pending));
for (parent, mut entries) in pending {
entries.sort_by(|left, right| right.generation.cmp(&left.generation));
for entry in entries {
let target = parent.join(&entry.child);
match entry.kind {
EntryKind::CreateFile => remove_file_idempotent(&target)?,
EntryKind::CreateDir => remove_dir_idempotent(&target)?,
EntryKind::ReplaceInstall(bytes) => std::fs::write(&target, &bytes)?,
}
}
}
Ok(())
}
fn remove_file_idempotent(target: &Path) -> io::Result<()> {
match std::fs::remove_file(target) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
fn remove_dir_idempotent(target: &Path) -> io::Result<()> {
match std::fs::remove_dir_all(target) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}