use std::collections::HashMap;
use std::ffi::OsString;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TxId(pub u64);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DbIdentity(OsString);
impl DbIdentity {
pub fn new(canonical_path: impl Into<OsString>) -> Self {
Self(canonical_path.into())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TxOrigin {
Database(DbIdentity),
Memory,
Unscoped,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TxOriginFilter {
Main(DbIdentity),
Secondary(DbIdentity),
}
impl TxOriginFilter {
fn matches(&self, origin: &TxOrigin) -> bool {
match (self, origin) {
(TxOriginFilter::Main(id), TxOrigin::Database(origin_id)) => origin_id == id,
(TxOriginFilter::Main(_), TxOrigin::Unscoped) => true,
(TxOriginFilter::Main(_), TxOrigin::Memory) => false,
(TxOriginFilter::Secondary(id), TxOrigin::Database(origin_id)) => origin_id == id,
(TxOriginFilter::Secondary(_), TxOrigin::Unscoped | TxOrigin::Memory) => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OldestSpan {
pub id: TxId,
pub age: Duration,
pub label: Option<String>,
pub origin: TxOrigin,
}
#[derive(Clone, Debug)]
struct TxMeta {
opened_at: Instant,
label: Option<String>,
origin: TxOrigin,
}
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
static REGISTRY: LazyLock<Mutex<HashMap<TxId, TxMeta>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub struct TxHandle {
id: TxId,
}
impl Drop for TxHandle {
fn drop(&mut self) {
let mut registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry.remove(&self.id);
}
}
pub fn register_scoped(label: Option<String>, origin: TxOrigin) -> TxHandle {
let id = TxId(NEXT_ID.fetch_add(1, Ordering::Relaxed));
let meta = TxMeta {
opened_at: Instant::now(),
label,
origin,
};
let mut registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry.insert(id, meta);
drop(registry);
TxHandle { id }
}
pub fn register(label: Option<String>) -> TxHandle {
register_scoped(label, TxOrigin::Unscoped)
}
pub fn oldest() -> Option<(TxId, Duration, Option<String>)> {
let registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry
.iter()
.min_by_key(|(_, meta)| meta.opened_at)
.map(|(id, meta)| (*id, meta.opened_at.elapsed(), meta.label.clone()))
}
pub fn oldest_for(filter: &TxOriginFilter) -> Option<OldestSpan> {
let registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry
.iter()
.filter(|(_, meta)| filter.matches(&meta.origin))
.min_by_key(|(_, meta)| meta.opened_at)
.map(|(id, meta)| OldestSpan {
id: *id,
age: meta.opened_at.elapsed(),
label: meta.label.clone(),
origin: meta.origin.clone(),
})
}
pub fn any_open_labeled(filter: &TxOriginFilter, label: &str) -> bool {
let registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry
.values()
.any(|meta| filter.matches(&meta.origin) && meta.label.as_deref() == Some(label))
}
pub fn snapshot() -> Vec<(Duration, Option<String>)> {
let registry = REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry
.values()
.map(|meta| (meta.opened_at.elapsed(), meta.label.clone()))
.collect()
}
#[cfg(test)]
#[path = "tx_registry/tests.rs"]
mod tests;