use crate::portability::{AtomicBool, AtomicU64, Ordering};
use kovan_queue::seg_queue::SegQueue;
use crate::txn_buffer::TxnBuffer;
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use crate::sync::{Condvar, Mutex};
use crate::column_family::{DEFAULT_CF_ID, prefix_key};
use crate::engine::{CommitOutcome, ConflictKey, RegolithEngine};
use crate::{Db, DbSlice, Error, Options, Result};
const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Debug, thiserror::Error)]
pub enum TransactionError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(
"transaction conflict on key {key:?}: observed seq {observed_seq}, latest seq {latest_seq}"
)]
Conflict {
key: Vec<u8>,
observed_seq: u64,
latest_seq: u64,
},
#[error("transaction busy acquiring lock on key {0:?}")]
Busy(Vec<u8>),
#[error("no savepoint to roll back to")]
NoSavepoint,
#[error("transactional range deletes are not supported")]
UnsupportedRangeDelete,
}
pub type TxResult<T> = std::result::Result<T, TransactionError>;
impl From<Error> for TransactionError {
fn from(e: Error) -> Self {
match e {
Error::Io(io) => TransactionError::Io(io),
Error::Corruption(io) => TransactionError::Io(io),
Error::InvalidArgument(message) | Error::InvalidColumnFamily(message) => {
TransactionError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
message,
))
}
Error::ReadOnly => TransactionError::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"database was opened read-only",
)),
Error::Closed => TransactionError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"database is closed",
)),
other => TransactionError::Io(std::io::Error::other(other.to_string())),
}
}
}
pub struct OptimisticTransactionDb {
isolation: IsolationLevel,
inner: Db,
}
impl std::fmt::Debug for OptimisticTransactionDb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OptimisticTransactionDb")
.finish_non_exhaustive()
}
}
impl OptimisticTransactionDb {
pub fn open<P: AsRef<Path>>(path: P, opts: Options) -> Result<Self> {
Ok(Self {
inner: Db::open(path, opts)?,
isolation: IsolationLevel::default(),
})
}
pub fn db(&self) -> &Db {
&self.inner
}
pub fn begin_transaction(&self) -> Transaction<'_> {
self.begin_transaction_with(self.isolation)
}
pub fn begin_transaction_with(&self, isolation: IsolationLevel) -> Transaction<'_> {
self.begin_inner(isolation)
}
pub fn begin_transaction_owned(
self: &Arc<Self>,
isolation: IsolationLevel,
) -> OwnedTransaction {
OwnedTransaction::new(self.begin_inner(isolation), Arc::clone(self) as Arc<_>)
}
fn begin_inner<'any>(&self, isolation: IsolationLevel) -> Transaction<'any> {
let engine = self.inner.engine_arc();
let snapshot_seq = engine.register_snapshot_at_horizon();
Transaction::new(
engine,
snapshot_seq,
self.inner.durability(),
TxMode::Optimistic,
None,
DEFAULT_LOCK_TIMEOUT,
isolation,
self.inner.transaction_keys_inline(),
)
}
pub fn with_isolation(mut self, isolation: IsolationLevel) -> Self {
self.isolation = isolation;
self
}
pub fn isolation(&self) -> IsolationLevel {
self.isolation
}
}
pub struct TransactionDb {
isolation: IsolationLevel,
inner: Db,
lock_manager: Arc<LockManager>,
tx_id: AtomicU64,
lock_timeout: Duration,
}
impl std::fmt::Debug for TransactionDb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransactionDb")
.field("lock_timeout", &self.lock_timeout)
.finish_non_exhaustive()
}
}
impl TransactionDb {
pub fn open<P: AsRef<Path>>(path: P, opts: Options) -> Result<Self> {
Ok(Self {
inner: Db::open(path, opts)?,
isolation: IsolationLevel::default(),
lock_manager: Arc::new(LockManager::new()),
tx_id: AtomicU64::new(1),
lock_timeout: DEFAULT_LOCK_TIMEOUT,
})
}
pub fn db(&self) -> &Db {
&self.inner
}
pub fn with_lock_timeout(mut self, timeout: Duration) -> Self {
self.lock_timeout = timeout;
self
}
pub fn begin_transaction(&self) -> Transaction<'_> {
self.begin_transaction_with(self.isolation)
}
pub fn begin_transaction_with(&self, isolation: IsolationLevel) -> Transaction<'_> {
self.begin_inner(isolation)
}
pub fn begin_transaction_owned(
self: &Arc<Self>,
isolation: IsolationLevel,
) -> OwnedTransaction {
OwnedTransaction::new(self.begin_inner(isolation), Arc::clone(self) as Arc<_>)
}
fn begin_inner<'any>(&self, isolation: IsolationLevel) -> Transaction<'any> {
let engine = self.inner.engine_arc();
let snapshot_seq = engine.register_snapshot_at_horizon();
let id = self.tx_id.fetch_add(1, Ordering::Relaxed);
Transaction::new(
engine,
snapshot_seq,
self.inner.durability(),
TxMode::Pessimistic { tx_id: id },
Some(Arc::clone(&self.lock_manager)),
self.lock_timeout,
isolation,
self.inner.transaction_keys_inline(),
)
}
pub fn with_isolation(mut self, isolation: IsolationLevel) -> Self {
self.isolation = isolation;
self
}
pub fn isolation(&self) -> IsolationLevel {
self.isolation
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScanDirection {
#[default]
Forward,
Reverse,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IsolationLevel {
ReadCommitted,
#[default]
SnapshotIsolation,
Serializable,
}
#[derive(Clone, Copy)]
enum TxMode {
Optimistic,
Pessimistic { tx_id: u64 },
}
pub struct Transaction<'db> {
engine: Arc<RegolithEngine>,
isolation: IsolationLevel,
snapshot_seq: u64,
durability: crate::engine::DurabilityMode,
mode: TxMode,
writes: TxnBuffer<Vec<u8>, Option<Vec<u8>>>,
range_deletes: SegQueue<(Vec<u8>, Vec<u8>)>,
merges: SegQueue<(Vec<u8>, Vec<u8>)>,
tracked: TxnBuffer<Vec<u8>, Arc<KeyState>>,
savepoints: Vec<Savepoint>,
held_locks: TxnBuffer<Vec<u8>, ()>,
lock_manager: Option<Arc<LockManager>>,
lock_timeout: Duration,
keys_inline: usize,
resolved: bool,
resources_released: bool,
_phantom: std::marker::PhantomData<&'db ()>,
}
#[derive(Clone)]
struct Savepoint {
writes: BTreeMap<Vec<u8>, Option<Vec<u8>>>,
range_deletes: Vec<(Vec<u8>, Vec<u8>)>,
merges: Vec<(Vec<u8>, Vec<u8>)>,
held_lock_count: usize,
}
struct KeyState {
first_read_seq: u64,
read_seq: AtomicU64,
for_update: AtomicBool,
}
impl KeyState {
fn new(horizon: u64, for_update: bool) -> Self {
Self {
first_read_seq: horizon,
read_seq: AtomicU64::new(horizon),
for_update: AtomicBool::new(for_update),
}
}
}
impl<'db> Transaction<'db> {
#[allow(clippy::too_many_arguments)]
fn new(
engine: Arc<RegolithEngine>,
snapshot_seq: u64,
durability: crate::engine::DurabilityMode,
mode: TxMode,
lock_manager: Option<Arc<LockManager>>,
lock_timeout: Duration,
isolation: IsolationLevel,
keys_inline: usize,
) -> Self {
Self {
engine,
snapshot_seq,
durability,
mode,
isolation,
writes: TxnBuffer::new(keys_inline),
range_deletes: SegQueue::new(),
merges: SegQueue::new(),
tracked: TxnBuffer::new(keys_inline),
savepoints: Vec::new(),
held_locks: TxnBuffer::new(keys_inline),
lock_manager,
lock_timeout,
keys_inline,
resolved: false,
resources_released: false,
_phantom: std::marker::PhantomData,
}
}
pub fn get(&self, key: &[u8]) -> TxResult<Option<Vec<u8>>> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
if let Some(buffered) = self.writes.get(&prefixed) {
return Ok(buffered);
}
let read_seq = self.observe(&prefixed, self.snapshot_seq, false);
self.engine
.get_at(&prefixed, read_seq)
.map_err(TransactionError::Io)
}
pub fn get_slice(&self, key: &[u8]) -> TxResult<Option<DbSlice>> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
if let Some(buffered) = self.writes.get(&prefixed) {
return Ok(buffered.map(DbSlice::from));
}
let read_seq = self.observe(&prefixed, self.snapshot_seq, false);
self.engine
.get_slice_at(&prefixed, read_seq)
.map_err(TransactionError::Io)
}
pub fn scan_stream(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> TxnScanStream<'_> {
self.scan_stream_in(start, end, ScanDirection::Forward)
}
pub fn scan_stream_in(
&self,
start: Option<&[u8]>,
end: Option<&[u8]>,
direction: ScanDirection,
) -> TxnScanStream<'_> {
let lo = start.map(|s| prefix_key(DEFAULT_CF_ID, s));
let hi = end.map(|e| prefix_key(DEFAULT_CF_ID, e));
let reverse = direction == ScanDirection::Reverse;
let mut buffered: Vec<(Vec<u8>, Option<Vec<u8>>)> = self
.writes
.snapshot()
.into_iter()
.filter(|(key, _)| {
lo.as_ref().is_none_or(|lo| key >= lo) && hi.as_ref().is_none_or(|hi| key < hi)
})
.collect();
if reverse {
buffered.sort_unstable_by(|a, b| b.0.cmp(&a.0));
} else {
buffered.sort_unstable_by(|a, b| a.0.cmp(&b.0));
}
let mut cursor = crate::CfIter::new(
crate::Iter::from_internal(self.engine.new_iter_at(self.snapshot_seq)),
DEFAULT_CF_ID,
);
if reverse {
match &hi {
Some(hi) => {
let end = &hi[4..];
cursor.seek_for_prev(end);
if cursor.valid() && cursor.key() == Some(end) {
cursor.prev();
}
}
None => cursor.seek_to_last(),
}
} else {
match &lo {
Some(lo) => cursor.seek(&lo[4..]),
None => cursor.seek_to_first(),
}
}
TxnScanStream {
cursor,
cursor_done: false,
buffered: buffered.into_iter().peekable(),
start: lo.map(|lo| lo[4..].to_vec()),
end: hi.map(|hi| hi[4..].to_vec()),
reverse,
}
}
pub fn get_for_update(&self, key: &[u8]) -> TxResult<Option<Vec<u8>>> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
let already_held = self.lock_key(&prefixed)?;
let horizon = self.read_horizon(&prefixed, already_held);
let read_seq = self.observe(&prefixed, horizon, true);
if let Some(buffered) = self.writes.get(&prefixed) {
return Ok(buffered);
}
self.engine
.get_at(&prefixed, read_seq)
.map_err(TransactionError::Io)
}
pub fn put(&self, key: &[u8], value: &[u8]) -> TxResult<()> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
self.lock_key(&prefixed)?;
self.writes.insert(prefixed, Some(value.to_vec()));
Ok(())
}
pub fn delete(&self, key: &[u8]) -> TxResult<()> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
self.lock_key(&prefixed)?;
self.writes.insert(prefixed, None);
Ok(())
}
pub fn delete_range(&self, start: &[u8], end: &[u8]) -> TxResult<()> {
if start >= end {
return Ok(());
}
Err(TransactionError::UnsupportedRangeDelete)
}
pub fn merge(&self, key: &[u8], operand: &[u8]) -> TxResult<()> {
let prefixed = prefix_key(DEFAULT_CF_ID, key);
self.lock_key(&prefixed)?;
self.merges.push((prefixed, operand.to_vec()));
Ok(())
}
pub fn set_savepoint(&mut self) {
self.savepoints.push(Savepoint {
writes: self.writes.snapshot().into_iter().collect(),
range_deletes: drain(&self.range_deletes),
merges: drain(&self.merges),
held_lock_count: self.held_locks.len(),
});
if let Some(sp) = self.savepoints.last() {
for entry in &sp.range_deletes {
self.range_deletes.push(entry.clone());
}
for entry in &sp.merges {
self.merges.push(entry.clone());
}
}
}
pub fn rollback_to_savepoint(&mut self) -> TxResult<()> {
let sp = self.savepoints.pop().ok_or(TransactionError::NoSavepoint)?;
self.writes = TxnBuffer::new(self.keys_inline);
for (key, value) in sp.writes {
self.writes.insert(key, value);
}
self.range_deletes = SegQueue::new();
for entry in sp.range_deletes {
self.range_deletes.push(entry);
}
self.merges = SegQueue::new();
for entry in sp.merges {
self.merges.push(entry);
}
let _ = sp.held_lock_count;
Ok(())
}
pub fn commit(mut self) -> TxResult<()> {
let result = self.commit_inner();
self.resolved = true;
result
}
pub fn rollback(mut self) {
self.resolved = true;
self.release_resources();
}
fn commit_inner(&mut self) -> TxResult<()> {
let mut writes: BTreeMap<Vec<u8>, Option<Vec<u8>>> = BTreeMap::new();
for (key, value) in self.writes.drain() {
writes.entry(key).or_insert(value);
}
let range_deletes = drain(&self.range_deletes);
let merges = drain(&self.merges);
let mut seen = std::collections::HashSet::new();
let tracked: Vec<(Vec<u8>, Arc<KeyState>)> = self
.tracked
.drain()
.into_iter()
.filter(|(key, _)| seen.insert(key.clone()))
.collect();
let conflict_keys = self.validation_set(tracked, &writes, &merges);
let outcome = self
.engine
.commit_with_conflict_check(
&conflict_keys,
writes,
range_deletes,
merges,
self.durability,
)
.map_err(TransactionError::Io)?;
match outcome {
CommitOutcome::Ok => Ok(()),
CommitOutcome::Conflict {
key,
observed_seq,
latest_seq,
} => Err(TransactionError::Conflict {
key: strip_cf_prefix(key),
observed_seq,
latest_seq,
}),
}
}
fn validation_set(
&self,
tracked: Vec<(Vec<u8>, Arc<KeyState>)>,
writes: &BTreeMap<Vec<u8>, Option<Vec<u8>>>,
merges: &[(Vec<u8>, Vec<u8>)],
) -> Vec<ConflictKey> {
let optimistic = matches!(self.mode, TxMode::Optimistic);
let serializable = self.isolation == IsolationLevel::Serializable;
let read_committed = self.isolation == IsolationLevel::ReadCommitted;
let mut checks: BTreeMap<Vec<u8>, (u64, bool)> = BTreeMap::new();
for (key, state) in tracked {
let written =
writes.contains_key(&key) || merges.iter().any(|(merged, _)| *merged == key);
let validate = if serializable {
true
} else if read_committed {
written
} else {
state.for_update.load(Ordering::Acquire) || written
};
if validate {
checks.insert(key, (state.first_read_seq, true));
}
}
if optimistic {
for key in writes.keys() {
checks
.entry(key.clone())
.or_insert((self.snapshot_seq, false));
}
for (key, _) in merges {
checks
.entry(key.clone())
.or_insert((self.snapshot_seq, false));
}
}
checks
.into_iter()
.map(|(key, (observed_seq, read))| ConflictKey {
key,
observed_seq,
read,
})
.collect()
}
fn lock_key(&self, key: &[u8]) -> TxResult<bool> {
match self.mode {
TxMode::Optimistic => Ok(false),
TxMode::Pessimistic { tx_id } => self.acquire_lock(key, tx_id),
}
}
fn read_horizon(&self, key: &[u8], already_held: bool) -> u64 {
match self.mode {
TxMode::Optimistic => self.snapshot_seq,
TxMode::Pessimistic { .. } => {
if already_held && let Some(state) = self.tracked.get(key) {
return state.read_seq.load(Ordering::Acquire);
}
self.engine.snapshot_seq()
}
}
}
fn observe(&self, key: &[u8], horizon: u64, for_update: bool) -> u64 {
let state = self
.tracked
.get_or_insert(key.to_vec(), Arc::new(KeyState::new(horizon, for_update)));
if for_update {
state.for_update.store(true, Ordering::Release);
}
state
.read_seq
.fetch_max(horizon, Ordering::AcqRel)
.max(horizon)
}
fn acquire_lock(&self, key: &[u8], tx_id: u64) -> TxResult<bool> {
let Some(lm) = self.lock_manager.as_ref() else {
return Ok(false);
};
if self.held_locks.get(key).is_some() {
return Ok(true);
}
lm.acquire(key, tx_id, self.lock_timeout)
.map_err(|_| TransactionError::Busy(strip_cf_prefix(key.to_vec())))?;
self.held_locks.insert(key.to_vec(), ());
Ok(false)
}
fn release_resources(&mut self) {
if self.resources_released {
return;
}
self.resources_released = true;
if let Some(lm) = self.lock_manager.as_ref()
&& let TxMode::Pessimistic { tx_id } = self.mode
{
for (key, ()) in self.held_locks.drain() {
lm.release(&key, tx_id);
}
}
self.engine.release_snapshot(self.snapshot_seq);
}
}
type BufferedWrites = std::iter::Peekable<std::vec::IntoIter<(Vec<u8>, Option<Vec<u8>>)>>;
pub struct TxnScanStream<'txn> {
cursor: crate::CfIter<'txn>,
cursor_done: bool,
buffered: BufferedWrites,
start: Option<Vec<u8>>,
end: Option<Vec<u8>>,
reverse: bool,
}
impl TxnScanStream<'_> {
fn peek_cursor(&mut self) -> Option<Vec<u8>> {
if self.cursor_done || !self.cursor.valid() {
return None;
}
let key = self.cursor.key()?.to_vec();
let past_bound = if self.reverse {
self.start
.as_ref()
.is_some_and(|start| key.as_slice() < start.as_slice())
} else {
self.end
.as_ref()
.is_some_and(|end| key.as_slice() >= end.as_slice())
};
if past_bound {
self.cursor_done = true;
return None;
}
Some(key)
}
fn step_cursor(&mut self) {
if self.reverse {
self.cursor.prev();
} else {
self.cursor.next();
}
}
fn precedes(&self, first: &[u8], second: &[u8]) -> std::cmp::Ordering {
if self.reverse {
second.cmp(first)
} else {
first.cmp(second)
}
}
}
impl Iterator for TxnScanStream<'_> {
type Item = (Vec<u8>, DbSlice);
fn next(&mut self) -> Option<Self::Item> {
loop {
let cursor_key = self.peek_cursor();
let buffered_key = self.buffered.peek().map(|(key, _)| key[4..].to_vec());
match (cursor_key, buffered_key) {
(None, None) => return None,
(None, Some(_)) => {
let (key, value) = self.buffered.next()?;
if let Some(value) = value {
return Some((key[4..].to_vec(), DbSlice::from(value)));
}
}
(Some(key), None) => {
let value = self.cursor.value_slice()?;
self.step_cursor();
return Some((key, value));
}
(Some(ckey), Some(bkey)) => match self.precedes(&ckey, &bkey) {
std::cmp::Ordering::Less => {
let value = self.cursor.value_slice()?;
self.step_cursor();
return Some((ckey, value));
}
std::cmp::Ordering::Greater => {
let (key, value) = self.buffered.next()?;
if let Some(value) = value {
return Some((key[4..].to_vec(), DbSlice::from(value)));
}
}
std::cmp::Ordering::Equal => {
self.step_cursor();
let (key, value) = self.buffered.next()?;
if let Some(value) = value {
return Some((key[4..].to_vec(), DbSlice::from(value)));
}
}
},
}
}
}
}
impl std::fmt::Debug for TxnScanStream<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TxnScanStream").finish_non_exhaustive()
}
}
fn drain<T: 'static>(queue: &SegQueue<T>) -> Vec<T> {
let mut drained = Vec::with_capacity(queue.len());
while let Some(entry) = queue.pop() {
drained.push(entry);
}
drained
}
fn strip_cf_prefix(key: Vec<u8>) -> Vec<u8> {
if key.len() >= 4 {
key[4..].to_vec()
} else {
key
}
}
pub struct OwnedTransaction {
txn: Transaction<'static>,
_db: Arc<dyn core::any::Any + Send + Sync>,
}
impl OwnedTransaction {
fn new(txn: Transaction<'static>, db: Arc<dyn core::any::Any + Send + Sync>) -> Self {
Self { txn, _db: db }
}
pub fn commit(self) -> TxResult<()> {
self.txn.commit()
}
pub fn rollback(self) {
self.txn.rollback();
}
}
impl core::ops::Deref for OwnedTransaction {
type Target = Transaction<'static>;
fn deref(&self) -> &Self::Target {
&self.txn
}
}
impl core::ops::DerefMut for OwnedTransaction {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.txn
}
}
impl std::fmt::Debug for OwnedTransaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedTransaction").finish_non_exhaustive()
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if !self.resolved {
self.resolved = true;
}
self.release_resources();
}
}
struct LockManager {
locks: Mutex<HashMap<Vec<u8>, u64>>,
cvar: Condvar,
}
impl LockManager {
fn new() -> Self {
Self {
locks: Mutex::new(HashMap::new()),
cvar: Condvar::new(),
}
}
fn acquire(&self, key: &[u8], tx_id: u64, timeout: Duration) -> std::result::Result<(), ()> {
let deadline =
crate::env::platform_micros().map(|now| now.saturating_add(timeout.as_micros() as u64));
let mut guard = self.locks.lock();
loop {
match guard.get(key) {
Some(&holder) if holder == tx_id => {
return Ok(());
}
Some(_) => {
let remaining = match (deadline, crate::env::platform_micros()) {
(Some(deadline), Some(now)) => {
if now >= deadline {
return Err(());
}
Duration::from_micros(deadline - now)
}
_ => timeout,
};
let (next, result) = self
.cvar
.wait_timeout(guard, remaining)
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard = next;
if result.timed_out() && guard.get(key).is_some_and(|&h| h != tx_id) {
return Err(());
}
}
None => {
guard.insert(key.to_vec(), tx_id);
return Ok(());
}
}
}
}
fn release(&self, key: &[u8], tx_id: u64) {
let mut guard = self.locks.lock();
if let Some(&holder) = guard.get(key)
&& holder == tx_id
{
guard.remove(key);
self.cvar.notify_all();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn opt_db() -> (OptimisticTransactionDb, TempDir) {
let dir = TempDir::new().unwrap();
let db = OptimisticTransactionDb::open(dir.path(), Options::default()).unwrap();
(db, dir)
}
fn pes_db() -> (TransactionDb, TempDir) {
let dir = TempDir::new().unwrap();
let db = TransactionDb::open(dir.path(), Options::default()).unwrap();
(db, dir)
}
#[test]
fn optimistic_basic_put_commit_read() {
let (db, _dir) = opt_db();
let tx = db.begin_transaction();
tx.put(b"k", b"v").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn optimistic_read_your_own_writes() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"initial").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get(b"k").unwrap(), Some(b"initial".to_vec()));
tx.put(b"k", b"staged").unwrap();
assert_eq!(tx.get(b"k").unwrap(), Some(b"staged".to_vec()));
assert_eq!(db.db().get(b"k").unwrap(), Some(b"initial".to_vec()));
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"staged".to_vec()));
}
#[test]
fn optimistic_rollback_discards_writes() {
let (db, _dir) = opt_db();
let tx = db.begin_transaction();
tx.put(b"k", b"never").unwrap();
tx.rollback();
assert_eq!(db.db().get(b"k").unwrap(), None);
}
#[test]
fn optimistic_rollback_releases_shared_snapshot_pin_once() {
let (db, _dir) = opt_db();
let tx1 = db.begin_transaction();
let tx2 = db.begin_transaction();
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(2));
tx1.rollback();
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(1));
drop(tx2);
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(0));
}
#[test]
fn optimistic_conflict_detected() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v0").unwrap();
let tx1 = db.begin_transaction();
assert_eq!(tx1.get(b"k").unwrap(), Some(b"v0".to_vec()));
db.db().put(b"k", b"v1").unwrap();
tx1.put(b"k", b"v2").unwrap();
match tx1.commit() {
Err(TransactionError::Conflict { key, .. }) => {
assert_eq!(key, b"k".to_vec());
}
other => panic!("expected conflict, got {other:?}"),
}
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v1".to_vec()));
}
#[test]
fn optimistic_get_for_update_tracks_conflicts() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
db.db().put(b"k", b"v1").unwrap();
tx.put(b"other", b"stuff").unwrap();
match tx.commit() {
Err(TransactionError::Conflict { key, .. }) => {
assert_eq!(key, b"k".to_vec());
}
other => panic!("expected conflict, got {other:?}"),
}
}
#[test]
fn optimistic_conflict_reports_the_lowest_conflicting_key() {
let (db, _dir) = opt_db();
db.db().put(b"a", b"v0").unwrap();
db.db().put(b"z", b"v0").unwrap();
let tx = db.begin_transaction();
tx.get_for_update(b"z").unwrap();
tx.get_for_update(b"a").unwrap();
db.db().put(b"z", b"v1").unwrap();
db.db().put(b"a", b"v1").unwrap();
match tx.commit() {
Err(TransactionError::Conflict { key, .. }) => assert_eq!(key, b"a".to_vec()),
other => panic!("expected conflict, got {other:?}"),
}
}
#[test]
fn optimistic_no_conflict_passes() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
tx.put(b"other", b"stuff").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"other").unwrap(), Some(b"stuff".to_vec()));
}
#[test]
fn optimistic_snapshot_isolation_reads() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
db.db().put(b"k", b"v1").unwrap();
assert_eq!(tx.get(b"k").unwrap(), Some(b"v0".to_vec()));
}
#[test]
fn optimistic_savepoint_rollback() {
let (db, _dir) = opt_db();
let mut tx = db.begin_transaction();
tx.put(b"a", b"1").unwrap();
tx.set_savepoint();
tx.put(b"b", b"2").unwrap();
tx.put(b"c", b"3").unwrap();
tx.rollback_to_savepoint().unwrap();
assert_eq!(tx.get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(tx.get(b"b").unwrap(), None);
assert_eq!(tx.get(b"c").unwrap(), None);
tx.commit().unwrap();
assert_eq!(db.db().get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(db.db().get(b"b").unwrap(), None);
}
#[test]
fn optimistic_rollback_to_savepoint_without_savepoint_errors() {
let (db, _dir) = opt_db();
let mut tx = db.begin_transaction();
assert!(matches!(
tx.rollback_to_savepoint(),
Err(TransactionError::NoSavepoint)
));
}
#[test]
fn optimistic_delete_commit() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v").unwrap();
let tx = db.begin_transaction();
tx.delete(b"k").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), None);
}
#[test]
fn optimistic_range_delete_is_rejected() {
let (db, _dir) = opt_db();
let tx = db.begin_transaction();
assert!(matches!(
tx.delete_range(b"a", b"z"),
Err(TransactionError::UnsupportedRangeDelete)
));
assert!(tx.delete_range(b"z", b"a").is_ok());
}
#[test]
fn pessimistic_basic_put_commit_read() {
let (db, _dir) = pes_db();
let tx = db.begin_transaction();
tx.put(b"k", b"v").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn pessimistic_range_delete_is_rejected() {
let (db, _dir) = pes_db();
let tx = db.begin_transaction();
assert!(matches!(
tx.delete_range(b"a", b"z"),
Err(TransactionError::UnsupportedRangeDelete)
));
assert!(tx.delete_range(b"z", b"a").is_ok());
}
#[test]
fn pessimistic_lock_blocks_second_writer() {
let (db, _dir) = pes_db();
let db = Arc::new(db);
let tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
let db2 = Arc::clone(&db);
let join = std::thread::spawn(move || {
let tx2 = db2.begin_transaction();
tx2.put(b"k", b"v2")
});
std::thread::sleep(std::time::Duration::from_millis(50));
tx1.commit().unwrap();
let result = join.join().unwrap();
assert!(result.is_ok(), "tx2 put should succeed once tx1 commits");
}
#[test]
fn pessimistic_lock_timeout_returns_busy() {
let dir = TempDir::new().unwrap();
let db = TransactionDb::open(dir.path(), Options::default())
.unwrap()
.with_lock_timeout(Duration::from_millis(50));
let db = Arc::new(db);
let tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
let db2 = Arc::clone(&db);
let join = std::thread::spawn(move || {
let tx2 = db2.begin_transaction();
tx2.put(b"k", b"v2")
});
let result = join.join().unwrap();
assert!(matches!(result, Err(TransactionError::Busy(_))));
tx1.rollback();
}
#[test]
fn pessimistic_reentrant_lock() {
let (db, _dir) = pes_db();
let tx = db.begin_transaction();
tx.put(b"k", b"v1").unwrap();
tx.put(b"k", b"v2").unwrap();
tx.get_for_update(b"k").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_rollback_releases_locks() {
let (db, _dir) = pes_db();
let db = Arc::new(db);
let tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
tx1.rollback();
let tx2 = db.begin_transaction();
tx2.put(b"k", b"v2").unwrap();
tx2.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_rollback_releases_shared_snapshot_pin_once() {
let (db, _dir) = pes_db();
let tx1 = db.begin_transaction();
let tx2 = db.begin_transaction();
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(2));
tx1.rollback();
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(1));
drop(tx2);
assert_eq!(db.db().get_int_property("regolith.num-snapshots"), Some(0));
}
#[test]
fn pessimistic_drop_releases_locks() {
let (db, _dir) = pes_db();
let db = Arc::new(db);
{
let tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
}
let tx2 = db.begin_transaction();
tx2.put(b"k", b"v2").unwrap();
tx2.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_read_your_own_writes() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"initial").unwrap();
let tx = db.begin_transaction();
tx.put(b"k", b"staged").unwrap();
assert_eq!(tx.get(b"k").unwrap(), Some(b"staged".to_vec()));
tx.commit().unwrap();
}
#[test]
fn pessimistic_get_for_update_locks() {
let dir = TempDir::new().unwrap();
let db = Arc::new(
TransactionDb::open(dir.path(), Options::default())
.unwrap()
.with_lock_timeout(Duration::from_millis(50)),
);
db.db().put(b"k", b"v0").unwrap();
let tx1 = db.begin_transaction();
assert_eq!(tx1.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
let db2 = Arc::clone(&db);
let join = std::thread::spawn(move || {
let tx2 = db2.begin_transaction();
tx2.put(b"k", b"v1")
});
let result = join.join().unwrap();
assert!(matches!(result, Err(TransactionError::Busy(_))));
tx1.rollback();
}
#[test]
fn pessimistic_savepoint_keeps_locks_but_rolls_back_writes() {
let (db, _dir) = pes_db();
let mut tx = db.begin_transaction();
tx.put(b"a", b"1").unwrap();
tx.set_savepoint();
tx.put(b"b", b"2").unwrap();
tx.rollback_to_savepoint().unwrap();
assert_eq!(tx.get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(tx.get(b"b").unwrap(), None);
tx.commit().unwrap();
assert_eq!(db.db().get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(db.db().get(b"b").unwrap(), None);
}
#[test]
fn pessimistic_get_for_update_sees_writes_committed_after_begin() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
db.db().put(b"k", b"v1").unwrap();
assert_eq!(tx.get_for_update(b"k").unwrap(), Some(b"v1".to_vec()));
assert_eq!(tx.get(b"k").unwrap(), Some(b"v1".to_vec()));
tx.put(b"k", b"v2").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_second_locker_does_not_observe_precommit_value() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx1 = db.begin_transaction();
let tx2 = db.begin_transaction();
assert_eq!(tx1.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
tx1.put(b"k", b"v1").unwrap();
tx1.commit().unwrap();
assert_eq!(tx2.get_for_update(b"k").unwrap(), Some(b"v1".to_vec()));
tx2.put(b"k", b"v2").unwrap();
tx2.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_commit_detects_write_from_outside_the_lock_manager() {
let (db, _dir) = pes_db();
let tx = db.begin_transaction();
tx.get_for_update(b"k").unwrap();
db.db().put(b"k", b"racer").unwrap();
tx.put(b"k", b"mine").unwrap();
match tx.commit() {
Err(TransactionError::Conflict { key, .. }) => assert_eq!(key, b"k".to_vec()),
other => panic!("expected conflict, got {other:?}"),
}
assert_eq!(db.db().get(b"k").unwrap(), Some(b"racer".to_vec()));
}
#[test]
fn pessimistic_sequential_transactions_do_not_conflict() {
let (db, _dir) = pes_db();
let tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
tx1.commit().unwrap();
let tx2 = db.begin_transaction();
assert_eq!(tx2.get_for_update(b"k").unwrap(), Some(b"v1".to_vec()));
tx2.put(b"k", b"v2").unwrap();
tx2.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_blind_put_after_external_write_is_not_a_conflict() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
db.db().put(b"k", b"v1").unwrap();
tx.put(b"k", b"v2").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_blind_put_before_external_write_is_not_a_conflict() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
tx.put(b"k", b"v2").unwrap();
db.db().put(b"k", b"v1").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v2".to_vec()));
}
#[test]
fn pessimistic_savepoint_rollback_keeps_untouched_keys_unvalidated() {
let (db, _dir) = pes_db();
let mut tx = db.begin_transaction();
tx.set_savepoint();
tx.put(b"b", b"rolled-back").unwrap();
tx.rollback_to_savepoint().unwrap();
db.db().put(b"b", b"external").unwrap();
tx.put(b"a", b"1").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(db.db().get(b"b").unwrap(), Some(b"external".to_vec()));
}
#[test]
fn pessimistic_savepoint_rollback_keeps_the_read_anchor() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let mut tx = db.begin_transaction();
assert_eq!(tx.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
tx.set_savepoint();
tx.put(b"k", b"rolled-back").unwrap();
tx.rollback_to_savepoint().unwrap();
db.db().put(b"k", b"racer").unwrap();
tx.put(b"k", b"mine").unwrap();
let err = tx.commit().expect_err("the bypassing write must be caught");
assert!(matches!(err, TransactionError::Conflict { .. }), "{err:?}");
assert_eq!(db.db().get(b"k").unwrap(), Some(b"racer".to_vec()));
}
#[test]
fn pessimistic_reads_do_not_travel_backwards_after_a_savepoint_rollback() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let mut tx = db.begin_transaction();
db.db().put(b"k", b"v1").unwrap();
let first = tx.get_for_update(b"k").unwrap();
assert_eq!(first, Some(b"v1".to_vec()));
tx.set_savepoint();
tx.put(b"k", b"staged").unwrap();
tx.rollback_to_savepoint().unwrap();
assert_eq!(tx.get(b"k").unwrap(), first);
}
#[test]
fn pessimistic_read_then_write_detects_a_concurrent_write() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get(b"k").unwrap(), Some(b"v0".to_vec()));
db.db().put(b"k", b"v1").unwrap();
tx.put(b"k", b"derived-from-v0").unwrap();
let err = tx.commit().expect_err("the stale read must be caught");
assert!(matches!(err, TransactionError::Conflict { .. }), "{err:?}");
assert_eq!(db.db().get(b"k").unwrap(), Some(b"v1".to_vec()));
}
#[test]
fn pessimistic_read_without_a_write_is_not_validated() {
let (db, _dir) = pes_db();
db.db().put(b"read-only", b"v0").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get(b"read-only").unwrap(), Some(b"v0".to_vec()));
db.db().put(b"read-only", b"v1").unwrap();
tx.put(b"other", b"1").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"other").unwrap(), Some(b"1".to_vec()));
}
#[test]
fn pessimistic_range_delete_over_a_tracked_key_is_a_conflict() {
let (db, _dir) = pes_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
db.db().delete_range(b"a", b"z").unwrap();
tx.put(b"k", b"resurrected").unwrap();
let err = tx
.commit()
.expect_err("a range delete over a tracked key is a conflict");
assert!(matches!(err, TransactionError::Conflict { .. }), "{err:?}");
assert_eq!(db.db().get(b"k").unwrap(), None);
}
#[test]
fn optimistic_range_delete_over_a_tracked_key_is_a_conflict() {
let (db, _dir) = opt_db();
db.db().put(b"k", b"v0").unwrap();
let tx = db.begin_transaction();
assert_eq!(tx.get_for_update(b"k").unwrap(), Some(b"v0".to_vec()));
db.db().delete_range(b"a", b"z").unwrap();
tx.put(b"k", b"resurrected").unwrap();
let err = tx
.commit()
.expect_err("a range delete over a tracked key is a conflict");
assert!(matches!(err, TransactionError::Conflict { .. }), "{err:?}");
assert_eq!(db.db().get(b"k").unwrap(), None);
}
#[test]
fn commit_is_atomic_with_respect_to_other_writers() {
let (db, _dir) = opt_db();
let tx = db.begin_transaction();
tx.put(b"a", b"1").unwrap();
tx.put(b"b", b"2").unwrap();
tx.put(b"c", b"3").unwrap();
tx.commit().unwrap();
assert_eq!(db.db().get(b"a").unwrap(), Some(b"1".to_vec()));
assert_eq!(db.db().get(b"b").unwrap(), Some(b"2".to_vec()));
assert_eq!(db.db().get(b"c").unwrap(), Some(b"3".to_vec()));
}
#[test]
fn multi_get_within_transaction() {
let (db, _dir) = opt_db();
db.db().put(b"a", b"1").unwrap();
db.db().put(b"b", b"2").unwrap();
let tx = db.begin_transaction();
tx.put(b"a", b"staged").unwrap();
assert_eq!(tx.get(b"a").unwrap(), Some(b"staged".to_vec()));
assert_eq!(tx.get(b"b").unwrap(), Some(b"2".to_vec()));
assert_eq!(tx.get(b"missing").unwrap(), None);
}
}