use std::collections::BTreeMap;
use std::sync::Arc;
use crate::sync::{Condvar, Mutex};
use crate::env::Env;
pub(crate) struct SnapshotRegistry {
active: Mutex<BTreeMap<u64, SlotState>>,
drained: Condvar,
env: Arc<dyn Env>,
}
#[derive(Debug, Clone, Copy)]
struct SlotState {
refcount: usize,
registered_at_unix: Option<u64>,
}
impl SnapshotRegistry {
pub(crate) fn with_env(env: Arc<dyn Env>) -> Self {
Self {
active: Mutex::new(BTreeMap::new()),
drained: Condvar::new(),
env,
}
}
#[cfg(test)]
pub(crate) fn new() -> Self {
Self::with_env(crate::env::std_env())
}
pub(crate) fn register(&self, seq: u64) {
let now = self.env.unix_secs();
let mut active = self.active.lock();
active
.entry(seq)
.or_insert(SlotState {
refcount: 0,
registered_at_unix: now,
})
.refcount += 1;
}
pub(crate) fn register_at(&self, sample: impl FnOnce() -> u64) -> u64 {
let now = self.env.unix_secs();
let mut active = self.active.lock();
let seq = sample();
active
.entry(seq)
.or_insert(SlotState {
refcount: 0,
registered_at_unix: now,
})
.refcount += 1;
seq
}
pub(crate) fn release(&self, seq: u64) {
let mut active = self.active.lock();
if let Some(slot) = active.get_mut(&seq) {
slot.refcount -= 1;
if slot.refcount == 0 {
active.remove(&seq);
}
}
let empty = active.is_empty();
drop(active);
if empty {
self.drained.notify_all();
}
}
pub(crate) fn wait_until_drained(&self, timeout: std::time::Duration) -> u64 {
let deadline = std::time::Instant::now() + timeout;
let mut active = self.active.lock();
while !active.is_empty() {
let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now()) else {
break;
};
let (next, _) = self
.drained
.wait_timeout(active, remaining)
.unwrap_or_else(std::sync::PoisonError::into_inner);
active = next;
}
active.values().map(|slot| slot.refcount as u64).sum()
}
pub(crate) fn oldest_live_seq(&self) -> u64 {
self.active
.lock()
.keys()
.next()
.copied()
.unwrap_or(u64::MAX)
}
pub(crate) fn live_count(&self) -> u64 {
self.active
.lock()
.values()
.map(|slot| slot.refcount as u64)
.sum()
}
pub(crate) fn oldest_snapshot_time_unix(&self) -> Option<u64> {
self.active
.lock()
.values()
.next()
.and_then(|slot| slot.registered_at_unix)
}
#[cfg(test)]
pub(crate) fn pin_count(&self) -> usize {
self.active.lock().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_release_refcounts_correctly() {
let r = SnapshotRegistry::new();
assert_eq!(r.oldest_live_seq(), u64::MAX);
assert_eq!(r.pin_count(), 0);
r.register(10);
r.register(10);
r.register(5);
r.register(20);
assert_eq!(r.oldest_live_seq(), 5);
assert_eq!(r.pin_count(), 3);
r.release(5);
assert_eq!(r.oldest_live_seq(), 10);
assert_eq!(r.pin_count(), 2);
r.release(10);
assert_eq!(r.oldest_live_seq(), 10);
assert_eq!(r.pin_count(), 2);
r.release(10);
assert_eq!(r.oldest_live_seq(), 20);
assert_eq!(r.pin_count(), 1);
r.release(20);
assert_eq!(r.oldest_live_seq(), u64::MAX);
assert_eq!(r.pin_count(), 0);
}
#[test]
fn release_unknown_seq_is_noop() {
let r = SnapshotRegistry::new();
r.release(42);
assert_eq!(r.oldest_live_seq(), u64::MAX);
}
#[test]
fn live_count_tracks_refcount_total_not_distinct_seqs() {
let r = SnapshotRegistry::new();
r.register(5);
r.register(5);
r.register(9);
assert_eq!(r.live_count(), 3);
assert_eq!(r.pin_count(), 2);
r.release(5);
assert_eq!(r.live_count(), 2);
}
#[test]
fn oldest_snapshot_time_is_none_when_empty_and_some_when_pinned() {
let r = SnapshotRegistry::new();
assert!(r.oldest_snapshot_time_unix().is_none());
r.register(7);
assert!(r.oldest_snapshot_time_unix().is_some());
r.release(7);
assert!(r.oldest_snapshot_time_unix().is_none());
}
#[test]
fn concurrent_register_release_stays_consistent() {
use std::sync::Arc;
use std::thread;
let r = Arc::new(SnapshotRegistry::new());
let mut handles = Vec::new();
for worker in 0..4u64 {
let r = Arc::clone(&r);
handles.push(thread::spawn(move || {
for i in 0..200u64 {
let seq = worker * 200 + i + 1;
r.register(seq);
r.release(seq);
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(r.pin_count(), 0);
assert_eq!(r.live_count(), 0);
assert_eq!(r.oldest_live_seq(), u64::MAX);
}
}