use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use super::Key;
const PRUNE_INTERVAL_COMMITS: u64 = 4096;
#[derive(Default)]
pub struct WriteConflictRegistry {
recent_writes: DashMap<Key, u64>,
active_snapshots: DashMap<u64, u64>,
inflight_commits: DashMap<u64, ()>,
inflight_count: AtomicUsize,
commit_count: AtomicU64,
}
impl WriteConflictRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_txn(&self, txn_id: u64, snapshot_ts: u64) {
self.active_snapshots.insert(txn_id, snapshot_ts);
}
pub fn refresh_txn(&self, txn_id: u64, snapshot_ts: u64) {
self.active_snapshots.insert(txn_id, snapshot_ts);
}
pub fn deregister_txn(&self, txn_id: u64) {
self.active_snapshots.remove(&txn_id);
}
pub fn snapshot_barrier(&self, snapshot_ts: u64) {
let mut spins: u32 = 0;
loop {
if self.inflight_count.load(Ordering::Acquire) == 0 {
return;
}
let pending = self
.inflight_commits
.iter()
.any(|entry| *entry.key() <= snapshot_ts);
if !pending {
return;
}
spins += 1;
if spins < 64 {
std::thread::yield_now();
} else {
std::thread::sleep(std::time::Duration::from_micros(50));
}
}
}
pub fn begin_commit(&self, commit_ts: u64) {
self.inflight_commits.insert(commit_ts, ());
self.inflight_count.fetch_add(1, Ordering::AcqRel);
}
pub fn end_commit(&self, commit_ts: u64) {
if self.inflight_commits.remove(&commit_ts).is_some() {
self.inflight_count.fetch_sub(1, Ordering::AcqRel);
}
}
pub fn validate_and_record(
&self,
write_set: &DashMap<Key, Option<Vec<u8>>>,
validate: bool,
snapshot_ts: u64,
commit_ts: u64,
) -> std::result::Result<(), (Key, u64)> {
if write_set.is_empty() {
return Ok(());
}
let mut recorded: Vec<(Key, Option<u64>)> = Vec::with_capacity(write_set.len());
let mut conflict: Option<(Key, u64)> = None;
for item in write_set.iter() {
let key = item.key();
let entry = self.recent_writes.entry(key.clone());
match entry {
dashmap::mapref::entry::Entry::Occupied(mut occupied) => {
let prior = *occupied.get();
if validate && prior > snapshot_ts {
conflict = Some((key.clone(), prior));
break;
}
occupied.insert(commit_ts);
recorded.push((key.clone(), Some(prior)));
}
dashmap::mapref::entry::Entry::Vacant(vacant) => {
vacant.insert(commit_ts);
recorded.push((key.clone(), None));
}
}
}
if let Some((key, ts)) = conflict {
for (k, prior) in recorded {
match prior {
Some(ts) => {
self.recent_writes.insert(k, ts);
}
None => {
self.recent_writes.remove(&k);
}
}
}
return Err((key, ts));
}
let n = self.commit_count.fetch_add(1, Ordering::Relaxed);
if n % PRUNE_INTERVAL_COMMITS == PRUNE_INTERVAL_COMMITS - 1 {
self.prune();
}
Ok(())
}
fn prune(&self) {
let min_active = self
.active_snapshots
.iter()
.map(|entry| *entry.value())
.min()
.unwrap_or(u64::MAX);
self.recent_writes.retain(|_, ts| *ts >= min_active);
}
pub fn tracked_keys(&self) -> usize {
self.recent_writes.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ws(keys: &[&str]) -> DashMap<Key, Option<Vec<u8>>> {
let m = DashMap::new();
for k in keys {
m.insert(k.as_bytes().to_vec(), Some(vec![1]));
}
m
}
#[test]
fn first_committer_wins() {
let reg = WriteConflictRegistry::new();
assert!(reg.validate_and_record(&ws(&["k1"]), true, 10, 20).is_ok());
let err = reg.validate_and_record(&ws(&["k1"]), true, 15, 25).unwrap_err();
assert_eq!(err.1, 20);
assert!(reg.validate_and_record(&ws(&["k1"]), true, 30, 35).is_ok());
}
#[test]
fn losing_commit_undoes_partial_records() {
let reg = WriteConflictRegistry::new();
assert!(reg.validate_and_record(&ws(&["b"]), true, 10, 20).is_ok());
let _ = reg.validate_and_record(&ws(&["a", "b"]), true, 15, 25).unwrap_err();
assert!(reg.validate_and_record(&ws(&["a"]), true, 5, 30).is_ok());
}
#[test]
fn non_validating_commits_record() {
let reg = WriteConflictRegistry::new();
assert!(reg.validate_and_record(&ws(&["k"]), false, 0, 20).is_ok());
let err = reg.validate_and_record(&ws(&["k"]), true, 10, 25).unwrap_err();
assert_eq!(err.1, 20);
}
#[test]
fn prune_respects_active_snapshots() {
let reg = WriteConflictRegistry::new();
reg.register_txn(1, 50);
for i in 0..(PRUNE_INTERVAL_COMMITS + 1) {
let m = ws(&[format!("k{i}").as_str()]);
let _ = reg.validate_and_record(&m, false, 0, 40 + i);
}
assert!(reg.tracked_keys() > 0);
reg.deregister_txn(1);
}
#[test]
fn snapshot_barrier_waits_for_inflight() {
let reg = std::sync::Arc::new(WriteConflictRegistry::new());
reg.begin_commit(10);
let r2 = reg.clone();
let h = std::thread::spawn(move || {
r2.snapshot_barrier(15);
});
std::thread::sleep(std::time::Duration::from_millis(20));
assert!(!h.is_finished(), "barrier returned while commit in flight");
reg.end_commit(10);
h.join().unwrap();
reg.begin_commit(100);
reg.snapshot_barrier(50);
reg.end_commit(100);
}
}