use std::sync::Arc;
use crate::sync::Mutex;
use kovan_mvcc::{LockInfo, Storage, Value, WriteInfo, WriteKind};
use super::layout;
use crate::WriteBatchOp;
use crate::column_family::{DEFAULT_CF_ID, prefix_key};
use crate::engine::{DurabilityMode, RegolithEngine};
pub(crate) const MAX_RANGE_DELETE_KEYS: usize = 1 << 20;
type LockTable = kovan_map::HopscotchMap<Vec<u8>, LockInfo>;
pub(crate) struct RegolithStorage {
engine: Arc<RegolithEngine>,
locks: LockTable,
durability: DurabilityMode,
failure: Mutex<Option<std::io::Error>>,
staged: Mutex<Vec<WriteBatchOp>>,
}
thread_local! {
static KEY_BUF: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
static PREFIX_BUF: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
}
fn with_key<R>(compose: impl FnOnce(&mut Vec<u8>), f: impl FnOnce(&[u8]) -> R) -> R {
KEY_BUF.with(|b| {
let mut buf = b.borrow_mut();
compose(&mut buf);
f(&buf)
})
}
impl RegolithStorage {
pub(crate) fn new(engine: Arc<RegolithEngine>, durability: DurabilityMode) -> Self {
Self {
engine,
locks: LockTable::with_capacity(1024),
durability,
failure: Mutex::new(None),
staged: Mutex::new(Vec::new()),
}
}
pub(crate) fn take_failure(&self) -> Option<std::io::Error> {
self.failure.lock().take()
}
fn record(&self, err: std::io::Error) {
let mut slot = self.failure.lock();
if slot.is_none() {
*slot = Some(err);
}
}
fn flush_staged(&self) {
let ops: Vec<WriteBatchOp> = {
let mut staged = self.staged.lock();
if staged.is_empty() {
return;
}
std::mem::take(&mut *staged)
};
if let Err(e) = self.engine.apply_batch(ops, self.durability, false) {
self.record(e);
}
}
fn stage(&self, op: WriteBatchOp) {
self.staged.lock().push(op);
}
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
match self.engine.get_latest(key) {
Ok(v) => v,
Err(e) => {
self.record(e);
None
}
}
}
}
impl Storage for RegolithStorage {
fn get_lock(&self, key: &[u8]) -> Option<LockInfo> {
self.locks.get(key)
}
fn put_lock(&self, key: &[u8], lock: LockInfo) -> Result<(), kovan_mvcc::MvccError> {
match self.locks.insert_if_absent(key.to_vec(), lock.clone()) {
None => Ok(()),
Some(existing) => {
if existing.txn_id == lock.txn_id {
self.locks.insert(key.to_vec(), lock);
Ok(())
} else {
Err(kovan_mvcc::MvccError::LockConflict {
key: key.to_vec(),
holder_txn: existing.txn_id,
})
}
}
}
}
fn delete_lock(&self, key: &[u8]) {
self.locks.remove(key);
}
fn get_latest_write(&self, key: &[u8], ts: u64) -> Option<(u64, WriteInfo)> {
self.seek_write(key, ts, false)
}
fn get_latest_commit(&self, key: &[u8], ts: u64) -> Option<(u64, WriteInfo)> {
self.seek_write(key, ts, true)
}
fn put_write(&self, key: &[u8], commit_ts: u64, info: WriteInfo) {
self.flush_staged();
let projection = self.projection(key, &info);
let mut k = Vec::with_capacity(layout::composed_len(key));
layout::write_key(key, commit_ts, &mut k);
self.stage(WriteBatchOp::Put {
key: k,
value: encode_write_info(&info),
});
if let Some(op) = projection {
self.stage(op);
}
self.flush_staged();
}
fn get_data(&self, key: &[u8], start_ts: u64) -> Option<Value> {
with_key(
|b| layout::data_key(key, start_ts, b),
|k| self.get(k).map(Arc::new),
)
}
fn put_data(&self, key: &[u8], start_ts: u64, value: Value) {
let mut k = Vec::with_capacity(layout::composed_len(key));
layout::data_key(key, start_ts, &mut k);
self.stage(WriteBatchOp::Put {
key: k,
value: value.as_ref().clone(),
});
}
fn delete_data(&self, key: &[u8], start_ts: u64) {
let mut k = Vec::with_capacity(layout::composed_len(key));
layout::data_key(key, start_ts, &mut k);
self.stage(WriteBatchOp::Delete { key: k });
}
}
impl RegolithStorage {
fn projection(&self, key: &[u8], info: &WriteInfo) -> Option<WriteBatchOp> {
let user_key = prefix_key(DEFAULT_CF_ID, key);
match info.kind {
WriteKind::Put => {
let value = self.get_data(key, info.start_ts)?.as_ref().clone();
Some(WriteBatchOp::Put {
key: user_key,
value,
})
}
WriteKind::Delete => Some(WriteBatchOp::Delete { key: user_key }),
WriteKind::Rollback => None,
}
}
pub(crate) fn keys_in_range(
&self,
start: &[u8],
end: &[u8],
ts: u64,
) -> std::io::Result<Vec<Vec<u8>>> {
let mut iter = self.engine.new_iter_latest();
with_key(
|b| layout::write_prefix(start, b),
|target| iter.seek(target),
);
let mut keys: Vec<Vec<u8>> = Vec::new();
let mut last: Option<Vec<u8>> = None;
while iter.valid() {
let Some(composed) = iter.key() else { break };
let Some(user_key) = layout::user_key_of(composed) else {
break;
};
if user_key.as_slice() >= end {
break;
}
if last.as_deref() != Some(user_key.as_slice())
&& let Some(ts_of) = layout::commit_ts_of(composed)
&& ts_of <= ts
&& let Some(info) = iter.value().and_then(decode_write_info)
{
{
if info.kind == WriteKind::Put {
if keys.len() == MAX_RANGE_DELETE_KEYS {
return Err(std::io::Error::other(format!(
"delete_range covers more than {MAX_RANGE_DELETE_KEYS} keys; \
every key is taken as a write so the range is \
conflict-checked, so narrow the range or delete in batches"
)));
}
keys.push(user_key.clone());
}
last = Some(user_key);
}
}
iter.next();
}
Ok(keys)
}
fn seek_write(&self, key: &[u8], ts: u64, skip_rollbacks: bool) -> Option<(u64, WriteInfo)> {
let mut iter = self.engine.new_iter_latest();
with_key(
|b| layout::write_seek(key, ts, b),
|target| iter.seek(target),
);
PREFIX_BUF.with(|b| {
let mut prefix = b.borrow_mut();
layout::write_prefix(key, &mut prefix);
self.walk_writes(&mut iter, &prefix, skip_rollbacks)
})
}
fn walk_writes(
&self,
iter: &mut crate::engine::iterator::RegolithIterator,
prefix: &[u8],
skip_rollbacks: bool,
) -> Option<(u64, WriteInfo)> {
while iter.valid() {
let k = iter.key()?;
if !k.starts_with(prefix) {
return None;
}
let commit_ts = layout::commit_ts_of(k)?;
let info = decode_write_info(iter.value()?)?;
if !(skip_rollbacks && info.kind == WriteKind::Rollback) {
return Some((commit_ts, info));
}
iter.next();
}
None
}
}
fn encode_write_info(info: &WriteInfo) -> Vec<u8> {
let mut out = Vec::with_capacity(9);
out.extend_from_slice(&info.start_ts.to_be_bytes());
out.push(match info.kind {
WriteKind::Put => 0,
WriteKind::Delete => 1,
WriteKind::Rollback => 2,
});
out
}
fn decode_write_info(bytes: &[u8]) -> Option<WriteInfo> {
if bytes.len() < 9 {
return None;
}
Some(WriteInfo {
start_ts: u64::from_be_bytes(bytes[0..8].try_into().ok()?),
kind: match bytes[8] {
0 => WriteKind::Put,
1 => WriteKind::Delete,
2 => WriteKind::Rollback,
_ => return None,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use kovan_mvcc::LockType;
use tempfile::TempDir;
fn storage() -> (TempDir, Arc<RegolithStorage>) {
let dir = TempDir::new().unwrap();
let db = crate::Db::open(dir.path(), crate::Options::default()).unwrap();
let engine = db.engine_arc();
std::mem::forget(db);
(
dir,
Arc::new(RegolithStorage::new(engine, DurabilityMode::Eventual)),
)
}
fn lock(txn_id: u128, start_ts: u64) -> LockInfo {
LockInfo {
txn_id,
start_ts,
primary_key: std::sync::Arc::from(&b"tprimary"[..]),
lock_type: LockType::Put,
short_value: None,
}
}
#[test]
fn a_second_transaction_cannot_take_a_held_lock() {
let (_d, s) = storage();
s.put_lock(b"tk", lock(1, 10)).expect("first acquires");
let err = s
.put_lock(b"tk", lock(2, 11))
.expect_err("a second transaction must be refused");
match err {
kovan_mvcc::MvccError::LockConflict { holder_txn, .. } => {
assert_eq!(holder_txn, 1, "the error must name the real holder")
}
other => panic!("expected LockConflict, got {other:?}"),
}
assert_eq!(s.get_lock(b"tk").map(|l| l.txn_id), Some(1));
}
#[test]
fn a_transaction_may_re_lock_its_own_key() {
let (_d, s) = storage();
s.put_lock(b"tk", lock(1, 10)).unwrap();
s.put_lock(b"tk", lock(1, 10))
.expect("re-prewriting our own key is allowed");
s.delete_lock(b"tk");
assert!(s.get_lock(b"tk").is_none());
s.put_lock(b"tk", lock(2, 11))
.expect("released, so another may take it");
}
#[test]
fn data_round_trips_at_its_start_timestamp() {
let (_d, s) = storage();
s.put_data(b"tk", 7, Arc::new(b"seven".to_vec()));
s.flush_staged();
assert_eq!(s.get_data(b"tk", 7).as_deref(), Some(&b"seven".to_vec()));
assert_eq!(
s.get_data(b"tk", 8),
None,
"a different version must not answer"
);
assert!(s.take_failure().is_none());
}
#[test]
fn the_newest_write_at_or_before_a_timestamp_is_found() {
let (_d, s) = storage();
for (commit_ts, start_ts) in [(10u64, 9u64), (20, 19), (30, 29)] {
s.put_write(
b"tk",
commit_ts,
WriteInfo {
start_ts,
kind: WriteKind::Put,
},
);
}
assert_eq!(s.get_latest_write(b"tk", 25).map(|(ts, _)| ts), Some(20));
assert_eq!(s.get_latest_write(b"tk", 30).map(|(ts, _)| ts), Some(30));
assert!(
s.get_latest_write(b"tk", 5).is_none(),
"nothing committed that early"
);
}
#[test]
fn a_rollback_is_reported_by_one_query_and_skipped_by_the_other() {
let (_d, s) = storage();
s.put_write(
b"tk",
10,
WriteInfo {
start_ts: 9,
kind: WriteKind::Put,
},
);
s.put_write(
b"tk",
20,
WriteInfo {
start_ts: 19,
kind: WriteKind::Rollback,
},
);
assert_eq!(s.get_latest_write(b"tk", 25).map(|(ts, _)| ts), Some(20));
assert_eq!(
s.get_latest_commit(b"tk", 25).map(|(ts, _)| ts),
Some(10),
"get_latest_commit must walk past the rollback"
);
}
#[test]
fn one_keys_writes_do_not_answer_for_another() {
let (_d, s) = storage();
s.put_write(
b"tk",
10,
WriteInfo {
start_ts: 9,
kind: WriteKind::Put,
},
);
assert!(
s.get_latest_write(b"tkk", 99).is_none(),
"a longer key must not match"
);
assert!(
s.get_latest_write(b"t", 99).is_none(),
"a shorter key must not match"
);
}
#[test]
fn write_records_survive_a_reopen() {
let dir = TempDir::new().unwrap();
{
let db = crate::Db::open(dir.path(), crate::Options::default()).unwrap();
let s = RegolithStorage::new(db.engine_arc(), DurabilityMode::Immediate);
s.put_data(b"tk", 9, Arc::new(b"v".to_vec()));
s.put_write(
b"tk",
10,
WriteInfo {
start_ts: 9,
kind: WriteKind::Put,
},
);
db.close().unwrap();
}
let db = crate::Db::open(dir.path(), crate::Options::default()).unwrap();
let s = RegolithStorage::new(db.engine_arc(), DurabilityMode::Immediate);
assert_eq!(s.get_latest_write(b"tk", 99).map(|(ts, _)| ts), Some(10));
assert_eq!(s.get_data(b"tk", 9).as_deref(), Some(&b"v".to_vec()));
assert!(
s.get_lock(b"tk").is_none(),
"locks must not survive a reopen"
);
}
#[test]
fn write_info_round_trips_through_its_encoding() {
for kind in [WriteKind::Put, WriteKind::Delete, WriteKind::Rollback] {
let info = WriteInfo {
start_ts: 12345,
kind,
};
let back = decode_write_info(&encode_write_info(&info)).expect("decodes");
assert_eq!(back.start_ts, 12345);
assert_eq!(back.kind, kind);
}
assert!(decode_write_info(b"short").is_none());
assert!(
decode_write_info(&[0; 8]).is_none(),
"missing the kind byte"
);
}
}