use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use crate::graph::dir_graph::DirGraph;
use crate::graph::handle::make_dir_graph_mut_preserving_lineage;
use crate::graph::io::file::save_graph;
use crate::graph::io::open::{GraphFileIdentity, GraphWriterLease, LeaseRefusal};
pub const LAZY_LEASE_ACQUIRE_TIMEOUT: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BeginWrite {
Held,
Acquired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Discarded {
pub restored: bool,
}
#[derive(Debug)]
pub enum WriteRefusal {
Contended(LeaseRefusal),
Stale { path: PathBuf },
Io(io::Error),
}
impl std::fmt::Display for WriteRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Contended(refusal) => write!(f, "{}", refusal.error),
Self::Stale { path } => {
write!(f, "{} changed on disk since it was loaded", path.display())
}
Self::Io(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for WriteRefusal {}
impl WriteRefusal {
fn from_lease(refusal: LeaseRefusal) -> Self {
match refusal.holder {
Some(_) => Self::Contended(refusal),
None => Self::Io(refusal.error),
}
}
}
pub struct WriteOwnership {
path: PathBuf,
lease: Option<GraphWriterLease>,
synced: GraphFileIdentity,
baseline: u64,
high_water: u64,
pristine: Option<Arc<DirGraph>>,
label: Option<String>,
keep_pristine: bool,
pinned_lease: bool,
}
impl WriteOwnership {
pub fn new(
path: PathBuf,
identity: GraphFileIdentity,
graph: &DirGraph,
label: Option<String>,
keep_pristine: bool,
) -> Self {
let version = graph.version();
Self {
path,
lease: None,
synced: identity,
baseline: version,
high_water: version,
pristine: None,
label,
keep_pristine,
pinned_lease: false,
}
}
pub fn adopt_lease(&mut self, lease: GraphWriterLease, graph: &Arc<DirGraph>, pinned: bool) {
self.lease = Some(lease);
self.pinned_lease = pinned;
if self.keep_pristine && self.pristine.is_none() {
self.pristine = Some(Arc::clone(graph));
}
self.note_version(graph);
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn synced(&self) -> &GraphFileIdentity {
&self.synced
}
pub fn holds_lease(&self) -> bool {
self.lease.is_some()
}
pub fn is_dirty(&self, graph: &DirGraph) -> bool {
graph.version() != self.baseline
}
pub fn note_version(&mut self, graph: &DirGraph) {
self.high_water = self.high_water.max(graph.version());
}
pub fn begin_write(&mut self, graph: &mut Arc<DirGraph>) -> Result<BeginWrite, WriteRefusal> {
if self.lease.is_some() {
self.note_version(graph);
return Ok(BeginWrite::Held);
}
let lease = GraphWriterLease::acquire_labeled(
&self.path,
LAZY_LEASE_ACQUIRE_TIMEOUT,
self.label.as_deref(),
)
.map_err(WriteRefusal::from_lease)?;
let identity = GraphFileIdentity::capture(&self.path).map_err(WriteRefusal::Io)?;
if identity != self.synced {
drop(lease);
return Err(WriteRefusal::Stale {
path: self.path.clone(),
});
}
if self.keep_pristine {
self.pristine = Some(Arc::clone(graph));
}
self.lease = Some(lease);
self.note_version(graph);
Ok(BeginWrite::Acquired)
}
pub fn publish(&mut self, graph: &mut Arc<DirGraph>) -> Result<(), WriteRefusal> {
if self.lease.is_none() {
let lease = GraphWriterLease::acquire_labeled(
&self.path,
LAZY_LEASE_ACQUIRE_TIMEOUT,
self.label.as_deref(),
)
.map_err(WriteRefusal::from_lease)?;
self.lease = Some(lease);
}
let identity = GraphFileIdentity::capture(&self.path).map_err(WriteRefusal::Io)?;
if identity != self.synced {
if !self.is_dirty(graph) && !self.pinned_lease {
self.lease = None;
}
return Err(WriteRefusal::Stale {
path: self.path.clone(),
});
}
save_graph(graph, &self.path.to_string_lossy())
.map_err(|error| WriteRefusal::Io(io::Error::other(error)))?;
self.synced = GraphFileIdentity::capture(&self.path).map_err(WriteRefusal::Io)?;
self.baseline = graph.version();
self.note_version(graph);
self.pristine = None;
if !self.pinned_lease {
self.lease = None;
}
Ok(())
}
pub fn discard(&mut self, graph: &mut Arc<DirGraph>) -> Discarded {
self.note_version(graph);
let restored = match self.pristine.take() {
Some(pristine) => {
*graph = pristine;
let cleared = self.high_water.saturating_add(1);
make_dir_graph_mut_preserving_lineage(graph).set_version(cleared);
self.baseline = graph.version();
self.high_water = self.baseline;
true
}
None => false,
};
self.lease = None;
self.pinned_lease = false;
Discarded { restored }
}
pub fn resynced(&mut self, identity: GraphFileIdentity, graph: &DirGraph) {
self.synced = identity;
self.baseline = graph.version();
self.pristine = None;
self.note_version(graph);
}
pub fn retarget(&mut self, new_path: PathBuf, identity: GraphFileIdentity) {
self.lease = None;
self.pinned_lease = false;
self.path = new_path;
self.synced = identity;
}
}
#[cfg(test)]
#[path = "write_ownership_tests.rs"]
mod write_ownership_tests;