mod conformance;
pub use conformance::verify_version_reclamation;
use std::collections::BTreeMap;
use std::sync::{Arc, Weak};
use parking_lot::Mutex;
use uqa_core::memory::MemoryReservation;
use super::{CommitSequence, VersionError, VersionResult};
use crate::read_control::StorageReadControl;
pub trait SnapshotLeaseTransport: Send + Sync {
fn acquire_admission(&self, control: &StorageReadControl) -> VersionResult<()>;
fn release_admission(&self);
fn retain(
&self,
sequence: CommitSequence,
control: &StorageReadControl,
) -> VersionResult<Box<dyn Send + Sync>>;
fn oldest(&self, control: &StorageReadControl) -> VersionResult<Option<CommitSequence>>;
}
struct Admission<'a>(Option<&'a dyn SnapshotLeaseTransport>);
impl Drop for Admission<'_> {
fn drop(&mut self) {
if let Some(transport) = self.0 {
transport.release_admission();
}
}
}
#[derive(Default)]
pub struct SnapshotRegistry {
leases: Mutex<BTreeMap<CommitSequence, SnapshotLeaseEntry>>,
transport: Option<Arc<dyn SnapshotLeaseTransport>>,
}
struct SnapshotLeaseEntry {
lease: Weak<SnapshotLease>,
_memory: MemoryReservation,
}
pub struct SnapshotLease {
sequence: CommitSequence,
_native: Option<Box<dyn Send + Sync>>,
registry: Arc<SnapshotRegistry>,
}
impl Drop for SnapshotLease {
fn drop(&mut self) {
if let Some(mut leases) = self.registry.leases.try_lock() {
if leases
.get(&self.sequence)
.is_some_and(|entry| entry.lease.as_ptr() == std::ptr::from_ref(self))
{
leases.remove(&self.sequence);
}
}
}
}
impl SnapshotLease {
pub fn sequence(&self) -> CommitSequence {
self.sequence
}
}
impl SnapshotRegistry {
pub fn with_transport(transport: Arc<dyn SnapshotLeaseTransport>) -> Self {
Self {
leases: Mutex::new(BTreeMap::new()),
transport: Some(transport),
}
}
fn admitted<T>(
&self,
control: &StorageReadControl,
operation: impl FnOnce(&mut BTreeMap<CommitSequence, SnapshotLeaseEntry>) -> VersionResult<T>,
) -> VersionResult<T> {
let mut leases = loop {
control.cancellation().check()?;
if let Some(leases) = self.leases.try_lock() {
break leases;
}
std::thread::sleep(std::time::Duration::from_millis(1));
};
if let Some(transport) = self.transport.as_deref() {
transport.acquire_admission(control)?;
}
let _admission = Admission(self.transport.as_deref());
leases.retain(|_, entry| entry.lease.strong_count() != 0);
operation(&mut leases)
}
pub fn capture(
self: &Arc<Self>,
control: &StorageReadControl,
read_sequence: impl FnOnce() -> VersionResult<CommitSequence>,
) -> VersionResult<Arc<SnapshotLease>> {
self.admitted(control, |leases| {
let sequence = read_sequence()?;
if let Some(lease) = leases
.get(&sequence)
.and_then(|entry| entry.lease.upgrade())
{
return Ok(lease);
}
let memory = control.memory().reserve(
std::mem::size_of::<SnapshotLease>()
+ 2 * std::mem::size_of::<usize>()
+ std::mem::size_of::<(CommitSequence, SnapshotLeaseEntry)>(),
)?;
let native = self
.transport
.as_deref()
.map(|transport| transport.retain(sequence, control))
.transpose()?;
let lease = Arc::new(SnapshotLease {
sequence,
_native: native,
registry: Arc::clone(self),
});
leases.insert(
sequence,
SnapshotLeaseEntry {
lease: Arc::downgrade(&lease),
_memory: memory,
},
);
Ok(lease)
})
}
pub fn reclaim<T>(
&self,
control: &StorageReadControl,
remove_versions: impl FnOnce(Option<CommitSequence>) -> VersionResult<T>,
) -> VersionResult<T> {
self.admitted(control, |leases| {
let local = leases.keys().next().copied();
let remote = self
.transport
.as_deref()
.map(|transport| transport.oldest(control))
.transpose()?
.flatten();
let oldest = local.into_iter().chain(remote).min();
remove_versions(oldest)
})
}
}
#[derive(Clone, Copy)]
pub struct ReclamationHorizon(CommitSequence);
impl ReclamationHorizon {
pub fn new(current: CommitSequence, oldest: Option<CommitSequence>) -> VersionResult<Self> {
if oldest.is_some_and(|oldest| oldest > current) {
return Err(VersionError::InvalidEncoding(
"snapshot lease exceeds the committed sequence",
));
}
Ok(Self(oldest.unwrap_or(current)))
}
pub fn sequence(self) -> CommitSequence {
self.0
}
pub fn anchor(self, revision: CommitSequence) -> VersionResult<CommitSequence> {
if revision == CommitSequence::INITIAL || revision > self.0 {
return Err(VersionError::InvalidEncoding(
"reclamation anchor is outside the retained horizon",
));
}
Ok(revision)
}
}
#[cfg(test)]
mod tests;