#![allow(dead_code)]
use std::collections::BTreeMap;
use std::sync::Arc;
use kovan_mvcc::{IsolationLevel as MvccIsolation, KovanMVCC, MvccError};
use super::storage::RegolithStorage;
use crate::engine::{DurabilityMode, RegolithEngine};
use crate::transaction::{IsolationLevel, TransactionError, TxResult};
pub(crate) struct MvccTransactions {
mvcc: KovanMVCC,
storage: Arc<RegolithStorage>,
}
impl MvccTransactions {
pub(crate) fn new(engine: Arc<RegolithEngine>, durability: DurabilityMode) -> Self {
let storage = Arc::new(RegolithStorage::new(engine, durability));
Self {
mvcc: KovanMVCC::with_storage(storage.clone() as Arc<dyn kovan_mvcc::Storage>),
storage,
}
}
pub(crate) fn begin(&self, isolation: IsolationLevel) -> MvccTxn<'_> {
MvccTxn {
inner: self.mvcc.begin_with_isolation(map_isolation(isolation)),
storage: &self.storage,
staged: BTreeMap::new(),
savepoints: Vec::new(),
}
}
}
fn map_isolation(level: IsolationLevel) -> MvccIsolation {
match level {
IsolationLevel::ReadCommitted => MvccIsolation::ReadCommitted,
IsolationLevel::SnapshotIsolation => MvccIsolation::RepeatableRead,
IsolationLevel::Serializable => MvccIsolation::Serializable,
}
}
pub(crate) struct MvccTxn<'db> {
inner: kovan_mvcc::Txn,
storage: &'db Arc<RegolithStorage>,
staged: BTreeMap<Vec<u8>, Option<Vec<u8>>>,
savepoints: Vec<BTreeMap<Vec<u8>, Option<Vec<u8>>>>,
}
impl MvccTxn<'_> {
fn staged(&self, key: &[u8]) -> Option<Option<Vec<u8>>> {
self.staged.get(key).cloned()
}
pub(crate) fn get(&mut self, k: &[u8]) -> TxResult<Option<Vec<u8>>> {
if let Some(local) = self.staged(k) {
return Ok(local);
}
let value = self.inner.read(k);
if let Some(err) = self.storage.take_failure() {
return Err(TransactionError::Io(err));
}
Ok(value)
}
pub(crate) fn get_for_update(&mut self, k: &[u8]) -> TxResult<Option<Vec<u8>>> {
let current = self.get(k)?;
match ¤t {
Some(v) => self.staged.insert(k.to_vec(), Some(v.clone())),
None => self.staged.insert(k.to_vec(), None),
};
Ok(current)
}
pub(crate) fn put(&mut self, k: &[u8], value: &[u8]) -> TxResult<()> {
self.staged.insert(k.to_vec(), Some(value.to_vec()));
Ok(())
}
pub(crate) fn delete(&mut self, k: &[u8]) -> TxResult<()> {
self.staged.insert(k.to_vec(), None);
Ok(())
}
pub(crate) fn delete_range(&mut self, start: &[u8], end: &[u8]) -> TxResult<()> {
if start >= end {
return Ok(());
}
let keys = self
.storage
.keys_in_range(start, end, self.inner.start_ts())
.map_err(TransactionError::Io)?;
for key in keys {
self.staged.insert(key, None);
}
Ok(())
}
pub(crate) fn merge(
&mut self,
merge_operator: Option<&Arc<dyn crate::options::MergeOperator>>,
k: &[u8],
operand: &[u8],
) -> TxResult<()> {
let op = merge_operator.ok_or_else(|| {
TransactionError::Io(std::io::Error::other(
"merge called with no merge operator configured; set Options::merge_operator",
))
})?;
let existing = self.get(k)?;
let merged = op
.full_merge(k, existing.as_deref(), std::slice::from_ref(&operand))
.ok_or_else(|| {
TransactionError::Io(std::io::Error::other("merge operator rejected the operand"))
})?;
self.staged.insert(k.to_vec(), Some(merged));
Ok(())
}
pub(crate) fn set_savepoint(&mut self) {
self.savepoints.push(self.staged.clone());
}
pub(crate) fn rollback_to_savepoint(&mut self) -> bool {
match self.savepoints.pop() {
Some(saved) => {
self.staged = saved;
true
}
None => false,
}
}
pub(crate) fn commit(mut self) -> TxResult<u64> {
let staged = std::mem::take(&mut self.staged);
for (key, value) in staged {
match value {
Some(v) => self.inner.write(&key, v).map_err(map_error)?,
None => self.inner.delete(&key).map_err(map_error)?,
}
}
let storage = self.storage;
let commit_ts = self.inner.commit().map_err(map_error)?;
if let Some(err) = storage.take_failure() {
return Err(TransactionError::Io(err));
}
Ok(commit_ts)
}
}
fn map_error(err: MvccError) -> TransactionError {
match err {
MvccError::LockConflict { key, .. } => TransactionError::Busy(key),
MvccError::WriteConflict {
key,
conflicting_ts,
}
| MvccError::SerializationFailure {
key,
conflicting_ts,
} => TransactionError::Conflict {
key,
observed_seq: 0,
latest_seq: conflicting_ts,
},
MvccError::RollbackRecord { key } => TransactionError::Conflict {
key,
observed_seq: 0,
latest_seq: 0,
},
MvccError::PrimaryLockMissing { .. } | MvccError::PrimaryLockMismatch => {
TransactionError::Io(std::io::Error::other(
"transaction lost its primary lock, which means another writer \
resolved it; retry the transaction",
))
}
MvccError::StorageError(msg) => TransactionError::Io(std::io::Error::other(msg)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn txns(dir: &TempDir) -> (crate::Db, MvccTransactions) {
let db = crate::Db::open(dir.path(), crate::Options::default()).unwrap();
let t = MvccTransactions::new(db.engine_arc(), DurabilityMode::Eventual);
(db, t)
}
#[test]
fn a_committed_transaction_is_visible_to_a_plain_db_read() {
let dir = TempDir::new().unwrap();
let (db, txns) = txns(&dir);
let mut tx = txns.begin(IsolationLevel::SnapshotIsolation);
tx.put(b"projected", b"value").unwrap();
tx.commit().unwrap();
assert_eq!(
db.get(b"projected").unwrap().as_deref(),
Some(&b"value"[..]),
"a committed transactional write must be visible to Db::get"
);
let mut tx = txns.begin(IsolationLevel::SnapshotIsolation);
tx.delete(b"projected").unwrap();
tx.commit().unwrap();
assert_eq!(
db.get(b"projected").unwrap(),
None,
"a committed transactional delete must be visible to Db::get"
);
}
#[test]
fn a_rollback_record_projects_nothing() {
let dir = TempDir::new().unwrap();
let (db, txns) = txns(&dir);
let mut tx = txns.begin(IsolationLevel::SnapshotIsolation);
tx.put(b"abandoned", b"value").unwrap();
drop(tx);
assert_eq!(db.get(b"abandoned").unwrap(), None);
}
#[test]
fn a_committed_write_is_visible_to_the_next_transaction() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(b"k", b"v").unwrap();
w.commit().expect("commit");
let mut r = t.begin(IsolationLevel::SnapshotIsolation);
assert_eq!(r.get(b"k").unwrap().as_deref(), Some(&b"v"[..]));
}
#[test]
fn a_transaction_reads_its_own_writes() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(b"k", b"first").unwrap();
assert_eq!(w.get(b"k").unwrap().as_deref(), Some(&b"first"[..]));
w.put(b"k", b"second").unwrap();
assert_eq!(w.get(b"k").unwrap().as_deref(), Some(&b"second"[..]));
w.commit().unwrap();
}
#[test]
fn an_uncommitted_write_is_invisible_to_another_transaction() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(b"k", b"pending").unwrap();
let mut r = t.begin(IsolationLevel::SnapshotIsolation);
assert_eq!(
r.get(b"k").unwrap(),
None,
"prewritten data must not be visible"
);
w.commit().unwrap();
}
#[test]
fn a_delete_hides_a_committed_value() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(b"k", b"v").unwrap();
w.commit().unwrap();
let mut d = t.begin(IsolationLevel::SnapshotIsolation);
d.delete(b"k").unwrap();
d.commit().unwrap();
let mut r = t.begin(IsolationLevel::SnapshotIsolation);
assert_eq!(r.get(b"k").unwrap(), None);
}
#[test]
fn a_binary_key_survives_the_round_trip_through_a_transaction() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let k = [0x00u8, 0xff, 0x80, b'/', 0xc8];
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(&k, b"binary").unwrap();
w.commit().unwrap();
let mut r = t.begin(IsolationLevel::SnapshotIsolation);
assert_eq!(r.get(&k).unwrap().as_deref(), Some(&b"binary"[..]));
assert_eq!(r.get(b"00ff802fc8").unwrap(), None);
}
#[test]
fn two_transactions_writing_the_same_key_do_not_both_commit() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut a = t.begin(IsolationLevel::SnapshotIsolation);
let mut b = t.begin(IsolationLevel::SnapshotIsolation);
a.put(b"k", b"a").unwrap();
b.put(b"k", b"b").unwrap();
let first = a.commit();
let second = b.commit();
assert!(
first.is_ok() != second.is_ok(),
"exactly one of two writers to the same key must commit; got {first:?} and {second:?}"
);
}
#[test]
fn a_commit_reports_a_sequence_that_moves_forward() {
let dir = TempDir::new().unwrap();
let (_db, t) = txns(&dir);
let mut last = 0;
for i in 0..8u8 {
let mut w = t.begin(IsolationLevel::SnapshotIsolation);
w.put(&[i], b"v").unwrap();
let ts = w.commit().unwrap();
assert!(ts > last, "commit {i} reported {ts}, not after {last}");
last = ts;
}
}
}