use alloc::collections::BTreeMap;
use alloc::vec::Vec;
pub type Timestamp = u64;
#[derive(Debug, Clone)]
struct Version {
commit_ts: Timestamp,
value: Vec<u8>,
}
#[derive(Debug, Default)]
pub struct MvccStore {
versions: BTreeMap<u64, Vec<Version>>,
clock: Timestamp,
}
#[derive(Debug)]
pub struct Transaction {
snapshot: Timestamp,
reads: Vec<u64>,
writes: BTreeMap<u64, Vec<u8>>,
committed: bool,
}
impl Transaction {
pub fn get_write(&self, key: u64) -> Option<&[u8]> {
self.writes.get(&key).map(|v| v.as_slice())
}
pub fn writes_iter(&self) -> impl Iterator<Item = (u64, &[u8])> {
self.writes.iter().map(|(k, v)| (*k, v.as_slice()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommitError {
Conflict,
}
impl MvccStore {
pub fn new() -> Self {
Self {
versions: BTreeMap::new(),
clock: 0,
}
}
pub fn begin(&self) -> Transaction {
Transaction {
snapshot: self.clock,
reads: Vec::new(),
writes: BTreeMap::new(),
committed: false,
}
}
fn visible_value(&self, key: u64, as_of: Timestamp) -> Option<&[u8]> {
let chain = self.versions.get(&key)?;
chain
.iter()
.rev()
.find(|v| v.commit_ts <= as_of)
.map(|v| v.value.as_slice())
}
pub fn read(&self, txn: &mut Transaction, key: u64) -> Option<Vec<u8>> {
txn.reads.push(key);
if let Some(v) = txn.writes.get(&key) {
return Some(v.clone());
}
self.visible_value(key, txn.snapshot).map(|v| v.to_vec())
}
pub fn write(&self, txn: &mut Transaction, key: u64, value: Vec<u8>) {
txn.writes.insert(key, value);
}
fn latest_commit_ts(&self, key: u64) -> Option<Timestamp> {
self.versions
.get(&key)
.and_then(|c| c.last())
.map(|v| v.commit_ts)
}
pub fn commit(&mut self, mut txn: Transaction) -> Result<Timestamp, CommitError> {
for &key in txn.reads.iter().chain(txn.writes.keys()) {
if let Some(latest) = self.latest_commit_ts(key) {
if latest > txn.snapshot {
return Err(CommitError::Conflict);
}
}
}
self.clock += 1;
let commit_ts = self.clock;
for (key, value) in txn.writes.iter() {
self.versions.entry(*key).or_default().push(Version {
commit_ts,
value: value.clone(),
});
}
txn.committed = true;
Ok(commit_ts)
}
pub fn latest(&self, key: u64) -> Option<Vec<u8>> {
self.versions
.get(&key)
.and_then(|c| c.last())
.map(|v| v.value.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_isolation_hides_later_commits() {
let mut store = MvccStore::new();
let mut t0 = store.begin();
store.write(&mut t0, 1, alloc::vec![10]);
store.commit(t0).unwrap();
let mut reader = store.begin();
let mut writer = store.begin();
store.write(&mut writer, 1, alloc::vec![20]);
store.commit(writer).unwrap();
assert_eq!(store.read(&mut reader, 1), Some(alloc::vec![10]));
}
#[test]
fn write_write_conflict_is_detected() {
let mut store = MvccStore::new();
let mut seed = store.begin();
store.write(&mut seed, 1, alloc::vec![0]);
store.commit(seed).unwrap();
let mut a = store.begin();
let mut b = store.begin();
store.write(&mut a, 1, alloc::vec![1]);
store.write(&mut b, 1, alloc::vec![2]);
assert!(store.commit(a).is_ok());
assert_eq!(store.commit(b), Err(CommitError::Conflict));
assert_eq!(store.latest(1), Some(alloc::vec![1]));
}
#[test]
fn read_write_conflict_aborts() {
let mut store = MvccStore::new();
let mut seed = store.begin();
store.write(&mut seed, 1, alloc::vec![0]);
store.commit(seed).unwrap();
let mut t = store.begin();
let _ = store.read(&mut t, 1);
let mut other = store.begin();
store.write(&mut other, 1, alloc::vec![9]);
store.commit(other).unwrap();
store.write(&mut t, 2, alloc::vec![5]);
assert_eq!(store.commit(t), Err(CommitError::Conflict));
}
#[test]
fn non_conflicting_transactions_both_commit() {
let mut store = MvccStore::new();
let mut a = store.begin();
let mut b = store.begin();
store.write(&mut a, 1, alloc::vec![1]);
store.write(&mut b, 2, alloc::vec![2]);
assert!(store.commit(a).is_ok());
assert!(store.commit(b).is_ok());
assert_eq!(store.latest(1), Some(alloc::vec![1]));
assert_eq!(store.latest(2), Some(alloc::vec![2]));
}
}