use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::io::{Error, ErrorKind, Result as IOResult};
use std::mem;
use std::sync::{Arc, Weak,
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}};
use std::time::{Duration, Instant};
use async_channel::{Receiver, Sender, bounded};
use async_lock::RwLock;
use crossbeam_channel::{Receiver as SyncReceiver, Sender as SyncSender, TryRecvError,
unbounded as sync_unbounded};
use dashmap::{DashMap, mapref::entry::Entry};
use futures::{FutureExt, future::{Either, select}};
use parking_lot::Mutex;
use pi_async_rt::rt::{AsyncRuntime, multi_thread::MultiTaskRuntime};
use pi_atom::Atom;
use pi_guid::Guid;
use pi_hash::XHashMap;
use crate::{Binary, KVActionLog};
const NO_DEADLINE: u64 = u64::MAX;
const MAX_TICK: u64 = u64::MAX - 1;
const TTL_SCAN_BATCH_SIZE: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Version {
Upsert(Guid),
Delete(Guid),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableKeyVersion {
pub table: Atom,
pub key: Binary,
pub version: Version,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableKey {
pub table: Atom,
pub key: Binary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VersionConflictKind {
ReadSetVersionMismatch,
TransactionConflict,
}
impl VersionConflictKind {
#[inline]
fn merge(self, other: Self) -> Self {
if matches!(self, Self::ReadSetVersionMismatch)
|| matches!(other, Self::ReadSetVersionMismatch) {
Self::ReadSetVersionMismatch
} else {
Self::TransactionConflict
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableKeyConflict {
pub table: Atom,
pub key: Binary,
pub kind: VersionConflictKind,
}
pub(crate) fn normalize_conflicts(
mut conflicts: Vec<TableKeyConflict>,
) -> Vec<TableKeyConflict> {
conflicts.sort_by(|left, right| {
left
.table
.as_str()
.as_bytes()
.cmp(right.table.as_str().as_bytes())
.then_with(|| left.key.as_ref().cmp(right.key.as_ref()))
});
conflicts.dedup_by(|next, previous| {
if previous.table.as_str().as_bytes() == next.table.as_str().as_bytes()
&& previous.key.as_ref() == next.key.as_ref() {
previous.kind = previous.kind.merge(next.kind);
true
} else {
false
}
});
conflicts
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionSource {
FirstObservation,
CommittedWrite,
}
#[derive(Debug, Clone)]
pub(crate) struct VersionRecord {
pub(crate) version: Version,
pub(crate) source: VersionSource,
pub(crate) revision: u64,
deadline_tick: u64,
generation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FirstObservation {
Inserted(Version),
Occupied(Version),
}
impl VersionRecord {
fn exact_eq(&self, other: &Self) -> bool {
self.version == other.version
&& self.source == other.source
&& self.revision == other.revision
&& self.deadline_tick == other.deadline_tick
&& self.generation == other.generation
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct KeyVersionConfig {
ttl_ticks: Option<u64>,
poll_interval_ticks: u64,
}
impl KeyVersionConfig {
pub(crate) fn new(ttl: Duration, poll_interval: Duration) -> IOResult<Self> {
if ttl.is_zero() {
return Ok(Self {
ttl_ticks: None,
poll_interval_ticks: 0,
});
}
if poll_interval.is_zero() {
return Err(Error::new(ErrorKind::InvalidInput,
"Start database failed, reason: key version TTL is enabled but poll interval is zero"));
}
Ok(Self {
ttl_ticks: Some(duration_to_ticks(ttl)),
poll_interval_ticks: duration_to_ticks(poll_interval),
})
}
fn ttl_ticks(&self) -> Option<u64> {
self.ttl_ticks
}
fn poll_interval_ticks(&self) -> u64 {
self.poll_interval_ticks
}
}
fn duration_to_ticks(duration: Duration) -> u64 {
let millis = duration.as_millis();
if millis == 0 {
1
} else if millis >= MAX_TICK as u128 {
MAX_TICK
} else {
millis as u64
}
}
fn timeout_ticks(ticks: u64) -> usize {
if ticks >= usize::MAX as u64 {
usize::MAX
} else {
ticks as usize
}
}
#[derive(Clone)]
pub(crate) struct KeyVersionRegistry(Arc<InnerKeyVersionRegistry>);
#[cfg(feature = "trace")]
#[derive(Clone, Copy)]
pub(crate) enum KeyVersionApiOperation {
Query,
Prepare,
Commit,
}
#[cfg(feature = "trace")]
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub(crate) struct KeyVersionApiMetricsSnapshot {
pub(crate) query_success: u64,
pub(crate) query_failure: u64,
pub(crate) prepare_success: u64,
pub(crate) prepare_failure: u64,
pub(crate) commit_success: u64,
pub(crate) commit_failure: u64,
}
#[cfg(feature = "trace")]
impl KeyVersionApiMetricsSnapshot {
pub(crate) fn delta_since(self, previous: Self) -> Self {
Self {
query_success: self.query_success.wrapping_sub(previous.query_success),
query_failure: self.query_failure.wrapping_sub(previous.query_failure),
prepare_success: self.prepare_success.wrapping_sub(previous.prepare_success),
prepare_failure: self.prepare_failure.wrapping_sub(previous.prepare_failure),
commit_success: self.commit_success.wrapping_sub(previous.commit_success),
commit_failure: self.commit_failure.wrapping_sub(previous.commit_failure),
}
}
}
#[cfg(feature = "trace")]
pub(crate) struct KeyVersionApiCallGuard<'a> {
success: &'a AtomicU64,
failure: &'a AtomicU64,
completed: bool,
}
#[cfg(feature = "trace")]
impl KeyVersionApiCallGuard<'_> {
pub(crate) fn finish(mut self, success: bool) {
self.completed = true;
if success {
self.success.fetch_add(1, Ordering::Relaxed);
} else {
self.failure.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg(feature = "trace")]
impl Drop for KeyVersionApiCallGuard<'_> {
fn drop(&mut self) {
if !self.completed {
self.failure.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg(feature = "trace")]
#[derive(Default)]
struct KeyVersionApiMetrics {
query_success: AtomicU64,
query_failure: AtomicU64,
prepare_success: AtomicU64,
prepare_failure: AtomicU64,
commit_success: AtomicU64,
commit_failure: AtomicU64,
}
struct InnerKeyVersionRegistry {
tables: DashMap<Atom, KeyVersions>,
ttl_ticks: Option<u64>,
poll_interval_ticks: u64,
origin: Instant,
shutdown_tx: Option<Sender<()>>,
#[cfg(feature = "trace")]
api_metrics: KeyVersionApiMetrics,
}
impl KeyVersionRegistry {
pub(crate) fn new(config: KeyVersionConfig) -> (Self, Option<Receiver<()>>) {
let (shutdown_tx, shutdown_rx) = if config.ttl_ticks().is_some() {
let (tx, rx) = bounded(1);
(Some(tx), Some(rx))
} else {
(None, None)
};
let inner = InnerKeyVersionRegistry {
tables: DashMap::new(),
ttl_ticks: config.ttl_ticks(),
poll_interval_ticks: config.poll_interval_ticks(),
origin: Instant::now(),
shutdown_tx,
#[cfg(feature = "trace")]
api_metrics: KeyVersionApiMetrics::default(),
};
(Self(Arc::new(inner)), shutdown_rx)
}
pub(crate) fn create_table_versions(&self) -> KeyVersions {
KeyVersions::new(Arc::downgrade(&self.0), self.0.ttl_ticks.is_some())
}
pub(crate) fn install(&self, table: Atom, versions: KeyVersions) {
self.0.tables.insert(table, versions);
}
pub(crate) fn remove_exact(&self, table: &Atom, versions: &KeyVersions) {
let _ = self
.0
.tables
.remove_if(table, |_name, current| current.ptr_eq(versions));
}
#[cfg(feature = "trace")]
pub(crate) fn begin_api_call(&self,
operation: KeyVersionApiOperation)
-> KeyVersionApiCallGuard<'_> {
let metrics = &self.0.api_metrics;
let (success, failure) = match operation {
KeyVersionApiOperation::Query => {
(&metrics.query_success, &metrics.query_failure)
},
KeyVersionApiOperation::Prepare => {
(&metrics.prepare_success, &metrics.prepare_failure)
},
KeyVersionApiOperation::Commit => {
(&metrics.commit_success, &metrics.commit_failure)
},
};
KeyVersionApiCallGuard {
success,
failure,
completed: false,
}
}
#[cfg(feature = "trace")]
pub(crate) fn api_metrics_snapshot(&self) -> KeyVersionApiMetricsSnapshot {
let metrics = &self.0.api_metrics;
KeyVersionApiMetricsSnapshot {
query_success: metrics.query_success.load(Ordering::Relaxed),
query_failure: metrics.query_failure.load(Ordering::Relaxed),
prepare_success: metrics.prepare_success.load(Ordering::Relaxed),
prepare_failure: metrics.prepare_failure.load(Ordering::Relaxed),
commit_success: metrics.commit_success.load(Ordering::Relaxed),
commit_failure: metrics.commit_failure.load(Ordering::Relaxed),
}
}
pub(crate) fn clear_records(&self) {
for entry in self.0.tables.iter() {
entry.value().clear_records();
}
}
pub(crate) fn start_ttl_task(&self,
rt: MultiTaskRuntime<()>,
receiver: Option<Receiver<()>>) {
let Some(receiver) = receiver else {
return;
};
let weak = Arc::downgrade(&self.0);
let task_rt = rt.clone();
let _ = rt.spawn(async move {
ttl_loop(task_rt, weak, receiver).await;
});
}
}
async fn ttl_loop(rt: MultiTaskRuntime<()>,
registry: Weak<InnerKeyVersionRegistry>,
receiver: Receiver<()>) {
let Some(initial) = registry.upgrade() else {
return;
};
let interval = initial.poll_interval_ticks;
let ttl = initial.ttl_ticks.unwrap_or(0);
drop(initial);
log::info!(target: "pi_db::key_version_ttl",
"Key version TTL task started, ttl_ms: {}, interval_ms: {}",
ttl,
interval);
let mut round = 0u64;
loop {
let timeout = rt.timeout(timeout_ticks(interval)).fuse();
let shutdown = receiver.recv().fuse();
futures::pin_mut!(timeout, shutdown);
match select(timeout, shutdown).await {
Either::Left((_timeout_result, _shutdown_future)) => (),
Either::Right((_shutdown_result, _timeout_future)) => break,
}
let Some(registry_ref) = registry.upgrade() else {
break;
};
round = round.saturating_add(1);
let statistics = collect_ttl_round(&rt, ®istry_ref, round).await;
drop(registry_ref);
statistics.log();
}
log::info!(target: "pi_db::key_version_ttl",
"Key version TTL task stopped, ttl_ms: {}, interval_ms: {}, completed_rounds: {}",
ttl,
interval,
round);
}
#[derive(Default)]
struct TtlCollectStatistics {
round: u64,
ttl_ticks: u64,
interval_ticks: u64,
elapsed: Duration,
registered_tables: usize,
due_tables: usize,
scanned_tables: usize,
scanned_records: usize,
due_candidates: usize,
removed_records: usize,
removed_first_observations: usize,
removed_committed_writes: usize,
first_observation_blocked: usize,
snapshot_blocked: usize,
stale_candidates: usize,
records_before: usize,
records_after: usize,
batches: usize,
yields: usize,
next_deadline: u64,
}
impl TtlCollectStatistics {
fn log(&self) {
log::info!(target: "pi_db::key_version_ttl",
"Key version TTL round completed, round: {}, ttl_ms: {}, interval_ms: {}, elapsed_ms: {}, registered_tables: {}, due_tables: {}, scanned_tables: {}, scanned_records: {}, due_candidates: {}, removed_records: {}, removed_first_observations: {}, removed_committed_writes: {}, first_observation_blocked: {}, snapshot_blocked: {}, stale_candidates: {}, records_before: {}, records_after: {}, batches: {}, yields: {}, next_deadline_tick: {}",
self.round,
self.ttl_ticks,
self.interval_ticks,
self.elapsed.as_millis(),
self.registered_tables,
self.due_tables,
self.scanned_tables,
self.scanned_records,
self.due_candidates,
self.removed_records,
self.removed_first_observations,
self.removed_committed_writes,
self.first_observation_blocked,
self.snapshot_blocked,
self.stale_candidates,
self.records_before,
self.records_after,
self.batches,
self.yields,
self.next_deadline);
}
}
async fn collect_ttl_round(rt: &MultiTaskRuntime<()>,
registry: &Arc<InnerKeyVersionRegistry>,
round: u64) -> TtlCollectStatistics {
let started = Instant::now();
let now = monotonic_tick(registry.origin);
let mut statistics = TtlCollectStatistics {
round,
ttl_ticks: registry.ttl_ticks.unwrap_or(0),
interval_ticks: registry.poll_interval_ticks,
next_deadline: NO_DEADLINE,
..TtlCollectStatistics::default()
};
let tables: Vec<KeyVersions> = registry
.tables
.iter()
.map(|entry| entry.value().clone())
.collect();
statistics.registered_tables = tables.len();
statistics.records_before = tables.iter().map(KeyVersions::len).sum();
for versions in &tables {
let earliest = versions.0.earliest_deadline.load(Ordering::Acquire);
let blocked_changed = versions.0.has_blocked_expiry.load(Ordering::Acquire)
&& versions.0.lease_epoch.load(Ordering::Acquire)
!= versions.0.blocked_lease_epoch.load(Ordering::Acquire);
if earliest > now && !blocked_changed {
statistics.next_deadline = statistics.next_deadline.min(earliest);
continue;
}
statistics.due_tables += 1;
statistics.scanned_tables += 1;
versions
.collect_expired(rt, now, &mut statistics)
.await;
statistics.next_deadline = statistics
.next_deadline
.min(versions.0.earliest_deadline.load(Ordering::Acquire));
}
statistics.records_after = tables.iter().map(KeyVersions::len).sum();
statistics.elapsed = started.elapsed();
statistics
}
fn monotonic_tick(origin: Instant) -> u64 {
let elapsed = origin.elapsed().as_millis();
if elapsed >= MAX_TICK as u128 {
MAX_TICK
} else {
elapsed as u64
}
}
fn deadline_tick(elapsed: Duration, ttl_ticks: u64) -> u64 {
let elapsed_millis = elapsed.as_millis();
let rounded_millis = if elapsed.subsec_nanos() % 1_000_000 == 0 {
elapsed_millis
} else {
elapsed_millis.saturating_add(1)
};
let base_tick = if rounded_millis >= MAX_TICK as u128 {
MAX_TICK
} else {
rounded_millis as u64
};
base_tick.saturating_add(ttl_ticks).min(MAX_TICK)
}
struct TtlKeyIndex {
sender: SyncSender<Binary>,
receiver: SyncReceiver<Binary>,
}
impl TtlKeyIndex {
fn new() -> Self {
let (sender, receiver) = sync_unbounded();
Self { sender, receiver }
}
fn len(&self) -> usize {
self.receiver.len()
}
fn push(&self, key: Binary) {
if self.sender.try_send(key).is_err() {
unreachable!("TTL key index channel disconnected while its receiver is alive");
}
}
fn push_batch(&self, keys: Vec<Binary>) {
for key in keys {
self.push(key);
}
}
fn take_batch(&self, limit: usize) -> Vec<Binary> {
let mut keys = Vec::with_capacity(limit);
for _ in 0..limit {
match self.receiver.try_recv() {
Ok(key) => keys.push(key),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
unreachable!("TTL key index channel disconnected while its sender is alive");
},
}
}
keys
}
fn clear(&self) {
loop {
match self.receiver.try_recv() {
Ok(_) => (),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
unreachable!("TTL key index channel disconnected while its sender is alive");
},
}
}
}
}
#[cfg(feature = "trace")]
fn estimated_record_memory_bytes(key: &Binary, ttl_enabled: bool) -> u64 {
let map_entry = mem::size_of::<(Binary, VersionRecord)>();
let arc_vec = mem::size_of::<AtomicUsize>()
.saturating_mul(2)
.saturating_add(mem::size_of::<Vec<u8>>())
.saturating_add(key.0.capacity());
let ttl_slot = if ttl_enabled {
mem::size_of::<(Binary, AtomicUsize)>()
} else {
0
};
map_entry
.saturating_add(arc_vec)
.saturating_add(ttl_slot) as u64
}
#[derive(Clone)]
pub(crate) struct KeyVersions(Arc<InnerKeyVersions>);
#[cfg(feature = "trace")]
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub(crate) struct KeyVersionCacheMetricsSnapshot {
pub(crate) record_count: u64,
pub(crate) estimated_memory_bytes: u64,
}
#[cfg(feature = "trace")]
#[derive(Default)]
struct KeyVersionCacheMetrics {
record_count: AtomicU64,
estimated_memory_bytes: AtomicU64,
}
struct InnerKeyVersions {
versions: DashMap<Binary, VersionRecord>,
ttl_keys: TtlKeyIndex,
pub(crate) publication: RwLock<()>,
ttl_enabled: bool,
active_first_observations: AtomicUsize,
completed_revision: AtomicU64,
active_snapshots: Mutex<BTreeMap<u64, usize>>,
lease_epoch: AtomicU64,
earliest_deadline: AtomicU64,
has_blocked_expiry: AtomicBool,
blocked_lease_epoch: AtomicU64,
registry: Weak<InnerKeyVersionRegistry>,
#[cfg(feature = "trace")]
metrics: KeyVersionCacheMetrics,
}
impl KeyVersions {
fn new(registry: Weak<InnerKeyVersionRegistry>, ttl_enabled: bool) -> Self {
Self(Arc::new(InnerKeyVersions {
versions: DashMap::new(),
ttl_keys: TtlKeyIndex::new(),
publication: RwLock::new(()),
ttl_enabled,
active_first_observations: AtomicUsize::new(0),
completed_revision: AtomicU64::new(0),
active_snapshots: Mutex::new(BTreeMap::new()),
lease_epoch: AtomicU64::new(0),
earliest_deadline: AtomicU64::new(NO_DEADLINE),
has_blocked_expiry: AtomicBool::new(false),
blocked_lease_epoch: AtomicU64::new(0),
registry,
#[cfg(feature = "trace")]
metrics: KeyVersionCacheMetrics::default(),
}))
}
pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
pub(crate) fn publication(&self) -> &RwLock<()> {
&self.0.publication
}
pub(crate) fn len(&self) -> usize {
self.0.versions.len()
}
pub(crate) fn current(&self, key: &Binary) -> Option<VersionRecord> {
self.0.versions.get(key).map(|record| record.clone())
}
pub(crate) fn current_version(&self, key: &Binary) -> Option<Version> {
self.0
.versions
.get(key)
.map(|record| record.version.clone())
}
pub(crate) fn lease_first_observation(&self) -> FirstObservationLease<'_> {
if self.0.ttl_enabled {
let mut observed = self.0.active_first_observations.load(Ordering::Acquire);
loop {
let next = observed
.checked_add(1)
.expect("active first-observation lease count overflow");
match self.0.active_first_observations.compare_exchange_weak(
observed,
next,
Ordering::AcqRel,
Ordering::Acquire) {
Ok(_) => break,
Err(current) => observed = current,
}
}
FirstObservationLease {
versions: self,
active: true,
}
} else {
FirstObservationLease {
versions: self,
active: false,
}
}
}
#[inline]
fn has_active_first_observation(&self) -> bool {
self.0.active_first_observations.load(Ordering::Acquire) != 0
}
#[cfg(feature = "trace")]
pub(crate) fn metrics_snapshot(&self) -> KeyVersionCacheMetricsSnapshot {
KeyVersionCacheMetricsSnapshot {
record_count: self.0.metrics.record_count.load(Ordering::Relaxed),
estimated_memory_bytes: self
.0
.metrics
.estimated_memory_bytes
.load(Ordering::Relaxed),
}
}
#[cfg(feature = "trace")]
fn record_inserted(&self, estimated_memory_bytes: u64) {
self.0
.metrics
.estimated_memory_bytes
.fetch_add(estimated_memory_bytes, Ordering::Relaxed);
self.0.metrics.record_count.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "trace")]
fn record_removed(&self, estimated_memory_bytes: u64) {
let old_count = self.0.metrics.record_count.fetch_sub(1, Ordering::Relaxed);
let old_memory = self
.0
.metrics
.estimated_memory_bytes
.fetch_sub(estimated_memory_bytes, Ordering::Relaxed);
debug_assert!(old_count >= 1,
"key version metric record count underflow");
debug_assert!(old_memory >= estimated_memory_bytes,
"key version metric estimated memory underflow");
}
pub(crate) fn first_observation<F>(&self,
key: Binary,
exists: bool,
alloc_uid: F) -> FirstObservation
where F: FnOnce() -> Guid
{
match self.0.versions.entry(key) {
Entry::Occupied(entry) => FirstObservation::Occupied(entry.get().version.clone()),
Entry::Vacant(entry) => {
let version = if exists {
Version::Upsert(alloc_uid())
} else {
Version::Delete(alloc_uid())
};
let deadline = self.next_deadline();
let record = VersionRecord {
version: version.clone(),
source: VersionSource::FirstObservation,
revision: self.0.completed_revision.load(Ordering::Acquire),
deadline_tick: deadline,
generation: 1,
};
let ttl_key = if deadline == NO_DEADLINE {
None
} else {
Some(entry.key().clone())
};
#[cfg(feature = "trace")]
let estimated_memory_bytes = estimated_record_memory_bytes(
entry.key(),
deadline != NO_DEADLINE);
#[cfg(feature = "trace")]
self.record_inserted(estimated_memory_bytes);
drop(entry.insert(record));
if let Some(ttl_key) = ttl_key {
self.0.ttl_keys.push(ttl_key);
}
self.register_deadline(deadline);
FirstObservation::Inserted(version)
},
}
}
pub(crate) fn lease_current(&self) -> SnapshotLease {
let revision = self.0.completed_revision.load(Ordering::Acquire);
let mut active = self.0.active_snapshots.lock();
let count = active.entry(revision).or_insert(0);
*count = count.saturating_add(1);
drop(active);
SnapshotLease {
versions: self.clone(),
revision,
released: AtomicBool::new(false),
}
}
pub(crate) fn completed_revision(&self) -> u64 {
self.0.completed_revision.load(Ordering::Acquire)
}
pub(crate) fn checked_next_revision(&self) -> Option<u64> {
self.completed_revision().checked_add(1)
}
pub(crate) fn complete_revision(&self, revision: u64) {
self.0.completed_revision.store(revision, Ordering::Release);
}
pub(crate) fn has_committed_after(&self,
key: &Binary,
snapshot_revision: u64) -> bool {
if let Some(record) = self.0.versions.get(key) {
record.source == VersionSource::CommittedWrite
&& record.revision > snapshot_revision
} else {
false
}
}
pub(crate) fn publish(&self,
table: Atom,
key: Binary,
value: Option<&Binary>,
transaction_uid: Guid,
revision: u64) -> TableKeyVersion {
let version = if value.is_some() {
Version::Upsert(transaction_uid)
} else {
Version::Delete(transaction_uid)
};
let deadline = self.next_deadline();
let inserted = match self.0.versions.entry(key.clone()) {
Entry::Occupied(mut entry) => {
let generation = entry.get().generation.saturating_add(1);
entry.insert(VersionRecord {
version: version.clone(),
source: VersionSource::CommittedWrite,
revision,
deadline_tick: deadline,
generation,
});
false
},
Entry::Vacant(entry) => {
let record = VersionRecord {
version: version.clone(),
source: VersionSource::CommittedWrite,
revision,
deadline_tick: deadline,
generation: 1,
};
#[cfg(feature = "trace")]
self.record_inserted(estimated_record_memory_bytes(
entry.key(),
deadline != NO_DEADLINE));
drop(entry.insert(record));
true
},
};
if inserted {
if deadline != NO_DEADLINE {
self.0.ttl_keys.push(key.clone());
}
}
self.register_deadline(deadline);
TableKeyVersion {
table,
key,
version,
}
}
pub(crate) fn clear_records(&self) {
debug_assert_eq!(self.0.active_first_observations.load(Ordering::Acquire), 0,
"repair-time version clear must not overlap a first-observation query");
self.0.ttl_keys.clear();
self.0.versions.clear();
#[cfg(feature = "trace")]
{
self.0.metrics.record_count.store(0, Ordering::Relaxed);
self.0
.metrics
.estimated_memory_bytes
.store(0, Ordering::Relaxed);
}
self.0.earliest_deadline.store(NO_DEADLINE, Ordering::Release);
self.0.has_blocked_expiry.store(false, Ordering::Release);
self.0.blocked_lease_epoch.store(
self.0.lease_epoch.load(Ordering::Acquire),
Ordering::Release);
}
fn next_deadline(&self) -> u64 {
let Some(registry) = self.0.registry.upgrade() else {
return NO_DEADLINE;
};
let Some(ttl) = registry.ttl_ticks else {
return NO_DEADLINE;
};
deadline_tick(registry.origin.elapsed(), ttl)
}
fn register_deadline(&self, deadline: u64) {
if deadline != NO_DEADLINE {
self.0.earliest_deadline.fetch_min(deadline, Ordering::AcqRel);
}
}
async fn collect_expired(&self,
rt: &MultiTaskRuntime<()>,
now: u64,
statistics: &mut TtlCollectStatistics) {
self.0.earliest_deadline.swap(NO_DEADLINE, Ordering::AcqRel);
self.0.has_blocked_expiry.store(false, Ordering::Release);
let scan_epoch = self.0.lease_epoch.load(Ordering::Acquire);
let mut remaining = self.0.ttl_keys.len();
let mut blocked = false;
while remaining > 0 {
let batch = self
.0
.ttl_keys
.take_batch(remaining.min(TTL_SCAN_BATCH_SIZE));
if batch.is_empty() {
break;
}
remaining -= batch.len();
statistics.scanned_records += batch.len();
statistics.batches += 1;
let mut retained = Vec::with_capacity(batch.len());
for key in batch {
let Some(candidate) = self.current(&key) else {
continue;
};
if candidate.deadline_tick > now {
self.register_deadline(candidate.deadline_tick);
retained.push(key);
continue;
}
statistics.due_candidates += 1;
if self.has_active_first_observation() {
statistics.first_observation_blocked += 1;
blocked = true;
retained.push(key);
continue;
}
if candidate.source == VersionSource::CommittedWrite {
let min_active = self
.0
.active_snapshots
.lock()
.keys()
.next()
.copied();
if min_active
.map(|revision| candidate.revision > revision)
.unwrap_or(false) {
statistics.snapshot_blocked += 1;
blocked = true;
retained.push(key);
continue;
}
}
let removed = self
.0
.versions
.remove_if(&key, |_key, current| current.exact_eq(&candidate));
if removed.is_some() {
#[cfg(feature = "trace")]
if let Some((removed_key, removed_record)) = removed.as_ref() {
self.record_removed(estimated_record_memory_bytes(
removed_key,
removed_record.deadline_tick != NO_DEADLINE));
}
statistics.removed_records += 1;
match candidate.source {
VersionSource::FirstObservation => {
statistics.removed_first_observations += 1;
},
VersionSource::CommittedWrite => {
statistics.removed_committed_writes += 1;
},
}
} else {
statistics.stale_candidates += 1;
if let Some(current) = self.current(&key) {
if current.deadline_tick > now
|| current.source == VersionSource::FirstObservation {
self.register_deadline(current.deadline_tick);
} else {
let min_active = self
.0
.active_snapshots
.lock()
.keys()
.next()
.copied();
if min_active
.map(|revision| current.revision > revision)
.unwrap_or(false) {
statistics.snapshot_blocked += 1;
blocked = true;
} else {
self.register_deadline(current.deadline_tick);
}
}
retained.push(key);
}
}
}
self.0.ttl_keys.push_batch(retained);
rt.timeout(0).await;
statistics.yields += 1;
}
if blocked {
self.0.blocked_lease_epoch.store(scan_epoch, Ordering::Release);
self.0.has_blocked_expiry.store(true, Ordering::Release);
}
}
}
pub(crate) struct FirstObservationLease<'a> {
versions: &'a KeyVersions,
active: bool,
}
impl Drop for FirstObservationLease<'_> {
fn drop(&mut self) {
if !self.active {
return;
}
let mut observed = self
.versions
.0
.active_first_observations
.load(Ordering::Acquire);
loop {
let next = observed
.checked_sub(1)
.expect("active first-observation lease count underflow");
match self.versions.0.active_first_observations.compare_exchange_weak(
observed,
next,
Ordering::AcqRel,
Ordering::Acquire) {
Ok(_) => break,
Err(current) => observed = current,
}
}
self.versions.0.lease_epoch.fetch_add(1, Ordering::AcqRel);
}
}
pub(crate) struct SnapshotLease {
versions: KeyVersions,
revision: u64,
released: AtomicBool,
}
impl SnapshotLease {
pub(crate) fn revision(&self) -> u64 {
self.revision
}
pub(crate) fn release(&self) {
if self.released.swap(true, Ordering::AcqRel) {
return;
}
let mut active = self.versions.0.active_snapshots.lock();
if let Some(count) = active.get_mut(&self.revision) {
if *count <= 1 {
active.remove(&self.revision);
} else {
*count -= 1;
}
}
drop(active);
self.versions.0.lease_epoch.fetch_add(1, Ordering::AcqRel);
}
}
impl Drop for SnapshotLease {
fn drop(&mut self) {
self.release();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PrepareMode {
Ordinary,
SchemaCreate,
Versioned,
}
pub(crate) struct PreparedActions {
pub(crate) mode: PrepareMode,
pub(crate) actions: XHashMap<Binary, KVActionLog>,
}
pub(crate) type SharedPreparedActions = Arc<PreparedActions>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PreparedCommitError {
Missing,
ModeMismatch(PrepareMode),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PreparedCleanupError {
Missing,
IdentityMismatch,
}
#[inline]
pub(crate) fn has_prepared_transaction<P>(prepare: &XHashMap<Guid, P>,
transaction_uid: &Guid) -> bool {
prepare.contains_key(transaction_uid)
}
#[inline]
pub(crate) fn retain_prepared_for_commit(
prepare: &mut XHashMap<Guid, SharedPreparedActions>,
transaction_uid: &Guid,
expected_mode: PrepareMode,
is_writable: bool,
) -> Result<Option<SharedPreparedActions>, PreparedCommitError> {
match prepare.get(transaction_uid) {
Some(prepared) if prepared.mode == expected_mode => Ok(Some(prepared.clone())),
Some(prepared) => {
let prepared_mode = prepared.mode;
let _ = prepare.remove(transaction_uid);
Err(PreparedCommitError::ModeMismatch(prepared_mode))
},
None if is_writable => Err(PreparedCommitError::Missing),
None => Ok(None),
}
}
#[inline]
pub(crate) fn remove_retained_prepared(
prepare: &mut XHashMap<Guid, SharedPreparedActions>,
transaction_uid: &Guid,
retained: &SharedPreparedActions,
) -> Result<(), PreparedCleanupError> {
match prepare.get(transaction_uid) {
Some(current) if Arc::ptr_eq(current, retained) => {
let _ = prepare.remove(transaction_uid);
Ok(())
},
Some(_) => Err(PreparedCleanupError::IdentityMismatch),
None => Err(PreparedCleanupError::Missing),
}
}
#[derive(Clone)]
pub(crate) struct VersionReceipt(Arc<Mutex<Vec<TableKeyVersion>>>);
impl VersionReceipt {
pub(crate) fn new() -> Self {
Self(Arc::new(Mutex::new(Vec::new())))
}
pub(crate) fn append(&self, mut versions: Vec<TableKeyVersion>) {
if versions.is_empty() {
return;
}
let mut receipt = self.0.lock();
if receipt.is_empty() {
*receipt = versions;
} else {
receipt.append(&mut versions);
}
}
pub(crate) fn take(&self) -> Vec<TableKeyVersion> {
mem::take(&mut *self.0.lock())
}
pub(crate) fn clear(&self) {
self.0.lock().clear();
}
}
pub(crate) struct TableVersionContext {
versions: KeyVersions,
snapshot: SnapshotLease,
mode: PrepareMode,
expected: XHashMap<Binary, Version>,
receipt: Option<VersionReceipt>,
}
impl TableVersionContext {
pub(crate) fn new(versions: KeyVersions,
snapshot: SnapshotLease,
mode: PrepareMode,
expected: XHashMap<Binary, Version>,
receipt: Option<VersionReceipt>) -> Self {
Self {
versions,
snapshot,
mode,
expected,
receipt,
}
}
pub(crate) fn versions(&self) -> &KeyVersions {
&self.versions
}
pub(crate) fn snapshot_revision(&self) -> u64 {
self.snapshot.revision()
}
pub(crate) fn mode(&self) -> PrepareMode {
self.mode
}
pub(crate) fn expected(&self) -> &XHashMap<Binary, Version> {
&self.expected
}
pub(crate) fn receipt(&self) -> Option<&VersionReceipt> {
self.receipt.as_ref()
}
pub(crate) fn release_snapshot(&self) {
self.snapshot.release();
}
}
pub(crate) fn prepared_actions_conflict(existing_mode: PrepareMode,
existing: &KVActionLog,
current_mode: PrepareMode,
current: &KVActionLog) -> bool {
if existing_mode != PrepareMode::Ordinary || current_mode != PrepareMode::Ordinary {
return !matches!((existing, current), (KVActionLog::Read, KVActionLog::Read));
}
match existing {
KVActionLog::Read => matches!(current, KVActionLog::Write(_)),
KVActionLog::DirtyWrite(_) => false,
KVActionLog::Write(_) => !matches!(current, KVActionLog::DirtyWrite(_)),
}
}
pub(crate) fn binary_state_equal(left: Option<&Binary>, right: Option<&Binary>) -> bool {
match (left, right) {
(None, None) => true,
(Some(left), Some(right)) => Binary::binary_equal(left, right),
_ => false,
}
}
pub(crate) fn has_prepared_conflict<P>(prepare: &XHashMap<Guid, P>,
key: &Binary,
mode: PrepareMode,
action: &KVActionLog) -> bool
where P: Borrow<PreparedActions>
{
prepare.values().any(|prepared| {
let prepared = prepared.borrow();
prepared
.actions
.get(key)
.map(|existing| prepared_actions_conflict(prepared.mode,
existing,
mode,
action))
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeSet,
io::ErrorKind,
panic::AssertUnwindSafe,
sync::{Arc, Barrier, atomic::Ordering},
thread,
time::Duration};
use futures::executor::block_on;
use pi_async_rt::rt::multi_thread::MultiTaskRuntimeBuilder;
use pi_atom::Atom;
use pi_bon::{Encode, WriteBuffer};
use pi_guid::Guid;
use crate::{Binary, KVActionLog, TableKeyVersion};
use super::{KeyVersionConfig,
KeyVersionRegistry,
MAX_TICK,
FirstObservation,
PrepareMode,
PreparedActions,
PreparedCleanupError,
PreparedCommitError,
TableKeyConflict,
TTL_SCAN_BATCH_SIZE,
TtlKeyIndex,
VersionReceipt,
VersionConflictKind,
deadline_tick,
duration_to_ticks,
has_prepared_transaction,
normalize_conflicts,
prepared_actions_conflict,
remove_retained_prepared,
retain_prepared_for_commit};
#[cfg(feature = "trace")]
use super::{KeyVersionApiMetricsSnapshot,
KeyVersionApiOperation,
NO_DEADLINE,
estimated_record_memory_bytes};
#[test]
fn test_normalize_conflicts_orders_deduplicates_and_merges_kind() {
let table_a = Atom::from("conflict_a");
let table_b = Atom::from("conflict_b");
let key_1 = binary_from_u32(1);
let key_2 = binary_from_u32(2);
let normalized = normalize_conflicts(vec![
TableKeyConflict {
table: table_b.clone(),
key: key_2.clone(),
kind: VersionConflictKind::TransactionConflict,
},
TableKeyConflict {
table: table_a.clone(),
key: key_2.clone(),
kind: VersionConflictKind::TransactionConflict,
},
TableKeyConflict {
table: table_a.clone(),
key: key_1.clone(),
kind: VersionConflictKind::TransactionConflict,
},
TableKeyConflict {
table: table_a.clone(),
key: key_2.clone(),
kind: VersionConflictKind::ReadSetVersionMismatch,
},
TableKeyConflict {
table: table_a.clone(),
key: key_1.clone(),
kind: VersionConflictKind::TransactionConflict,
},
]);
assert_eq!(normalized, vec![
TableKeyConflict {
table: table_a.clone(),
key: key_1,
kind: VersionConflictKind::TransactionConflict,
},
TableKeyConflict {
table: table_a,
key: key_2.clone(),
kind: VersionConflictKind::ReadSetVersionMismatch,
},
TableKeyConflict {
table: table_b,
key: key_2,
kind: VersionConflictKind::TransactionConflict,
},
]);
}
#[test]
fn test_schema_create_uses_strict_prepared_conflict_matrix() {
let read = KVActionLog::Read;
let write = KVActionLog::Write(Some(binary_from_u32(1)));
let dirty_write = KVActionLog::DirtyWrite(Some(binary_from_u32(2)));
for strict_mode in [PrepareMode::SchemaCreate, PrepareMode::Versioned] {
assert!(!prepared_actions_conflict(strict_mode,
&read,
PrepareMode::Ordinary,
&read));
assert!(prepared_actions_conflict(strict_mode,
&read,
PrepareMode::Ordinary,
&write));
assert!(prepared_actions_conflict(PrepareMode::Ordinary,
&dirty_write,
strict_mode,
&write));
assert!(prepared_actions_conflict(strict_mode,
&write,
PrepareMode::Ordinary,
&dirty_write));
}
assert!(!prepared_actions_conflict(PrepareMode::Ordinary,
&dirty_write,
PrepareMode::Ordinary,
&write));
}
#[test]
fn test_prepared_transaction_duplicate_guard_is_non_destructive() {
let transaction_uid = Guid(11);
let other_uid = Guid(12);
let mut prepare = pi_hash::XHashMap::default();
prepare.insert(transaction_uid.clone(), PreparedActions {
mode: PrepareMode::Ordinary,
actions: pi_hash::XHashMap::default(),
});
assert!(has_prepared_transaction(&prepare, &transaction_uid));
assert!(!has_prepared_transaction(&prepare, &other_uid));
assert_eq!(prepare.len(), 1);
assert_eq!(prepare.get(&transaction_uid).map(|item| item.mode),
Some(PrepareMode::Ordinary));
}
#[test]
fn test_retain_prepared_for_commit_enforces_owner_lifecycle() {
let ordinary_uid = Guid(21);
let mismatch_uid = Guid(22);
let missing_uid = Guid(23);
let mut prepare = pi_hash::XHashMap::default();
let mut ordinary_actions = pi_hash::XHashMap::default();
ordinary_actions.insert(binary_from_u32(1), KVActionLog::Read);
let ordinary = Arc::new(PreparedActions {
mode: PrepareMode::Ordinary,
actions: ordinary_actions,
});
prepare.insert(ordinary_uid.clone(), ordinary.clone());
prepare.insert(mismatch_uid.clone(), Arc::new(PreparedActions {
mode: PrepareMode::Versioned,
actions: pi_hash::XHashMap::default(),
}));
let retained = match retain_prepared_for_commit(&mut prepare,
&ordinary_uid,
PrepareMode::Ordinary,
true) {
Ok(Some(prepared)) => {
assert!(Arc::ptr_eq(&prepared, &ordinary));
assert_eq!(prepared.mode, PrepareMode::Ordinary);
assert!(prepared
.actions
.get(&binary_from_u32(1))
.map(|action| matches!(action, KVActionLog::Read))
.unwrap_or(false));
prepared
},
_ => panic!("matching writable prepared item must be returned"),
};
assert!(prepare.contains_key(&ordinary_uid),
"retaining commit input must keep the prepare reservation visible");
assert_eq!(remove_retained_prepared(&mut prepare, &ordinary_uid, &retained), Ok(()));
assert!(!prepare.contains_key(&ordinary_uid));
assert!(matches!(retain_prepared_for_commit(&mut prepare,
&mismatch_uid,
PrepareMode::Ordinary,
true),
Err(PreparedCommitError::ModeMismatch(PrepareMode::Versioned))));
assert!(!prepare.contains_key(&mismatch_uid));
assert!(matches!(retain_prepared_for_commit(&mut prepare,
&missing_uid,
PrepareMode::Ordinary,
true),
Err(PreparedCommitError::Missing)));
assert!(matches!(retain_prepared_for_commit(&mut prepare,
&missing_uid,
PrepareMode::Ordinary,
false),
Ok(None)));
let retained = Arc::new(PreparedActions {
mode: PrepareMode::Ordinary,
actions: pi_hash::XHashMap::default(),
});
let replacement = Arc::new(PreparedActions {
mode: PrepareMode::Ordinary,
actions: pi_hash::XHashMap::default(),
});
prepare.insert(ordinary_uid.clone(), retained.clone());
prepare.insert(ordinary_uid.clone(), replacement.clone());
assert_eq!(remove_retained_prepared(&mut prepare, &ordinary_uid, &retained),
Err(PreparedCleanupError::IdentityMismatch));
assert!(Arc::ptr_eq(prepare.get(&ordinary_uid).unwrap(), &replacement),
"identity mismatch must not remove another owner");
assert_eq!(remove_retained_prepared(&mut prepare, &ordinary_uid, &replacement), Ok(()));
assert_eq!(remove_retained_prepared(&mut prepare, &ordinary_uid, &replacement),
Err(PreparedCleanupError::Missing));
}
#[test]
fn test_publish_atomically_advances_current_record() {
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let table = Atom::from("publish-unit");
let key = binary_from_u32(31);
let value = binary_from_u32(310);
assert_eq!(versions.first_observation(key.clone(), false, || Guid(1)),
FirstObservation::Inserted(super::Version::Delete(Guid(1))));
let first = versions.current(&key).unwrap();
assert_eq!(first.source, super::VersionSource::FirstObservation);
assert_eq!(first.revision, 0);
assert_eq!(first.generation, 1);
let upsert = versions.publish(table.clone(),
key.clone(),
Some(&value),
Guid(2),
1);
assert_table_key_version_fields(&upsert, &TableKeyVersion {
table: table.clone(),
key: key.clone(),
version: super::Version::Upsert(Guid(2)),
});
let second = versions.current(&key).unwrap();
assert_eq!(second.version, super::Version::Upsert(Guid(2)));
assert_eq!(second.source, super::VersionSource::CommittedWrite);
assert_eq!(second.revision, 1);
assert_eq!(second.generation, 2);
let delete = versions.publish(table.clone(), key.clone(), None, Guid(3), 2);
assert_table_key_version_fields(&delete, &TableKeyVersion {
table,
key: key.clone(),
version: super::Version::Delete(Guid(3)),
});
let third = versions.current(&key).unwrap();
assert_eq!(third.version, super::Version::Delete(Guid(3)));
assert_eq!(third.source, super::VersionSource::CommittedWrite);
assert_eq!(third.revision, 2);
assert_eq!(third.generation, 3);
assert_eq!(versions.len(), 1);
}
#[test]
fn test_first_observation_lease_closes_stale_value_redline() {
let config = KeyVersionConfig::new(Duration::from_millis(1),
Duration::from_millis(1)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let table = Atom::from("first-observation-redline");
let key = binary_from_u32(32);
let committed_value = binary_from_u32(320);
let stale_value = None::<Binary>;
let lease = versions.lease_first_observation();
let _ = versions.publish(table,
key.clone(),
Some(&committed_value),
Guid(2),
1);
let rt = MultiTaskRuntimeBuilder::default()
.init_worker_size(1)
.build();
let mut blocked_statistics = super::TtlCollectStatistics::default();
block_on(versions.collect_expired(&rt,
MAX_TICK,
&mut blocked_statistics));
assert_eq!(versions.current_version(&key),
Some(super::Version::Upsert(Guid(2))),
"TTL must retain a committed version while a missing query owns a lease");
assert_eq!(blocked_statistics.due_candidates, 1);
assert_eq!(blocked_statistics.first_observation_blocked, 1);
assert_eq!(blocked_statistics.removed_records, 0);
assert_eq!(index_len(&versions), 1,
"a lease-blocked candidate must retain its TTL token");
let observed = versions.first_observation(key, stale_value.is_some(), || Guid(3));
assert_eq!(observed,
FirstObservation::Occupied(super::Version::Upsert(Guid(2))));
drop(lease);
assert!(!versions.has_active_first_observation());
let mut released_statistics = super::TtlCollectStatistics::default();
block_on(versions.collect_expired(&rt,
MAX_TICK,
&mut released_statistics));
assert_eq!(released_statistics.removed_records, 1);
assert_eq!(released_statistics.removed_committed_writes, 1);
assert!(versions.current_version(&binary_from_u32(32)).is_none());
assert_eq!(index_len(&versions), 0,
"the last lease release must make the retained token collectible");
}
#[test]
fn test_first_observation_lease_is_noop_when_ttl_disabled() {
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let epoch = versions.0.lease_epoch.load(Ordering::Acquire);
let lease = versions.lease_first_observation();
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 0);
assert!(!versions.has_active_first_observation());
drop(lease);
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 0);
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), epoch);
}
#[test]
fn test_first_observation_lease_balances_nested_guards() {
let config = KeyVersionConfig::new(Duration::from_millis(1),
Duration::from_millis(1)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let first = versions.lease_first_observation();
let second = versions.lease_first_observation();
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 2);
assert!(versions.has_active_first_observation());
drop(first);
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 1);
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), 1);
drop(second);
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 0);
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), 2);
}
#[test]
fn test_first_observation_lease_balances_cross_thread_guards() {
const THREADS: usize = 8;
let config = KeyVersionConfig::new(Duration::from_millis(1),
Duration::from_millis(1)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = Arc::new(registry.create_table_versions());
let acquired = Arc::new(Barrier::new(THREADS + 1));
let release = Arc::new(Barrier::new(THREADS + 1));
let mut threads = Vec::with_capacity(THREADS);
for _ in 0..THREADS {
let versions = versions.clone();
let acquired = acquired.clone();
let release = release.clone();
threads.push(thread::spawn(move || {
let lease = versions.lease_first_observation();
acquired.wait();
release.wait();
drop(lease);
}));
}
acquired.wait();
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), THREADS);
release.wait();
for thread in threads {
thread.join().expect("first-observation lease worker must not panic");
}
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 0);
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), THREADS as u64);
}
#[test]
fn test_first_observation_lease_releases_during_unwind() {
let config = KeyVersionConfig::new(Duration::from_millis(1),
Duration::from_millis(1)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let unwind = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _lease = versions.lease_first_observation();
panic!("intentional first-observation lease unwind");
}));
assert!(unwind.is_err());
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), 0);
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), 1);
}
#[test]
fn test_first_observation_lease_count_never_wraps() {
let config = KeyVersionConfig::new(Duration::from_millis(1),
Duration::from_millis(1)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
versions.0.active_first_observations.store(usize::MAX, Ordering::Release);
let overflow = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _lease = versions.lease_first_observation();
}));
assert!(overflow.is_err());
assert_eq!(versions.0.active_first_observations.load(Ordering::Acquire), usize::MAX);
versions.0.active_first_observations.store(0, Ordering::Release);
}
#[test]
fn test_version_receipt_moves_first_batch_and_appends_in_order() {
let receipt = VersionReceipt::new();
let first_item = TableKeyVersion {
table: Atom::from("receipt-a"),
key: binary_from_u32(41),
version: super::Version::Upsert(Guid(4)),
};
let second_item = TableKeyVersion {
table: Atom::from("receipt-b"),
key: binary_from_u32(42),
version: super::Version::Delete(Guid(5)),
};
let third_item = TableKeyVersion {
table: Atom::from("receipt-c"),
key: binary_from_u32(43),
version: super::Version::Upsert(Guid(6)),
};
let first_batch = vec![first_item.clone(), second_item.clone()];
let first_ptr = first_batch.as_ptr();
let first_capacity = first_batch.capacity();
receipt.append(first_batch);
{
let stored = receipt.0.lock();
assert_eq!(stored.as_ptr(), first_ptr,
"empty receipt must take ownership of the first Vec allocation");
assert_eq!(stored.capacity(), first_capacity);
assert_eq!(stored.len(), 2);
assert_table_key_version_fields(&stored[0], &first_item);
assert_table_key_version_fields(&stored[1], &second_item);
}
receipt.append(Vec::new());
{
let stored = receipt.0.lock();
assert_eq!(stored.as_ptr(), first_ptr,
"an empty append must not replace the current allocation");
assert_eq!(stored.len(), 2);
assert_table_key_version_fields(&stored[0], &first_item);
assert_table_key_version_fields(&stored[1], &second_item);
}
receipt.append(vec![third_item.clone()]);
let taken = receipt.take();
assert_eq!(taken.len(), 3);
assert_table_key_version_fields(&taken[0], &first_item);
assert_table_key_version_fields(&taken[1], &second_item);
assert_table_key_version_fields(&taken[2], &third_item);
assert!(receipt.take().is_empty());
}
#[test]
fn test_key_version_config_zero_poll_interval_depends_on_ttl_enablement() {
let error = KeyVersionConfig::new(Duration::from_secs(1), Duration::ZERO)
.expect_err("enabled TTL with zero poll interval must be rejected");
assert_eq!(error.kind(), ErrorKind::InvalidInput);
let disabled = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO)
.expect("disabled TTL must ignore a zero poll interval");
assert_eq!(disabled.ttl_ticks(), None);
assert_eq!(disabled.poll_interval_ticks(), 0);
let (disabled_registry, disabled_receiver) = KeyVersionRegistry::new(disabled);
assert!(disabled_receiver.is_none());
assert!(disabled_registry.0.shutdown_tx.is_none());
let enabled = KeyVersionConfig::new(Duration::from_secs(1), Duration::from_millis(1))
.expect("enabled TTL with a positive poll interval must be accepted");
assert_eq!(enabled.ttl_ticks(), Some(1000));
assert_eq!(enabled.poll_interval_ticks(), 1);
let (enabled_registry, enabled_receiver) = KeyVersionRegistry::new(enabled);
assert!(enabled_receiver.is_some());
assert!(enabled_registry.0.shutdown_tx.is_some());
}
#[test]
fn test_duration_to_ticks_uses_one_millisecond_granularity() {
assert_eq!(duration_to_ticks(Duration::from_nanos(1)), 1);
assert_eq!(duration_to_ticks(Duration::from_micros(999)), 1);
assert_eq!(duration_to_ticks(Duration::from_millis(1)), 1);
assert_eq!(duration_to_ticks(Duration::from_micros(1_999)), 1);
assert_eq!(duration_to_ticks(Duration::from_millis(2)), 2);
}
#[test]
fn test_deadline_tick_never_shortens_effective_ttl() {
assert_eq!(deadline_tick(Duration::from_millis(10), 20), 30);
assert_eq!(deadline_tick(
Duration::from_millis(10) + Duration::from_nanos(1), 20), 31);
assert_eq!(deadline_tick(Duration::from_micros(10_999), 20), 31);
assert_eq!(deadline_tick(Duration::from_millis(MAX_TICK - 1), 20), MAX_TICK);
assert_eq!(deadline_tick(Duration::from_millis(MAX_TICK), MAX_TICK), MAX_TICK);
}
#[test]
fn test_ttl_key_index_round_snapshot_defers_requeued_and_new_tokens() {
let index = TtlKeyIndex::new();
index.push(binary_from_u32(1));
index.push(binary_from_u32(2));
index.push(binary_from_u32(3));
let mut remaining = index.len();
let first = index.take_batch(2);
remaining -= first.len();
assert_eq!(binary_values(&first), vec![1, 2]);
index.push(first[0].clone());
index.push(binary_from_u32(4));
let rest_of_round = index.take_batch(remaining);
assert_eq!(binary_values(&rest_of_round), vec![3]);
let deferred = index.take_batch(index.len());
assert_eq!(binary_values(&deferred), vec![1, 4]);
assert_eq!(index.len(), 0);
}
#[test]
fn test_ttl_key_index_honors_batch_limit() {
let index = TtlKeyIndex::new();
for value in 0..(TTL_SCAN_BATCH_SIZE + 44) {
index.push(binary_from_u32(value as u32));
}
let first = index.take_batch(TTL_SCAN_BATCH_SIZE);
assert_eq!(first.len(), TTL_SCAN_BATCH_SIZE);
assert_eq!(index.len(), 44);
let second = index.take_batch(TTL_SCAN_BATCH_SIZE);
assert_eq!(second.len(), 44);
assert_eq!(index.len(), 0);
}
#[test]
fn test_ttl_key_index_accepts_concurrent_unique_producers() {
const PRODUCERS: usize = 4;
const TOKENS_PER_PRODUCER: usize = 128;
let index = Arc::new(TtlKeyIndex::new());
thread::scope(|scope| {
for producer in 0..PRODUCERS {
let index = index.clone();
scope.spawn(move || {
for offset in 0..TOKENS_PER_PRODUCER {
let value = producer * TOKENS_PER_PRODUCER + offset;
index.push(binary_from_u32(value as u32));
}
});
}
});
let tokens = index.take_batch(index.len());
assert_eq!(tokens.len(), PRODUCERS * TOKENS_PER_PRODUCER);
let values: BTreeSet<u32> = binary_values(&tokens).into_iter().collect();
assert_eq!(values.len(), PRODUCERS * TOKENS_PER_PRODUCER);
assert_eq!(values.first(), Some(&0));
assert_eq!(values.last(), Some(&((PRODUCERS * TOKENS_PER_PRODUCER - 1) as u32)));
assert_eq!(index.len(), 0);
}
#[test]
fn test_key_versions_maintains_one_token_per_current_record() {
let config = KeyVersionConfig::new(Duration::from_secs(1),
Duration::from_millis(10)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let key = encode_usize(7);
let value = binary_from_u32(70);
let first = versions.first_observation(key.clone(), false, || Guid(1));
assert_eq!(first, FirstObservation::Inserted(super::Version::Delete(Guid(1))));
assert_eq!(index_len(&versions), 1);
let cached = versions.first_observation(key.clone(), false, || {
panic!("cache hit must not allocate another first-observation Guid")
});
assert_eq!(cached, FirstObservation::Occupied(super::Version::Delete(Guid(1))));
assert_eq!(index_len(&versions), 1);
let _ = versions.publish(Atom::from("ttl-unit"),
key.clone(),
Some(&value),
Guid(2),
1);
assert_eq!(index_len(&versions), 1);
let token = versions.0.ttl_keys.take_batch(1);
assert_eq!(token.len(), 1);
assert_eq!(token[0].as_ref(), key.as_ref());
let candidate = versions.current(&key).unwrap();
assert!(versions
.0
.versions
.remove_if(&key, |_key, current| current.exact_eq(&candidate))
.is_some());
assert_eq!(index_len(&versions), 0);
let _ = versions.publish(Atom::from("ttl-unit"),
key.clone(),
None,
Guid(3),
2);
assert_eq!(index_len(&versions), 1);
}
#[test]
fn test_ttl_key_index_clear_releases_binary_owner() {
let index = TtlKeyIndex::new();
let key = binary_from_u32(99);
let payload = Arc::downgrade(&key.0);
index.push(key.clone());
drop(key);
assert!(payload.upgrade().is_some());
index.clear();
assert!(payload.upgrade().is_none());
assert_eq!(index.len(), 0);
}
#[test]
fn test_key_versions_drop_releases_table_and_binary_owners() {
let config = KeyVersionConfig::new(Duration::from_secs(1),
Duration::from_millis(10)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let versions_owner = Arc::downgrade(&versions.0);
let table = Atom::from("ttl-drop-unit");
let key = encode_usize(101);
let payload = Arc::downgrade(&key.0);
let _ = versions.first_observation(key.clone(), false, || Guid(1));
assert_eq!(index_len(&versions), 1);
registry.install(table.clone(), versions.clone());
drop(key);
assert!(versions_owner.upgrade().is_some());
assert!(payload.upgrade().is_some());
registry.remove_exact(&table, &versions);
assert!(registry.0.tables.get(&table).is_none());
drop(versions);
assert!(versions_owner.upgrade().is_none());
assert!(payload.upgrade().is_none());
}
#[test]
fn test_snapshot_lease_release_is_idempotent() {
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let lease = versions.lease_current();
assert_eq!(lease.revision(), 0);
assert_eq!(versions.0.active_snapshots.lock().get(&0), Some(&1));
lease.release();
assert!(versions.0.active_snapshots.lock().is_empty());
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), 1);
lease.release();
drop(lease);
assert!(versions.0.active_snapshots.lock().is_empty());
assert_eq!(versions.0.lease_epoch.load(Ordering::Acquire), 1);
}
#[test]
fn test_next_revision_never_wraps() {
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
versions.0.completed_revision.store(u64::MAX - 1, Ordering::Release);
assert_eq!(versions.checked_next_revision(), Some(u64::MAX));
versions.complete_revision(u64::MAX);
assert_eq!(versions.completed_revision(), u64::MAX);
assert_eq!(versions.checked_next_revision(), None);
}
#[cfg(feature = "trace")]
#[test]
fn test_trace_record_memory_estimate_uses_capacity_and_ttl_owner() {
let small = binary_with_capacity(1, 32);
let large = binary_with_capacity(1, 256);
let small_without_ttl = estimated_record_memory_bytes(&small, false);
let large_without_ttl = estimated_record_memory_bytes(&large, false);
let large_with_ttl = estimated_record_memory_bytes(&large, true);
assert_eq!(large_without_ttl - small_without_ttl,
(large.0.capacity() - small.0.capacity()) as u64);
assert_eq!(large_with_ttl - large_without_ttl,
std::mem::size_of::<(Binary, std::sync::atomic::AtomicUsize)>() as u64);
}
#[cfg(feature = "trace")]
#[test]
fn test_trace_cache_metrics_track_insert_replace_remove_and_clear() {
let config = KeyVersionConfig::new(Duration::from_secs(1),
Duration::from_millis(10)).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let first_key = binary_with_capacity(11, 64);
let second_key = binary_with_capacity(22, 128);
let value = binary_from_u32(99);
let first_bytes = estimated_record_memory_bytes(&first_key, true);
let second_bytes = estimated_record_memory_bytes(&second_key, true);
assert_eq!(versions.metrics_snapshot(), Default::default());
let first_version = versions.first_observation(first_key.clone(), false, || Guid(1));
assert_eq!(first_version,
FirstObservation::Inserted(super::Version::Delete(Guid(1))));
assert_eq!(versions.metrics_snapshot(), super::KeyVersionCacheMetricsSnapshot {
record_count: 1,
estimated_memory_bytes: first_bytes,
});
assert_eq!(versions.first_observation(first_key.clone(), false, || {
panic!("cache hit must not allocate a new Guid")
}), FirstObservation::Occupied(super::Version::Delete(Guid(1))));
assert_eq!(versions.metrics_snapshot().record_count, 1);
let _ = versions.publish(Atom::from("trace-metrics"),
first_key.clone(),
Some(&value),
Guid(2),
1);
assert_eq!(versions.metrics_snapshot(), super::KeyVersionCacheMetricsSnapshot {
record_count: 1,
estimated_memory_bytes: first_bytes,
});
let _ = versions.publish(Atom::from("trace-metrics"),
second_key.clone(),
None,
Guid(3),
2);
assert_eq!(versions.metrics_snapshot(), super::KeyVersionCacheMetricsSnapshot {
record_count: 2,
estimated_memory_bytes: first_bytes + second_bytes,
});
let candidate = versions.current(&first_key).unwrap();
let removed = versions
.0
.versions
.remove_if(&first_key, |_key, current| current.exact_eq(&candidate))
.expect("exact current record must be removable");
versions.record_removed(estimated_record_memory_bytes(
&removed.0,
removed.1.deadline_tick != NO_DEADLINE));
assert_eq!(versions.metrics_snapshot(), super::KeyVersionCacheMetricsSnapshot {
record_count: 1,
estimated_memory_bytes: second_bytes,
});
versions.clear_records();
assert_eq!(versions.metrics_snapshot(), Default::default());
assert_eq!(versions.len(), 0);
assert_eq!(index_len(&versions), 0);
}
#[cfg(feature = "trace")]
#[test]
fn test_trace_cache_metrics_match_quiescent_map_after_concurrent_insertions() {
const THREADS: usize = 4;
const KEYS_PER_THREAD: usize = 128;
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
let versions = registry.create_table_versions();
let shared_key = binary_with_capacity(7, 96);
thread::scope(|scope| {
for worker in 0..THREADS {
let versions = versions.clone();
let shared_key = shared_key.clone();
scope.spawn(move || {
let _ = versions.first_observation(shared_key, false, || {
Guid((worker + 1) as u128)
});
for offset in 0..KEYS_PER_THREAD {
let value = 1_000 + worker * KEYS_PER_THREAD + offset;
let key = encode_usize(value);
let _ = versions.first_observation(key, false, || {
Guid((10_000 + value) as u128)
});
}
});
}
});
let expected_count = 1 + THREADS * KEYS_PER_THREAD;
let expected_memory = versions
.0
.versions
.iter()
.map(|entry| estimated_record_memory_bytes(
entry.key(),
entry.value().deadline_tick != NO_DEADLINE))
.sum::<u64>();
assert_eq!(versions.len(), expected_count);
assert_eq!(versions.metrics_snapshot(), super::KeyVersionCacheMetricsSnapshot {
record_count: expected_count as u64,
estimated_memory_bytes: expected_memory,
});
}
#[cfg(feature = "trace")]
#[test]
fn test_trace_api_call_guard_records_one_terminal_outcome() {
let config = KeyVersionConfig::new(Duration::ZERO, Duration::ZERO).unwrap();
let (registry, _shutdown) = KeyVersionRegistry::new(config);
registry.begin_api_call(KeyVersionApiOperation::Query).finish(true);
registry.begin_api_call(KeyVersionApiOperation::Query).finish(false);
drop(registry.begin_api_call(KeyVersionApiOperation::Query));
registry.begin_api_call(KeyVersionApiOperation::Prepare).finish(true);
drop(registry.begin_api_call(KeyVersionApiOperation::Prepare));
registry.begin_api_call(KeyVersionApiOperation::Commit).finish(false);
assert_eq!(registry.api_metrics_snapshot(), KeyVersionApiMetricsSnapshot {
query_success: 1,
query_failure: 2,
prepare_success: 1,
prepare_failure: 1,
commit_success: 0,
commit_failure: 1,
});
}
#[cfg(feature = "trace")]
#[test]
fn test_trace_api_metrics_delta_handles_counter_wrap() {
let previous = KeyVersionApiMetricsSnapshot {
query_success: u64::MAX,
query_failure: 7,
prepare_success: 8,
prepare_failure: 9,
commit_success: 10,
commit_failure: 11,
};
let current = KeyVersionApiMetricsSnapshot {
query_success: 1,
query_failure: 10,
prepare_success: 12,
prepare_failure: 14,
commit_success: 16,
commit_failure: 18,
};
assert_eq!(current.delta_since(previous), KeyVersionApiMetricsSnapshot {
query_success: 2,
query_failure: 3,
prepare_success: 4,
prepare_failure: 5,
commit_success: 6,
commit_failure: 7,
});
}
fn index_len(versions: &super::KeyVersions) -> usize {
versions.0.ttl_keys.len()
}
fn assert_table_key_version_fields(actual: &TableKeyVersion,
expected: &TableKeyVersion) {
assert_eq!(actual.table, expected.table);
assert_eq!(actual.key.as_ref(), expected.key.as_ref());
assert_eq!(actual.version, expected.version);
}
fn binary_from_u32(value: u32) -> Binary {
Binary::new(value.to_le_bytes().to_vec())
}
#[cfg(feature = "trace")]
fn binary_with_capacity(value: u32, capacity: usize) -> Binary {
let mut encoded = WriteBuffer::new();
(value as usize).encode(&mut encoded);
let mut bytes = Vec::with_capacity(capacity.max(encoded.len()));
bytes.extend_from_slice(&encoded.bytes);
Binary::new(bytes)
}
fn encode_usize(value: usize) -> Binary {
let mut buffer = WriteBuffer::new();
value.encode(&mut buffer);
Binary::new(buffer.bytes)
}
fn binary_values(tokens: &[Binary]) -> Vec<u32> {
tokens
.iter()
.map(|token| u32::from_le_bytes(token.as_ref().try_into().unwrap()))
.collect()
}
}