#![cfg(all(feature = "comms", any(unix, windows)))]
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::store::CommsStoreError;
const FATAL_FILE: &str = "last-fatal.json";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FatalStoreError {
pub epoch_secs: u64,
pub pid: u32,
pub version: String,
pub request: String,
pub error: String,
}
pub fn fatal_path(comms_dir: &Path) -> PathBuf {
comms_dir.join(FATAL_FILE)
}
pub fn is_fatal(error: &CommsStoreError) -> bool {
matches!(
error,
CommsStoreError::Fjall(
fjall::Error::Poisoned
| fjall::Error::Unrecoverable
| fjall::Error::Storage(fjall::LsmError::Unrecoverable)
)
)
}
pub fn persist(comms_dir: &Path, request: &str, error: &CommsStoreError) {
let path = fatal_path(comms_dir);
if path.exists() {
return;
}
let record = FatalStoreError {
epoch_secs: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
pid: std::process::id(),
version: env!("CARGO_PKG_VERSION").to_string(),
request: request.to_string(),
error: error.to_string(),
};
if let Ok(bytes) = serde_json::to_vec_pretty(&record) {
let _ = std::fs::write(&path, bytes);
}
}
pub fn read(comms_dir: &Path) -> Option<FatalStoreError> {
let bytes = std::fs::read(fatal_path(comms_dir)).ok()?;
serde_json::from_slice(&bytes).ok()
}
pub fn clear(comms_dir: &Path) -> bool {
std::fs::remove_file(fatal_path(comms_dir)).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn poisoned() -> CommsStoreError {
CommsStoreError::Fjall(fjall::Error::Poisoned)
}
#[test]
fn only_permanent_fjall_states_are_fatal() {
assert!(is_fatal(&poisoned()));
assert!(is_fatal(&CommsStoreError::Fjall(fjall::Error::Unrecoverable)));
assert!(
is_fatal(&CommsStoreError::Fjall(fjall::Error::Storage(
fjall::LsmError::Unrecoverable
))),
"an unrecoverable LSM tree reaches us wrapped in Storage; it must not slip past"
);
assert!(
!is_fatal(&CommsStoreError::Fjall(fjall::Error::Storage(
fjall::LsmError::InvalidTrailer
))),
"block-scoped corruption does not prove the whole store is unusable"
);
assert!(
!is_fatal(&CommsStoreError::Limit("threads")),
"a retention ceiling is a per-request refusal, not a broken store"
);
assert!(
!is_fatal(&CommsStoreError::Locked(PathBuf::from("/tmp/x"))),
"a lost startup race converges; it must not tear the winner down"
);
assert!(
!is_fatal(&CommsStoreError::Io {
path: PathBuf::from("/tmp/x"),
source: std::io::Error::other("transient"),
}),
"an io error on one path does not prove the store is unusable"
);
}
#[test]
fn persist_is_write_once_and_round_trips() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(read(dir.path()), None, "no record before anything fails");
persist(dir.path(), "ThreadPost", &poisoned());
let first = read(dir.path()).expect("record written");
assert_eq!(first.request, "ThreadPost");
assert!(
first.error.contains("fjall"),
"the fjall variant is named: {}",
first.error
);
assert_eq!(first.pid, std::process::id());
persist(
dir.path(),
"ThreadHistory",
&CommsStoreError::Fjall(fjall::Error::Unrecoverable),
);
assert_eq!(
read(dir.path()).expect("record still there"),
first,
"the FIRST error explains the wedge; later ones must not overwrite it"
);
}
#[test]
fn clear_removes_the_record_and_reports_whether_it_did() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(!clear(dir.path()), "clearing nothing reports nothing removed");
persist(dir.path(), "Ping", &poisoned());
assert!(clear(dir.path()), "an existing record is removed");
assert_eq!(read(dir.path()), None);
}
}