use std::time::Duration;
use crate::bitcask::bitcask_mvcc::{encode_key, MVCC, Transaction};
use crate::common::codec::parse_i64_user;
pub use crate::common::{
decode_value_meta, key_matches, logical_to_user, now_unix_secs, pack_plain, pack_ttl,
prefix_upper_bound, ExportRecord, IncrError, KeyMatchMode, KeyMeta, SearchPage, SearchQuery,
ValueMeta,
};
impl Transaction {
pub fn batch_set(&self, items: &[(Vec<u8>, Vec<u8>)]) -> Result<(), Vec<u8>> {
let mut written: Vec<Vec<u8>> = Vec::new();
for (k, v) in items {
let packed = pack_plain(v);
if !self.set(k, packed) {
self.rollback_keys(&written);
return Err(k.clone());
}
written.push(k.clone());
}
Ok(())
}
pub fn batch_put(&self, items: &[(Vec<u8>, Vec<u8>)]) -> Result<(), Vec<u8>> {
self.batch_set(items)
}
pub fn batch_delete(&self, keys: &[Vec<u8>]) -> Result<(), Vec<u8>> {
let mut written: Vec<Vec<u8>> = Vec::new();
for k in keys {
if !self.delete(k) {
self.rollback_keys(&written);
return Err(k.clone());
}
written.push(k.clone());
}
Ok(())
}
fn rollback_keys(&self, keys: &[Vec<u8>]) {
if keys.is_empty() {
return;
}
let ver = self.version();
let mut kv = self.kv.lock().unwrap();
for k in keys {
let _ = kv.remove(&encode_key(k, ver));
}
if let Ok(mut active) = self.active_txn.lock() {
if let Some(writes) = active.get_mut(&ver) {
writes.retain(|w| !keys.iter().any(|k| k == w));
}
}
}
pub fn multi_get(&self, keys: &[Vec<u8>]) -> Vec<Option<Vec<u8>>> {
keys.iter().map(|k| self.get(k)).collect()
}
pub fn incr(&self, key: &[u8], delta: i64) -> Result<i64, IncrError> {
self.incr_inner(key, delta, None)
}
pub fn decr(&self, key: &[u8], delta: i64) -> Result<i64, IncrError> {
self.incr(key, delta.checked_neg().ok_or(IncrError::Overflow)?)
}
pub fn incr_with_ttl(
&self,
key: &[u8],
delta: i64,
ttl: Duration,
) -> Result<i64, IncrError> {
let expire = now_unix_secs().saturating_add(ttl.as_secs().max(1));
self.incr_inner(key, delta, Some(expire))
}
fn incr_inner(
&self,
key: &[u8],
delta: i64,
expire_unix: Option<u64>,
) -> Result<i64, IncrError> {
let now = now_unix_secs();
let current = match self.latest_visible_raw(key) {
Some((_ver, Some(raw))) => {
let m = decode_value_meta(&raw);
if m.is_expired_at(now) {
0i64
} else {
parse_i64_user(m.user())?
}
}
_ => 0i64,
};
let new_val = current.checked_add(delta).ok_or(IncrError::Overflow)?;
let user = new_val.to_string().into_bytes();
let packed = match expire_unix {
Some(e) => pack_ttl(&user, e),
None => pack_plain(&user),
};
if !self.set(key, packed) {
return Err(IncrError::WriteConflict);
}
Ok(new_val)
}
pub fn scan(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Vec<ExportRecord> {
let mut out = Vec::new();
self.scan_foreach(start, end, |rec| {
out.push(rec);
true
});
out
}
pub fn scan_foreach(
&self,
start: Option<&[u8]>,
end: Option<&[u8]>,
mut f: impl FnMut(ExportRecord) -> bool,
) {
let now = now_unix_secs();
for (key, _ver, raw) in self.collect_latest_raw(false) {
if let Some(s) = start {
if key.as_slice() < s {
continue;
}
}
if let Some(e) = end {
if key.as_slice() >= e {
break; }
}
let Some(bytes) = raw else { continue };
let m = decode_value_meta(&bytes);
if m.is_expired_at(now) {
continue;
}
let rec = ExportRecord {
key,
value: Some(m.user().to_vec()),
};
if !f(rec) {
break;
}
}
}
pub fn prefix_scan_foreach(
&self,
prefix: &[u8],
f: impl FnMut(ExportRecord) -> bool,
) {
let end = prefix_upper_bound(prefix);
self.scan_foreach(Some(prefix), end.as_deref(), f);
}
pub fn prefix_scan(&self, prefix: &[u8]) -> Vec<ExportRecord> {
let end = prefix_upper_bound(prefix);
self.scan(Some(prefix), end.as_deref())
}
pub fn reverse_scan(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Vec<ExportRecord> {
let mut v = self.scan(start, end);
v.reverse();
v
}
pub fn seek(&self, key: &[u8]) -> Option<ExportRecord> {
self.scan(Some(key), None).into_iter().next()
}
pub fn seek_prev(&self, key: &[u8]) -> Option<ExportRecord> {
let mut upper = key.to_vec();
upper.push(0);
let mut v = self.scan(None, Some(&upper));
v.retain(|r| r.key.as_slice() <= key);
v.pop()
}
pub fn search_keys(&self, query: &SearchQuery) -> SearchPage {
let page_size = query.page_size.max(1);
let page = query.page;
let matched = self.collect_for_search(
query.include_deleted,
Some(query.pattern.as_slice()),
query.mode,
);
let total = matched.len();
let total_pages = if total == 0 {
0
} else {
(total + page_size - 1) / page_size
};
let start = page.saturating_mul(page_size);
let items = if start >= total {
Vec::new()
} else {
let end = (start + page_size).min(total);
matched[start..end].to_vec()
};
SearchPage {
items,
total,
page,
page_size,
total_pages,
}
}
fn collect_for_search(
&self,
include_deleted: bool,
pattern: Option<&[u8]>,
mode: KeyMatchMode,
) -> Vec<ExportRecord> {
self.collect_latest_raw(true)
.into_iter()
.filter_map(|(key, _ver, raw)| {
if let Some(pat) = pattern {
if !key_matches(&key, pat, mode) {
return None;
}
}
match raw {
None => {
if include_deleted {
Some(ExportRecord {
key,
value: None,
})
} else {
None
}
}
Some(bytes) => match logical_to_user(bytes) {
Some(user) => Some(ExportRecord {
key,
value: Some(user),
}),
None => {
if include_deleted {
Some(ExportRecord {
key,
value: None,
})
} else {
None
}
}
},
}
})
.collect()
}
pub fn exists(&self, key: &[u8]) -> bool {
match self.latest_visible_raw(key) {
Some((_ver, Some(raw))) => {
let m = decode_value_meta(&raw);
!m.is_expired_at(now_unix_secs())
}
_ => false,
}
}
pub fn get_meta(&self, key: &[u8]) -> Option<KeyMeta> {
let (version, raw) = self.latest_visible_raw(key)?;
match raw {
None => Some(KeyMeta {
key: key.to_vec(),
value_len: None,
version,
expire_unix_secs: None,
expired: false,
deleted: true,
}),
Some(bytes) => {
let m = decode_value_meta(&bytes);
let expired = m.is_expired_at(now_unix_secs());
Some(KeyMeta {
key: key.to_vec(),
value_len: if expired {
None
} else {
Some(m.user().len())
},
version,
expire_unix_secs: m.expire_unix_secs(),
expired,
deleted: false,
})
}
}
}
pub fn key_count(&self, prefix: &[u8]) -> usize {
self.prefix_scan(prefix).len()
}
pub fn set_with_ttl(&self, key: &[u8], value: Vec<u8>, ttl: Duration) -> bool {
let expire = now_unix_secs().saturating_add(ttl.as_secs().max(1));
let packed = pack_ttl(&value, expire);
self.set(key, packed)
}
pub fn refresh_ttl(&self, key: &[u8], ttl: Duration) -> bool {
let Some((_ver, Some(raw))) = self.latest_visible_raw(key) else {
return false;
};
let m = decode_value_meta(&raw);
if m.is_expired_at(now_unix_secs()) {
return false;
}
let expire = now_unix_secs().saturating_add(ttl.as_secs().max(1));
let packed = pack_ttl(m.user(), expire);
self.set(key, packed)
}
pub fn get_ttl(&self, key: &[u8]) -> Option<Option<Duration>> {
let (_ver, raw) = self.latest_visible_raw(key)?;
let raw = raw?;
let m = decode_value_meta(&raw);
let now = now_unix_secs();
if m.is_expired_at(now) {
return None;
}
match m.expire_unix_secs() {
None => Some(None),
Some(e) => {
let left = e.saturating_sub(now);
Some(Some(Duration::from_secs(left)))
}
}
}
pub fn persist(&self, key: &[u8]) -> bool {
let Some((_ver, Some(raw))) = self.latest_visible_raw(key) else {
return false;
};
let m = decode_value_meta(&raw);
if m.is_expired_at(now_unix_secs()) {
return false;
}
let packed = pack_plain(m.user());
self.set(key, packed)
}
pub fn purge_expired(&self) -> usize {
let now = now_unix_secs();
let expired_keys: Vec<Vec<u8>> = self
.collect_latest_raw(false)
.into_iter()
.filter_map(|(key, _ver, raw)| {
let raw = raw?;
let m = decode_value_meta(&raw);
if m.is_expired_at(now) {
Some(key)
} else {
None
}
})
.collect();
let mut n = 0usize;
for k in expired_keys {
if self.delete(&k) {
n += 1;
}
}
n
}
}
impl MVCC {
pub fn search_keys(&self, query: &SearchQuery) -> SearchPage {
let tx = self.begin_transaction();
let page = tx.search_keys(query);
tx.commit();
page
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn tmp(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!(
"bitcask_kv_ops_{tag}_{}_{}",
std::process::id(),
nanos
))
}
fn cleanup(path: &PathBuf) {
let _ = std::fs::remove_dir_all(path);
}
#[test]
fn test_value_meta_roundtrip() {
let p = pack_plain(b"hi");
match decode_value_meta(&p) {
ValueMeta::Plain { user } => assert_eq!(user, b"hi"),
_ => panic!(),
}
let t = pack_ttl(b"x", 1000);
match decode_value_meta(&t) {
ValueMeta::Ttl {
expire_unix_secs,
user,
} => {
assert_eq!(expire_unix_secs, 1000);
assert_eq!(user, b"x");
}
_ => panic!(),
}
assert_eq!(
decode_value_meta(b"raw"),
ValueMeta::Plain {
user: b"raw".to_vec()
}
);
}
#[test]
fn test_batch_and_multi_get() {
let path = tmp("batch");
{
let mvcc = MVCC::open(&path);
let tx = mvcc.begin_transaction();
let items = vec![
(b"a".to_vec(), b"1".to_vec()),
(b"b".to_vec(), b"2".to_vec()),
(b"c".to_vec(), b"3".to_vec()),
];
tx.batch_set(&items).unwrap();
let got = tx.multi_get(&[b"a".to_vec(), b"b".to_vec(), b"z".to_vec()]);
assert_eq!(got[0], Some(b"1".to_vec()));
assert_eq!(got[1], Some(b"2".to_vec()));
assert_eq!(got[2], None);
tx.batch_delete(&[b"b".to_vec()]).unwrap();
assert!(!tx.exists(b"b"));
tx.commit();
}
cleanup(&path);
}
#[test]
fn test_scan_prefix_seek() {
let path = tmp("scan");
{
let mvcc = MVCC::open(&path);
let tx = mvcc.begin_transaction();
for (k, v) in [
(&b"order:01"[..], &b"a"[..]),
(&b"order:02"[..], &b"b"[..]),
(&b"order:10"[..], &b"c"[..]),
(&b"user:1"[..], &b"u"[..]),
] {
assert!(tx.set(k, pack_plain(v)));
}
tx.commit();
let tx = mvcc.begin_transaction();
let s = tx.prefix_scan(b"order:");
assert_eq!(s.len(), 3);
assert_eq!(s[0].key, b"order:01");
let one = tx.seek(b"order:05").unwrap();
assert_eq!(one.key, b"order:10");
let prev = tx.seek_prev(b"order:05").unwrap();
assert_eq!(prev.key, b"order:02");
assert_eq!(tx.key_count(b"order:"), 3);
tx.commit();
}
cleanup(&path);
}
#[test]
fn test_search_pagination() {
let path = tmp("search");
{
let mvcc = MVCC::open(&path);
let tx = mvcc.begin_transaction();
for k in [&b"apple"[..], &b"apply"[..], &b"banana"[..], &b"user:1"[..]] {
assert!(tx.set(k, pack_plain(b"v")));
}
tx.commit();
let page = mvcc.search_keys(&SearchQuery::contains(b"pp", 0, 10));
assert_eq!(page.total, 2);
assert_eq!(page.items.len(), 2);
let p0 = mvcc.search_keys(&SearchQuery::prefix(b"user:", 0, 1));
assert_eq!(p0.total, 1);
assert!(!p0.has_next());
}
cleanup(&path);
}
#[test]
fn test_incr_and_ttl() {
let path = tmp("incr");
{
let mvcc = MVCC::open(&path);
let tx = mvcc.begin_transaction();
assert_eq!(tx.incr(b"n", 1).unwrap(), 1);
assert_eq!(tx.incr(b"n", 9).unwrap(), 10);
assert_eq!(tx.decr(b"n", 3).unwrap(), 7);
assert_eq!(tx.get(b"n"), Some(b"7".to_vec()));
assert_eq!(
tx.incr_with_ttl(b"hits", 1, Duration::from_secs(60))
.unwrap(),
1
);
assert!(matches!(tx.get_ttl(b"hits"), Some(Some(_))));
assert!(tx.set_with_ttl(b"tmp", b"x".to_vec(), Duration::from_secs(120)));
assert!(tx.exists(b"tmp"));
assert!(tx.persist(b"tmp"));
assert_eq!(tx.get_ttl(b"tmp"), Some(None));
let stale = pack_ttl(b"old", now_unix_secs().saturating_sub(5));
assert!(tx.set(b"gone", stale));
assert!(!tx.exists(b"gone"));
let n = tx.purge_expired();
assert!(n >= 1);
tx.commit();
}
cleanup(&path);
}
#[test]
fn test_meta_and_exists() {
let path = tmp("meta");
{
let mvcc = MVCC::open(&path);
let tx = mvcc.begin_transaction();
assert!(tx.set(b"k", pack_plain(b"hello")));
assert!(tx.exists(b"k"));
let m = tx.get_meta(b"k").unwrap();
assert_eq!(m.value_len, Some(5));
assert!(!m.deleted);
assert!(!m.expired);
assert!(tx.delete(b"k"));
assert!(!tx.exists(b"k"));
tx.commit();
}
cleanup(&path);
}
#[test]
fn test_prefix_upper_bound() {
assert_eq!(
prefix_upper_bound(b"ab"),
Some(b"ac".to_vec())
);
assert_eq!(prefix_upper_bound(b"a\xff"), Some(b"b".to_vec()));
}
}