use crate::portability::{AtomicU64, Ordering};
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, RegolithEngine};
use crate::{Db, 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<'_> {
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,
)
}
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<'_> {
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,
)
}
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 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: BTreeMap<Vec<u8>, Option<Vec<u8>>>,
range_deletes: Vec<(Vec<u8>, Vec<u8>)>,
merges: Vec<(Vec<u8>, Vec<u8>)>,
tracked: BTreeMap<Vec<u8>, KeyState>,
savepoints: Vec<Savepoint>,
held_locks: Vec<Vec<u8>>,
lock_manager: Option<Arc<LockManager>>,
lock_timeout: Duration,
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,
}
#[derive(Clone, Copy)]
struct KeyState {
first_read_seq: u64,
read_seq: u64,
for_update: bool,
}
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,
) -> Self {
Self {
engine,
snapshot_seq,
durability,
mode,
isolation,
writes: BTreeMap::new(),
range_deletes: Vec::new(),
merges: Vec::new(),
tracked: BTreeMap::new(),
savepoints: Vec::new(),
held_locks: Vec::new(),
lock_manager,
lock_timeout,
resolved: false,
resources_released: false,
_phantom: std::marker::PhantomData,
}
}
pub fn get(&mut 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.clone());
}
let read_seq = self.observe(&prefixed, self.snapshot_seq, false);
self.engine
.get_at(&prefixed, read_seq)
.map_err(TransactionError::Io)
}
pub fn get_for_update(&mut 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.clone());
}
self.engine
.get_at(&prefixed, read_seq)
.map_err(TransactionError::Io)
}
pub fn put(&mut 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(&mut 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(&mut self, start: &[u8], end: &[u8]) -> TxResult<()> {
if start >= end {
return Ok(());
}
Err(TransactionError::UnsupportedRangeDelete)
}
pub fn merge(&mut 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.clone(),
range_deletes: self.range_deletes.clone(),
merges: self.merges.clone(),
held_lock_count: self.held_locks.len(),
});
}
pub fn rollback_to_savepoint(&mut self) -> TxResult<()> {
let sp = self.savepoints.pop().ok_or(TransactionError::NoSavepoint)?;
self.writes = sp.writes;
self.range_deletes = sp.range_deletes;
self.merges = sp.merges;
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 conflict_keys = self.validation_set();
let writes = std::mem::take(&mut self.writes);
let range_deletes = std::mem::take(&mut self.range_deletes);
let merges = std::mem::take(&mut self.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(&mut self) -> Vec<(Vec<u8>, u64)> {
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> = BTreeMap::new();
for (key, state) in std::mem::take(&mut self.tracked) {
let written = self.writes.contains_key(&key)
|| self.merges.iter().any(|(merged, _)| *merged == key);
let validate = if serializable {
true
} else if read_committed {
written
} else {
state.for_update || written
};
if validate {
checks.insert(key, state.first_read_seq);
}
}
if optimistic {
for key in self.writes.keys() {
checks.entry(key.clone()).or_insert(self.snapshot_seq);
}
for (key, _) in &self.merges {
checks.entry(key.clone()).or_insert(self.snapshot_seq);
}
}
checks.into_iter().collect()
}
fn lock_key(&mut 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;
}
self.engine.snapshot_seq()
}
}
}
fn observe(&mut self, key: &[u8], horizon: u64, for_update: bool) -> u64 {
match self.tracked.get_mut(key) {
Some(state) => {
state.read_seq = state.read_seq.max(horizon);
state.for_update |= for_update;
state.read_seq
}
None => {
self.tracked.insert(
key.to_vec(),
KeyState {
first_read_seq: horizon,
read_seq: horizon,
for_update,
},
);
horizon
}
}
}
fn acquire_lock(&mut self, key: &[u8], tx_id: u64) -> TxResult<bool> {
let Some(lm) = self.lock_manager.as_ref() else {
return Ok(false);
};
if self.held_locks.iter().any(|k| k.as_slice() == key) {
return Ok(true);
}
lm.acquire(key, tx_id, self.lock_timeout)
.map_err(|_| TransactionError::Busy(strip_cf_prefix(key.to_vec())))?;
self.held_locks.push(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);
}
}
fn strip_cf_prefix(key: Vec<u8>) -> Vec<u8> {
if key.len() >= 4 {
key[4..].to_vec()
} else {
key
}
}
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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
let db2 = Arc::clone(&db);
let join = std::thread::spawn(move || {
let mut 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 mut tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
let db2 = Arc::clone(&db);
let join = std::thread::spawn(move || {
let mut 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 mut 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 mut tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
tx1.rollback();
let mut 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 mut tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
}
let mut 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 mut 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 mut 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 mut 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 mut 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 mut tx1 = db.begin_transaction();
let mut 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 mut 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 mut tx1 = db.begin_transaction();
tx1.put(b"k", b"v1").unwrap();
tx1.commit().unwrap();
let mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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 mut 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);
}
}