use super::document::TextDocument;
use super::files::{StoreFile, has_no_keys};
use super::store::diff_documents;
use crate::store::StoreEvent;
use crate::store::SubscriptionEntry;
use crate::store::backend::utils;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use tracing::{info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Standing {
Settled,
Unsaved,
Raced,
}
pub(super) fn standing(writes: &AtomicU64, persisted: &AtomicU64, read_after: u64) -> Standing {
let written = writes.load(Ordering::Acquire);
let saved = persisted.load(Ordering::Acquire);
if written != saved {
Standing::Unsaved
} else if saved != read_after {
Standing::Raced
} else {
Standing::Settled
}
}
#[derive(Debug)]
pub(super) enum Taken {
Applied(Vec<StoreEvent>),
Same,
Held(Standing),
Unreadable,
}
pub(super) fn look<D: TextDocument>(
file: &StoreFile<D>,
writes: &AtomicU64,
persisted: &AtomicU64,
settled: &AtomicU64,
) -> Taken {
let read_after = persisted.load(Ordering::Acquire);
let Ok(content) = std::fs::read_to_string(&file.path) else {
return Taken::Unreadable;
};
if file.wrote_exactly(&content) {
return Taken::Same;
}
let Ok(on_disk) = D::parse(&content) else {
return Taken::Unreadable;
};
let mut guard = file.doc.write();
match standing(writes, persisted, read_after) {
Standing::Settled => {}
held => return Taken::Held(held),
}
if has_no_keys(&on_disk) && !has_no_keys(&*guard) {
warn!(
file = %file.path.display(),
"the file came back holding nothing where the store holds keys, so it was left \
alone: a file is read as empty only when the store agrees it is"
);
return Taken::Unreadable;
}
let held = guard.serialize().unwrap_or_default();
let found = on_disk.serialize().unwrap_or_default();
if held == found {
return Taken::Same;
}
let before = guard.clone();
*guard = on_disk;
file.agrees_with(&content);
info!("external store change detected");
let at = settled.fetch_add(1, Ordering::AcqRel) + 1;
match diff_documents::<D>(&before, &guard, at) {
Ok(events) => Taken::Applied(events),
Err(e) => {
warn!("an external edit could not be read, so nobody was told about it: {e:?}");
Taken::Applied(Vec::new())
}
}
}
const RETRIES: usize = 3;
pub(super) fn take_outside_edit<D: TextDocument>(
file: &StoreFile<D>,
subscriptions: &RwLock<Vec<SubscriptionEntry>>,
writes: &AtomicU64,
persisted: &AtomicU64,
standoff: &super::standoff::Standoff,
settled: &AtomicU64,
) {
for _ in 0..RETRIES {
match look(file, writes, persisted, settled) {
Taken::Applied(events) => {
for event in events {
if let Err(refused) = utils::emit_found_events(subscriptions, event) {
warn!(
file = %file.path.display(),
"an outside edit was taken and somebody could not read it back, and \
there is nobody to tell: the edit came from the file, not from a \
caller. {refused:?}"
);
}
}
return;
}
Taken::Same => return,
Taken::Unreadable => {
standoff.hold();
return;
}
Taken::Held(Standing::Raced) => continue,
Taken::Held(Standing::Unsaved) => {
standoff.hold();
info!(
file = %file.path.display(),
"the file was edited outside while this store held writes it had not saved, \
so the edit stays in the file: the next save lays this store's own paths \
over what is there rather than replacing it"
);
return;
}
Taken::Held(Standing::Settled) => return,
}
}
warn!(
file = %file.path.display(),
"an outside edit lost the race with this store's own saving three times over, so it was \
not taken; the next change to the file brings it round again"
);
}
pub(super) struct Coalescing {
quiet: Duration,
waiting: Mutex<Waiting>,
woken: Condvar,
}
struct Waiting {
until: Instant,
stopped: bool,
opened: bool,
}
impl Coalescing {
pub(super) fn new(quiet: Duration) -> Arc<Self> {
Arc::new(Self {
quiet,
waiting: Mutex::new(Waiting {
until: Instant::now(),
stopped: false,
opened: false,
}),
woken: Condvar::new(),
})
}
pub(super) fn opened(&self) {
let mut waiting = self.waiting.lock().unwrap_or_else(|e| e.into_inner());
waiting.opened = true;
self.woken.notify_all();
}
pub(super) fn has_opened(&self) -> bool {
self.waiting
.lock()
.unwrap_or_else(|e| e.into_inner())
.opened
}
pub(super) fn stop(&self) {
let mut waiting = self.waiting.lock().unwrap_or_else(|e| e.into_inner());
waiting.stopped = true;
self.woken.notify_all();
}
pub(super) fn settle(&self) -> bool {
let mut waiting = self.waiting.lock().unwrap_or_else(|e| e.into_inner());
while !waiting.opened && !waiting.stopped {
waiting = self.woken.wait(waiting).unwrap_or_else(|e| e.into_inner());
}
if waiting.stopped {
return false;
}
waiting.until = Instant::now() + self.quiet;
loop {
let now = Instant::now();
if waiting.stopped {
return false;
}
if now >= waiting.until {
return true;
}
let left = waiting.until - now;
waiting = self
.woken
.wait_timeout(waiting, left)
.unwrap_or_else(|e| e.into_inner())
.0;
}
}
}
pub(super) fn heard(
res: notify::Result<notify::Event>,
file: &std::path::Path,
) -> Option<notify::Event> {
match res {
Ok(event) => Some(event),
Err(why) => {
warn!(
file = %file.display(),
error = %why,
"the watcher on the store's file reported a failure, so an edit made from \
outside may go unseen"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::{Coalescing, heard};
use std::path::Path;
use std::time::{Duration, Instant};
use tracing_test::traced_test;
#[test]
#[traced_test]
fn a_failure_the_watcher_reports_is_said_rather_than_dropped() {
let event = heard(
Err(notify::Error::generic("the handle went away")),
Path::new("settings.json"),
);
assert!(event.is_none());
assert!(logs_contain("the handle went away"));
assert!(logs_contain("settings.json"));
}
#[test]
fn a_wait_ends_when_the_store_it_belongs_to_does() {
let settling = Coalescing::new(Duration::from_secs(60));
let stopper = settling.clone();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
stopper.stop();
});
let started = Instant::now();
let settled = settling.settle();
assert!(!settled, "a stopped wait is not a settled one");
assert!(
started.elapsed() < Duration::from_secs(5),
"the wait ran out the whole quiet period after it was stopped: {:?}",
started.elapsed()
);
}
#[test]
fn a_wait_holds_until_the_open_has_written() {
let settling = Coalescing::new(Duration::from_millis(10));
let waiter = settling.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(waiter.settle());
});
assert!(
rx.recv_timeout(Duration::from_millis(200)).is_err(),
"the wait ended while the store was still opening"
);
settling.opened();
assert_eq!(rx.recv_timeout(Duration::from_secs(5)), Ok(true));
}
#[test]
fn a_wait_asked_for_after_the_stop_does_not_begin() {
let settling = Coalescing::new(Duration::from_secs(60));
settling.stop();
let started = Instant::now();
assert!(!settling.settle());
assert!(started.elapsed() < Duration::from_secs(5));
}
#[test]
fn a_wait_nobody_stopped_runs_out_the_quiet_period() {
let settling = Coalescing::new(Duration::from_millis(200));
settling.opened();
let started = Instant::now();
assert!(settling.settle(), "nobody stopped it");
assert!(
started.elapsed() >= Duration::from_millis(200),
"it came back early: {:?}",
started.elapsed()
);
}
}