use std::{
io::Write,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{Duration, Instant},
};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
const WRITER_STALL_LIMIT: Duration = Duration::from_secs(120);
#[derive(Debug)]
pub(super) struct WriteGate<T> {
in_flight_since: Option<Instant>,
queued: Option<T>,
}
impl<T> WriteGate<T> {
pub(super) const fn new() -> Self {
Self {
in_flight_since: None,
queued: None,
}
}
pub(super) fn admit(&mut self, request: T, now: Instant) -> Option<T> {
let busy = self
.in_flight_since
.is_some_and(|since| now.saturating_duration_since(since) < WRITER_STALL_LIMIT);
if busy {
self.queued = Some(request);
return None;
}
self.in_flight_since = Some(now);
Some(request)
}
pub(super) fn release(&mut self, now: Instant) -> Option<T> {
let next = self.queued.take();
self.in_flight_since = next.as_ref().map(|_| now);
next
}
}
pub(super) fn write_file_atomically(path: &Path, text: &str) -> Result<(), String> {
let staged = temp_sibling(path);
if let Err(e) = write_synced(&staged, text) {
let _ = std::fs::remove_file(&staged);
return Err(format!(
"Failed to write {}: {e}. Nothing was saved.",
path.display()
));
}
if let Err(message) = backup_existing(path) {
let _ = std::fs::remove_file(&staged);
return Err(message);
}
if let Err(e) = std::fs::rename(&staged, path) {
let _ = std::fs::remove_file(&staged);
return Err(format!(
"Failed to finalize {}: {e}. Nothing was saved.",
path.display()
));
}
Ok(())
}
pub(super) fn temp_sibling(path: &Path) -> PathBuf {
let suffix = format!(
".{}.{}.tmp",
std::process::id(),
TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let file_name = path.file_name().map_or_else(
|| std::ffi::OsString::from(format!("save{suffix}")),
|n| {
let mut s = n.to_os_string();
s.push(&suffix);
s
},
);
path.with_file_name(file_name)
}
pub(super) fn write_synced(path: &Path, contents: &str) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
file.write_all(contents.as_bytes())?;
file.sync_all()
}
fn backup_sibling(path: &Path) -> PathBuf {
let file_name = path.file_name().map_or_else(
|| std::ffi::OsString::from("save.bak"),
|n| {
let mut s = n.to_os_string();
s.push(".bak");
s
},
);
path.with_file_name(file_name)
}
fn backup_existing(path: &Path) -> Result<(), String> {
if !path.is_file() {
return Ok(());
}
let backup = backup_sibling(path);
std::fs::copy(path, &backup).map_err(|e| {
format!(
"Failed to back up {} to {} before overwriting it: {e}. Nothing was saved.",
path.display(),
backup.display()
)
})?;
Ok(())
}