use crate::epoch::Epoch;
use parking_lot::Mutex;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Default)]
pub struct ActiveSpills {
inner: Mutex<HashSet<u64>>,
}
impl ActiveSpills {
pub fn new() -> Self {
Self {
inner: Mutex::new(HashSet::new()),
}
}
pub fn register(self: &Arc<Self>, txn_id: u64) -> SpillGuard {
self.inner.lock().insert(txn_id);
SpillGuard {
registry: Arc::clone(self),
txn_id,
}
}
pub fn is_active(&self, txn_id: u64) -> bool {
self.inner.lock().contains(&txn_id)
}
pub fn is_idle(&self) -> bool {
self.inner.lock().is_empty()
}
fn release(&self, txn_id: u64) {
self.inner.lock().remove(&txn_id);
}
}
pub struct SpillGuard {
registry: Arc<ActiveSpills>,
txn_id: u64,
}
impl Drop for SpillGuard {
fn drop(&mut self) {
self.registry.release(self.txn_id);
}
}
#[derive(Default)]
pub struct SnapshotRegistry {
live: Mutex<BTreeMap<u64, u64>>,
history_epochs: AtomicU64,
history_start: AtomicU64,
}
impl SnapshotRegistry {
pub fn new() -> Self {
Self {
live: Mutex::new(BTreeMap::new()),
history_epochs: AtomicU64::new(0),
history_start: AtomicU64::new(0),
}
}
pub fn configure_history(&self, epochs: u64, start_epoch: Epoch) {
self.history_start.store(start_epoch.0, Ordering::Release);
self.history_epochs.store(epochs, Ordering::Release);
}
pub fn history_config(&self) -> (u64, Epoch) {
(
self.history_epochs.load(Ordering::Acquire),
Epoch(self.history_start.load(Ordering::Acquire)),
)
}
pub fn history_floor(&self, visible: Epoch) -> Option<Epoch> {
let (epochs, start) = self.history_config();
(epochs > 0).then(|| Epoch(start.0.max(visible.0.saturating_sub(epochs))))
}
pub fn register(&self, epoch: Epoch) -> SnapshotGuard<'_> {
let mut live = self.live.lock();
*live.entry(epoch.0).or_insert(0) += 1;
SnapshotGuard {
registry: self,
epoch,
}
}
pub fn min_active(&self, visible: Epoch) -> Epoch {
match self.live.lock().keys().next().copied() {
Some(min) => Epoch(min),
None => visible,
}
}
pub fn min_pinned(&self) -> Option<Epoch> {
self.live.lock().keys().next().copied().map(Epoch)
}
fn release(&self, epoch: Epoch) {
let mut live = self.live.lock();
if let Some(count) = live.get_mut(&epoch.0) {
*count -= 1;
if *count == 0 {
live.remove(&epoch.0);
}
}
}
}
pub struct SnapshotGuard<'r> {
registry: &'r SnapshotRegistry,
epoch: Epoch,
}
impl Drop for SnapshotGuard<'_> {
fn drop(&mut self) {
self.registry.release(self.epoch);
}
}
pub struct OwnedSnapshotGuard {
registry: Arc<SnapshotRegistry>,
epoch: Epoch,
}
impl OwnedSnapshotGuard {
pub fn epoch(&self) -> Epoch {
self.epoch
}
}
impl Drop for OwnedSnapshotGuard {
fn drop(&mut self) {
self.registry.release(self.epoch);
}
}
impl SnapshotRegistry {
pub fn register_owned(self: &Arc<Self>, epoch: Epoch) -> OwnedSnapshotGuard {
{
let mut live = self.live.lock();
*live.entry(epoch.0).or_insert(0) += 1;
}
OwnedSnapshotGuard {
registry: Arc::clone(self),
epoch,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PinSource {
TransactionSnapshot,
HistoryRetention,
BackupPitr,
Replication,
ReadGeneration,
OnlineIndexBuild,
}
impl PinSource {
pub const ALL: [PinSource; 6] = [
PinSource::TransactionSnapshot,
PinSource::HistoryRetention,
PinSource::BackupPitr,
PinSource::Replication,
PinSource::ReadGeneration,
PinSource::OnlineIndexBuild,
];
pub fn label(self) -> &'static str {
match self {
PinSource::TransactionSnapshot => "transaction_snapshot",
PinSource::HistoryRetention => "history_retention",
PinSource::BackupPitr => "backup_pitr",
PinSource::Replication => "replication",
PinSource::ReadGeneration => "read_generation",
PinSource::OnlineIndexBuild => "online_index_build",
}
}
}
#[derive(Debug, Clone)]
pub struct PinInfo {
pub source: PinSource,
pub oldest_epoch: Epoch,
pub held_since: Option<Instant>,
pub pin_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct PinsReport {
pub pins: Vec<PinInfo>,
}
impl PinsReport {
pub fn oldest_epoch(&self) -> Option<Epoch> {
self.pins.iter().map(|pin| pin.oldest_epoch).min()
}
pub fn get(&self, source: PinSource) -> Option<&PinInfo> {
self.pins.iter().find(|pin| pin.source == source)
}
pub fn is_empty(&self) -> bool {
self.pins.is_empty()
}
pub fn len(&self) -> usize {
self.pins.len()
}
pub fn record_projection(&mut self, source: PinSource, epoch: Epoch) {
match self.pins.iter_mut().find(|pin| pin.source == source) {
Some(info) => info.oldest_epoch = info.oldest_epoch.min(epoch),
None => self.pins.push(PinInfo {
source,
oldest_epoch: epoch,
held_since: None,
pin_count: 0,
}),
}
}
}
struct PinEntry {
source: PinSource,
epoch: Epoch,
held_since: Instant,
}
#[derive(Default)]
pub struct PinRegistry {
pins: Mutex<BTreeMap<u64, PinEntry>>,
next_id: AtomicU64,
}
impl PinRegistry {
pub fn new() -> Self {
Self {
pins: Mutex::new(BTreeMap::new()),
next_id: AtomicU64::new(1),
}
}
pub fn pin(self: &Arc<Self>, source: PinSource, epoch: Epoch) -> PinGuard {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.pins.lock().insert(
id,
PinEntry {
source,
epoch,
held_since: Instant::now(),
},
);
PinGuard {
registry: Arc::clone(self),
id,
source,
epoch,
}
}
pub fn oldest_pinned(&self) -> Option<Epoch> {
self.pins.lock().values().map(|entry| entry.epoch).min()
}
pub fn oldest_for(&self, source: PinSource) -> Option<Epoch> {
self.pins
.lock()
.values()
.filter(|entry| entry.source == source)
.map(|entry| entry.epoch)
.min()
}
pub fn report(&self) -> PinsReport {
let pins = self.pins.lock();
let mut by_source: BTreeMap<PinSource, PinInfo> = BTreeMap::new();
for entry in pins.values() {
by_source
.entry(entry.source)
.and_modify(|info| {
info.oldest_epoch = info.oldest_epoch.min(entry.epoch);
info.held_since = match info.held_since {
Some(since) => Some(since.min(entry.held_since)),
None => Some(entry.held_since),
};
info.pin_count += 1;
})
.or_insert(PinInfo {
source: entry.source,
oldest_epoch: entry.epoch,
held_since: Some(entry.held_since),
pin_count: 1,
});
}
PinsReport {
pins: by_source.into_values().collect(),
}
}
fn release(&self, id: u64) {
self.pins.lock().remove(&id);
}
}
pub struct PinGuard {
registry: Arc<PinRegistry>,
id: u64,
source: PinSource,
epoch: Epoch,
}
impl PinGuard {
pub fn source(&self) -> PinSource {
self.source
}
pub fn epoch(&self) -> Epoch {
self.epoch
}
}
impl Drop for PinGuard {
fn drop(&mut self) {
self.registry.release(self.id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retention_tracks_min_active_snapshot() {
let r = SnapshotRegistry::new();
assert_eq!(r.min_active(Epoch(10)), Epoch(10));
let g1 = r.register(Epoch(5));
let g2 = r.register(Epoch(8));
assert_eq!(r.min_active(Epoch(10)), Epoch(5));
drop(g1);
assert_eq!(r.min_active(Epoch(10)), Epoch(8));
drop(g2);
assert_eq!(r.min_active(Epoch(10)), Epoch(10));
}
#[test]
fn retention_refcounts_duplicate_epochs() {
let r = SnapshotRegistry::new();
let a = r.register(Epoch(3));
let b = r.register(Epoch(3));
assert_eq!(r.min_active(Epoch(9)), Epoch(3));
drop(a);
assert_eq!(r.min_active(Epoch(9)), Epoch(3));
drop(b);
assert_eq!(r.min_active(Epoch(9)), Epoch(9));
}
#[test]
fn pin_registry_tracks_oldest_epoch_per_source() {
let registry = Arc::new(PinRegistry::new());
assert_eq!(registry.oldest_pinned(), None);
let backup = registry.pin(PinSource::BackupPitr, Epoch(7));
let replication = registry.pin(PinSource::Replication, Epoch(4));
assert_eq!(registry.oldest_pinned(), Some(Epoch(4)));
assert_eq!(registry.oldest_for(PinSource::BackupPitr), Some(Epoch(7)));
assert_eq!(registry.oldest_for(PinSource::ReadGeneration), None);
drop(replication);
assert_eq!(registry.oldest_pinned(), Some(Epoch(7)));
drop(backup);
assert_eq!(registry.oldest_pinned(), None);
}
#[test]
fn pin_registry_report_lists_every_active_source_once() {
let registry = Arc::new(PinRegistry::new());
let mut guards = Vec::new();
for (offset, source) in PinSource::ALL.into_iter().enumerate() {
guards.push(registry.pin(source, Epoch(offset as u64 + 2)));
}
guards.push(registry.pin(PinSource::BackupPitr, Epoch(50)));
let report = registry.report();
assert_eq!(report.len(), PinSource::ALL.len());
for (offset, source) in PinSource::ALL.into_iter().enumerate() {
let info = report.get(source).expect("source listed");
assert_eq!(info.oldest_epoch, Epoch(offset as u64 + 2));
assert!(
info.held_since.is_some(),
"registered pins carry a timestamp"
);
}
assert_eq!(report.get(PinSource::BackupPitr).unwrap().pin_count, 2);
assert_eq!(report.oldest_epoch(), Some(Epoch(2)));
drop(guards);
assert!(registry.report().is_empty());
}
#[test]
fn pins_report_projection_only_lowers_the_floor() {
let registry = Arc::new(PinRegistry::new());
let guard = registry.pin(PinSource::ReadGeneration, Epoch(9));
let mut report = registry.report();
report.record_projection(PinSource::ReadGeneration, Epoch(4));
report.record_projection(PinSource::ReadGeneration, Epoch(12));
report.record_projection(PinSource::TransactionSnapshot, Epoch(6));
let info = report.get(PinSource::ReadGeneration).unwrap();
assert_eq!(info.oldest_epoch, Epoch(4));
assert_eq!(info.pin_count, 1, "projection is not a registered guard");
assert!(info.held_since.is_some());
let projected = report.get(PinSource::TransactionSnapshot).unwrap();
assert_eq!(projected.oldest_epoch, Epoch(6));
assert_eq!(projected.pin_count, 0);
assert!(projected.held_since.is_none());
assert_eq!(report.oldest_epoch(), Some(Epoch(4)));
drop(guard);
}
}