use super::document::{Navigable, TextDocument};
use super::files::{Standing, StoreFiles, Wrote, has_no_keys, standing_of};
use super::store::diff_documents;
use crate::errors::StorageError;
use crate::store::backend::utils;
use crate::store::facts::StoreFile as StoreFileFact;
use crate::store::{StorageResult, StoreEvent, SubscriptionEntry, WhenItWillNotRead};
use amethystate_core::path::StorePath;
use parking_lot::{Mutex, RwLock};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tracing::warn;
#[derive(Default)]
pub(crate) struct Touched {
at: std::collections::HashSet<StorePath>,
under: Vec<StorePath>,
named: std::collections::BTreeSet<StorePath>,
}
impl Touched {
fn absorb(&mut self, other: Touched) {
self.at.extend(other.at);
self.under.extend(other.under);
self.named.extend(other.named);
}
}
#[derive(Default)]
pub(crate) struct Standoff {
held: AtomicU64,
merged: AtomicU64,
touched: Mutex<Touched>,
left: Mutex<Option<Standing>>,
saving: Mutex<()>,
unreadable_since: Mutex<Option<std::time::Instant>>,
meta_owed: AtomicBool,
}
impl Standoff {
pub(super) fn owe_the_meta(&self) {
self.meta_owed.store(true, Ordering::Release);
}
pub(super) fn hold(&self) {
self.held.fetch_add(1, Ordering::Release);
}
pub(super) fn wrote(&self, at: &StorePath, named: &StorePath) {
let mut touched = self.touched.lock();
touched.at.insert(at.clone());
touched.named.insert(named.clone());
}
pub(super) fn swept(&self, under: &StorePath) {
let mut touched = self.touched.lock();
touched.under.push(under.clone());
touched.named.insert(under.clone());
}
pub(super) fn unsaved(&self) -> Vec<StorePath> {
self.touched.lock().named.iter().cloned().collect()
}
fn left(&self) -> Option<Standing> {
*self.left.lock()
}
fn unreadable_for(&self) -> std::time::Duration {
let mut since = self.unreadable_since.lock();
since.get_or_insert_with(std::time::Instant::now).elapsed()
}
fn reads_again(&self) {
*self.unreadable_since.lock() = None;
}
fn holding(&self, file: &Path) -> bool {
if self.held.load(Ordering::Acquire) != self.merged.load(Ordering::Acquire) {
return true;
}
match (self.left(), standing_of(file)) {
(Some(left), Some(now)) => left != now,
(None, Some(_)) => true,
(_, None) => false,
}
}
fn taking(&self) -> Touched {
std::mem::take(&mut *self.touched.lock())
}
fn put_back(&self, mine: Touched) {
self.touched.lock().absorb(mine);
}
fn left_it(&self, standing: Option<Standing>) {
*self.left.lock() = standing;
}
}
const SAVES: usize = 3;
pub(super) fn save<D: TextDocument>(
files: &StoreFiles<D>,
subscriptions: &RwLock<Vec<SubscriptionEntry>>,
writes: &AtomicU64,
persisted: &AtomicU64,
standoff: &Standoff,
settled: &AtomicU64,
will_not_read: WhenItWillNotRead,
commits: &crate::store::durable::CommitSignal,
) -> StorageResult<()> {
let _one_at_a_time = standoff.saving.lock();
let flush = commits.begin();
let meta_owed = standoff.meta_owed.swap(false, Ordering::AcqRel);
let saved = save_in_turn(
files,
subscriptions,
writes,
persisted,
standoff,
settled,
will_not_read,
meta_owed,
);
if saved.is_err() && meta_owed {
standoff.owe_the_meta();
}
commits.settle(flush, &saved);
saved
}
#[allow(clippy::too_many_arguments)]
fn save_in_turn<D: TextDocument>(
files: &StoreFiles<D>,
subscriptions: &RwLock<Vec<SubscriptionEntry>>,
writes: &AtomicU64,
persisted: &AtomicU64,
standoff: &Standoff,
settled: &AtomicU64,
will_not_read: WhenItWillNotRead,
meta_owed: bool,
) -> StorageResult<()> {
if !meta_owed
&& writes.load(Ordering::Acquire) == persisted.load(Ordering::Acquire)
&& standoff.unsaved().is_empty()
{
return Ok(());
}
for _ in 0..SAVES {
let saving = writes.load(Ordering::Acquire);
let laid = standoff.held.load(Ordering::Acquire);
let mine = standoff.taking();
let mut unmoved = standoff.left();
if standoff.holding(&files.data.path) {
let laid_over = lay_over_the_file(
files,
&mine,
settled,
writes,
saving,
will_not_read,
standoff,
);
let (brought, read_at) = match laid_over {
Laid::Took { brought, read_at } => (brought, read_at),
Laid::Raced => {
standoff.put_back(mine);
continue;
}
Laid::LeaveItAlone => {
standoff.put_back(mine);
standoff.hold();
return Err(error_stack::Report::new(StorageError::Flush)
.attach(StoreFileFact(files.data.path.clone()))
.attach(
"the file will not read and this store was told to leave one alone, \
so nothing was written: what is held is still held, and the next \
save tries again",
));
}
};
unmoved = read_at;
for event in brought {
if let Err(refused) = utils::emit_events(subscriptions, event) {
warn!(
file = %files.data.path.display(),
"an edit made outside was taken into this save and somebody could not \
read it back, and there is nobody to tell: the edit came from the \
file, not from a caller. {refused:?}"
);
}
}
}
match files.persist_while(unmoved) {
Ok(Wrote::Replaced(standing)) => {
persisted.store(saving, Ordering::Release);
standoff.merged.store(laid, Ordering::Release);
standoff.left_it(standing);
return Ok(());
}
Ok(Wrote::FileMoved) => {
standoff.put_back(mine);
standoff.hold();
}
Err(why) => {
standoff.put_back(mine);
return Err(why);
}
}
}
Err(error_stack::Report::new(StorageError::Flush)
.attach(StoreFileFact(files.data.path.clone()))
.attach(
"three times over, this save read the file and found the ground moved before it \
could replace it - either somebody else wrote the file, or a write of ours \
landed in the gap - so nothing was written",
))
}
fn what_to_do_about(
file: &Path,
rule: WhenItWillNotRead,
why: error_stack::Report<StorageError>,
) -> Laid {
match rule {
WhenItWillNotRead::TryAgainFor(_) | WhenItWillNotRead::Refuse => {
warn!(
file = %file.display(),
"the file will not read, so nothing was written: what this store holds stays \
in memory until the file parses again. {why:?}"
);
Laid::LeaveItAlone
}
WhenItWillNotRead::SetAside => {
let aside = file.with_extension(match file.extension() {
Some(had) => format!("{}.unreadable", had.to_string_lossy()),
None => "unreadable".to_string(),
});
match std::fs::rename(file, &aside) {
Ok(()) => {
warn!(
file = %file.display(),
aside = %aside.display(),
"the file will not read and was moved aside, so this save could go \
ahead: what was typed into it is still there under that name. {why:?}"
);
Laid::Took {
brought: Vec::new(),
read_at: None,
}
}
Err(io) => {
warn!(
file = %file.display(),
aside = %aside.display(),
error = %io,
"the file will not read and would not move aside either, so nothing \
was written rather than written over"
);
Laid::LeaveItAlone
}
}
}
WhenItWillNotRead::Overwrite => {
warn!(
file = %file.display(),
"the file will not read, so this save writes the document whole and what was \
in the file is gone. {why:?}"
);
Laid::Took {
brought: Vec::new(),
read_at: None,
}
}
}
}
fn plane_under<D: TextDocument>(doc: &D, prefix: &StorePath) -> Vec<StorePath> {
let Some(root) = doc.get(&StorePath::root()) else {
return Vec::new();
};
root.child_names()
.into_iter()
.filter(|name| {
StorePath::parse_joined(name.as_str())
.is_ok_and(|spelled| spelled.starts_with(prefix) && spelled != *prefix)
})
.map(StorePath::segment)
.collect()
}
enum Laid {
Took {
brought: Vec<StoreEvent>,
read_at: Option<Standing>,
},
Raced,
LeaveItAlone,
}
fn lay_over_the_file<D: TextDocument>(
files: &StoreFiles<D>,
mine: &Touched,
settled: &AtomicU64,
writes: &AtomicU64,
saving: u64,
will_not_read: WhenItWillNotRead,
standoff: &Standoff,
) -> Laid {
let refuse = |why: &str| {
warn!(
file = %files.data.path.display(),
"the file was edited outside while this store held writes of its own, and {why}, \
so this save writes the document whole and what was in the file is gone"
);
Laid::Took {
brought: Vec::new(),
read_at: None,
}
};
let read_at = standing_of(&files.data.path);
let on_disk = match files.data.load_or_empty() {
Ok(on_disk) => {
standoff.reads_again();
on_disk
}
Err(why) => {
let rule = match will_not_read {
WhenItWillNotRead::TryAgainFor(window) if standoff.unreadable_for() < window => {
WhenItWillNotRead::Refuse
}
WhenItWillNotRead::TryAgainFor(_) => WhenItWillNotRead::SetAside,
settled => settled,
};
return what_to_do_about(&files.data.path, rule, why);
}
};
let mut guard = files.data.doc.write();
if writes.load(Ordering::Acquire) != saving {
return Laid::Raced;
}
if has_no_keys(&on_disk) && !has_no_keys(&*guard) {
return refuse("it came back holding nothing where this store holds keys");
}
let before = guard.clone();
let mut merged = on_disk;
for under in &mine.under {
if let Err(why) = merged.delete_subtree(under) {
return refuse(&format!(
"a level this store swept would not come off it: {why:?}"
));
}
for beside in plane_under(&merged, under) {
if let Err(why) = merged.delete(&beside) {
return refuse(&format!(
"a key beside a level this store swept would not come off it: {why:?}"
));
}
}
}
for at in &mine.at {
let laid = match guard.get(at) {
Some(node) => merged.set(at, node.clone()),
None => merged.delete(at).map(|_| ()),
};
if let Err(why) = laid {
return refuse(&format!(
"a place this store wrote would not go back on it: {why:?}"
));
}
}
*guard = merged;
let at = settled.fetch_add(1, Ordering::AcqRel) + 1;
let brought = match diff_documents::<D>(&before, &guard, at) {
Ok(events) => events,
Err(why) => {
warn!(
file = %files.data.path.display(),
"an edit made outside was taken into this save and could not be read, so \
nobody was told about it: {why:?}"
);
Vec::new()
}
};
Laid::Took { brought, read_at }
}