use std::mem;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use std::collections::{VecDeque, hash_map::Entry as HashMapEntry};
use std::sync::{Arc,
atomic::{AtomicBool, AtomicUsize, Ordering}};
use parking_lot::Mutex;
use futures::{future::{FutureExt, BoxFuture},
stream::{StreamExt, BoxStream}};
use async_lock::Mutex as AsyncMutex;
use async_channel::Sender;
use async_stream::stream;
use log::{debug, info, error};
use pi_async_rt::{lock::spin_lock::SpinLock,
rt::{AsyncRuntime,
multi_thread::MultiTaskRuntime}};
use pi_atom::Atom;
use pi_guid::Guid;
use pi_hash::XHashMap;
use pi_ordmap::{ordmap::OrdMap, asbtree::Tree};
use pi_async_transaction::{AsyncTransaction,
Transaction2Pc,
Transaction2PcAllConflicts,
UnitTransaction,
SequenceTransaction,
TransactionTree,
TransactionError,
AsyncCommitLog,
ErrorLevel,
manager_2pc::Transaction2PcStatus};
use pi_ordmap::ordmap::ImOrdMap;
use pi_store::log_store::log_file::{PairLoader,
LogMethod,
LogFile};
use crate::{Binary, KVAction, TableTrQos, KVActionLog, KVDBCommitConfirm, KVTableTrError,
TableKeyConflict,
db::{KVDBTransaction, KVDBChildTrList},
key_version::{KeyVersions,
PrepareMode,
PreparedActions,
PreparedCommitError,
TableVersionContext,
Version,
VersionConflictKind,
VersionReceipt,
binary_state_equal,
has_prepared_conflict,
has_prepared_transaction,
take_prepared_for_commit},
tables::{KVTable, ordmap_snapshot::OrdMapSnapshot},
utils::KVDBEvent,
KVDBTableType};
const DEFAULT_LOG_FILE_COMMIT_DELAY_TIMEOUT: usize = 1000;
#[derive(Clone)]
pub struct MetaTable<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
>(Arc<InnerMetaTable<C, Log>>);
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Send for MetaTable<C, Log> {}
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Sync for MetaTable<C, Log> {}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> KVTable for MetaTable<C, Log> {
type Name = Atom;
type Tr = MetaTabTr<C, Log>;
type Error = KVTableTrError;
fn name(&self) -> <Self as KVTable>::Name {
self.0.name.clone()
}
fn path(&self) -> Option<&Path> {
Some(self.0.log_file.path())
}
#[inline]
fn is_persistent(&self) -> bool {
true
}
fn is_ordered(&self) -> bool {
true
}
fn len(&self) -> usize {
self.0.root.lock().size()
}
fn size(&self) -> u64 {
let root_copy = self.0.root.lock().clone();
root_copy.full_bytes_size()
}
fn transaction(&self,
source: Atom,
is_writable: bool,
is_persistent: bool,
prepare_timeout: u64,
commit_timeout: u64) -> Self::Tr {
MetaTabTr::new(source,
is_writable,
is_persistent,
prepare_timeout,
commit_timeout,
self.clone())
}
fn ready_collect(&self) -> BoxFuture<Result<(), Self::Error>> {
let table = self.clone();
async move {
let now = Instant::now();
match table.0.log_file.split().await {
Err(e) => {
return Err(KVTableTrError::new_transaction_error(ErrorLevel::Normal,
format!("Ready collect meta table failed, path: {:?}, table: {:?}, reason: {:?}",
table.0.log_file.path(),
table.0.name.as_str(),
e)));
},
Ok(writed_log_index) => {
info!("Ready collect meta table succeeded, time: {:?}, path: {:?}, table: {:?}, writed_log_index: {}",
now.elapsed(),
table.0.log_file.path(),
table.0.name.as_str(),
writed_log_index);
Ok(())
},
}
}.boxed()
}
fn collect(&self) -> BoxFuture<Result<(), Self::Error>> {
let table = self.clone();
async move {
let now = Instant::now();
match table.0.log_file.collect(1024 * 1024,
32 * 1024,
false).await {
Err(e) => {
return Err(KVTableTrError::new_transaction_error(ErrorLevel::Normal,
format!("Collect meta table failed, path: {:?}, table: {:?}, reason: {:?}",
table.0.log_file.path(),
table.0.name.as_str(),
e)));
},
Ok((size, len)) => {
info!("Collect meta table succeeded, time: {:?}, path: {:?}, table: {:?}, file_size: {}, file_len: {}",
now.elapsed(),
table.0.log_file.path(),
table.0.name.as_str(),
size,
len);
Ok(())
},
}
}.boxed()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> MetaTable<C, Log> {
pub(crate) fn query_committed(&self, key: &Binary) -> Option<Binary> {
self.0.root.lock().get(key).cloned()
}
pub async fn new<P: AsRef<Path>>(rt: MultiTaskRuntime<()>,
path: P,
name: Atom,
log_file_limit: usize,
block_limit: usize,
init_log_file_index: Option<usize>,
load_buf_len: u64,
is_checksum: bool,
waits_limit: usize,
wait_timeout: usize,
notifier: Option<Sender<KVDBEvent<Guid>>>) -> Self {
let root = Mutex::new(OrdMap::new(None));
let prepare = Mutex::new(XHashMap::default());
match LogFile::open(rt.clone(),
path.as_ref().to_path_buf(),
block_limit,
log_file_limit,
init_log_file_index).await {
Err(e) => {
panic!("Open meta table failed, table: {:?}, path: {:?}, reason: {:?}",
name.as_str(),
path.as_ref(),
e);
},
Ok(log_file) => {
let waits = AsyncMutex::new(VecDeque::new());
let waits_size = AtomicUsize::new(0);
let collecting = AtomicBool::new(false);
let inner = InnerMetaTable {
name: name.clone(),
root,
prepare,
rt,
waits,
waits_size,
waits_limit,
wait_timeout,
collecting,
log_file,
notifier,
};
let table = MetaTable(Arc::new(inner));
let now = Instant::now();
let mut loader = MetaTableLoader::new(table.clone());
if let Err(e) = table.0.log_file.load(&mut loader,
None,
load_buf_len,
is_checksum).await {
panic!("Load meta table failed, table: {:?}, path: {:?}, reason: {:?}",
name.as_str(),
path.as_ref(),
e);
}
info!("Load meta table succeeded, table: {:?}, path: {:?}, files: {}, keys: {}, bytes: {}, time: {:?}",
name.as_str(),
path.as_ref(),
loader.log_files_len(),
loader.keys_len(),
loader.bytes_len(),
now.elapsed());
let table_copy = table.clone();
let _ = table.0.rt.spawn(async move {
let table_ref = &table_copy;
loop {
match collect_waits(table_ref,
Some(table_copy.0.wait_timeout)).await {
Err((collect_time, statistics)) => {
error!("Collect meta table failed, table: {:?}, time: {:?}, statistics: {:?}, reason: out of time",
table_copy.name().as_str(),
collect_time,
statistics);
},
Ok((collect_time, statistics)) => {
debug!("Collect meta table succeeded, table: {:?}, time: {:?}, statistics: {:?}, reason: out of time",
table_copy.name().as_str(),
collect_time,
statistics);
},
}
}
});
table
},
}
}
}
struct InnerMetaTable<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> {
name: Atom,
root: Mutex<OrdMap<Tree<Binary, Binary>>>,
prepare: Mutex<XHashMap<Guid, PreparedActions>>,
rt: MultiTaskRuntime<()>,
waits: AsyncMutex<VecDeque<(MetaTabTr<C, Log>, XHashMap<Binary, KVActionLog>, <MetaTabTr<C, Log> as Transaction2Pc>::CommitConfirm)>>,
waits_size: AtomicUsize,
waits_limit: usize,
wait_timeout: usize,
collecting: AtomicBool,
log_file: LogFile,
notifier: Option<Sender<KVDBEvent<Guid>>>,
}
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Send for InnerMetaTable<C, Log> {}
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Sync for InnerMetaTable<C, Log> {}
#[derive(Clone)]
pub struct MetaTabTr<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
>(Arc<InnerMetaTabTr<C, Log>>);
#[derive(Clone, Copy)]
enum PrepareConflictKind {
Common,
First,
All,
}
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Send for MetaTabTr<C, Log> {}
unsafe impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Sync for MetaTabTr<C, Log> {}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> AsyncTransaction for MetaTabTr<C, Log> {
type Output = ();
type Error = KVTableTrError;
fn is_writable(&self) -> bool {
self.0.writable
}
fn is_concurrent_commit(&self) -> bool {
false
}
fn is_concurrent_rollback(&self) -> bool {
false
}
fn get_source(&self) -> Atom {
self.0.source.clone()
}
fn init(&self)
-> BoxFuture<Result<<Self as AsyncTransaction>::Output, <Self as AsyncTransaction>::Error>> {
async move {
Ok(())
}.boxed()
}
fn rollback(&self)
-> BoxFuture<Result<<Self as AsyncTransaction>::Output, <Self as AsyncTransaction>::Error>> {
let tr = self.clone();
async move {
let transaction_uid = tr.get_transaction_uid().unwrap();
let _ = tr.0.table.0.prepare.lock().remove(&transaction_uid);
if let Some(context) = tr.0.version_context.as_ref() {
context.release_snapshot();
}
Ok(())
}.boxed()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Transaction2Pc for MetaTabTr<C, Log> {
type Tid = Guid;
type Pid = Guid;
type Cid = Guid;
type PrepareOutput = Vec<u8>;
type PrepareError = KVTableTrError;
type ConfirmOutput = ();
type ConfirmError = KVTableTrError;
type CommitConfirm = KVDBCommitConfirm<C, Log>;
fn is_require_persistence(&self) -> bool {
self.0.persistence.load(Ordering::Relaxed)
}
fn require_persistence(&self) {
self.0.persistence.store(true, Ordering::Relaxed);
}
fn is_concurrent_prepare(&self) -> bool {
false
}
fn is_enable_inherit_uid(&self) -> bool {
true
}
fn get_transaction_uid(&self) -> Option<<Self as Transaction2Pc>::Tid> {
self.0.tid.lock().clone()
}
fn set_transaction_uid(&self, uid: <Self as Transaction2Pc>::Tid) {
*self.0.tid.lock() = Some(uid);
}
fn get_prepare_uid(&self) -> Option<<Self as Transaction2Pc>::Pid> {
None
}
fn set_prepare_uid(&self, _uid: <Self as Transaction2Pc>::Pid) {
}
fn get_commit_uid(&self) -> Option<<Self as Transaction2Pc>::Cid> {
self.0.cid.lock().clone()
}
fn set_commit_uid(&self, uid: <Self as Transaction2Pc>::Cid) {
*self.0.cid.lock() = Some(uid);
}
fn get_prepare_timeout(&self) -> u64 {
self.0.prepare_timeout
}
fn get_commit_timeout(&self) -> u64 {
self.0.commit_timeout
}
fn prepare(&self)
-> BoxFuture<Result<Option<<Self as Transaction2Pc>::PrepareOutput>, <Self as Transaction2Pc>::PrepareError>> {
let tr = self.clone();
async move {
tr.prepare_registered(PrepareConflictKind::Common).await
}.boxed()
}
fn prepare_conflicts(&self) -> BoxFuture<Result<Option<<Self as Transaction2Pc>::PrepareOutput>, <Self as Transaction2Pc>::PrepareError>> {
let tr = self.clone();
async move {
tr.prepare_registered(PrepareConflictKind::First).await
}.boxed()
}
fn commit(&self, confirm: <Self as Transaction2Pc>::CommitConfirm)
-> BoxFuture<Result<<Self as AsyncTransaction>::Output, <Self as AsyncTransaction>::Error>> {
let tr = self.clone();
async move {
let transaction_uid = tr.get_transaction_uid().unwrap();
let publication = match tr.0.version_context.as_ref() {
Some(context) => Some(context.versions().publication().write().await),
None => None,
};
let expected_mode = tr
.0
.version_context
.as_ref()
.map(TableVersionContext::mode)
.unwrap_or(PrepareMode::Ordinary);
let prepared = {
let mut prepare = tr.0.table.0.prepare.lock();
take_prepared_for_commit(&mut prepare,
&transaction_uid,
expected_mode,
tr.is_writable())
};
let actions = match prepared {
Ok(Some(prepared)) => prepared.actions,
Ok(None) => XHashMap::default(),
Err(PreparedCommitError::ModeMismatch(prepared_mode)) => {
drop(publication);
if let Some(context) = tr.0.version_context.as_ref() {
context.release_snapshot();
}
return Err(KVTableTrError::new_transaction_error(
ErrorLevel::Fatal,
format!("Commit meta table failed, table: {:?}, source: {:?}, transaction_uid: {:?}, expected_mode: {:?}, prepared_mode: {:?}, reason: prepared action protocol mismatch after entering non-rollbackable commit",
tr.0.table.name().as_str(),
tr.0.source,
transaction_uid,
expected_mode,
prepared_mode)));
},
Err(PreparedCommitError::Missing) => {
drop(publication);
if let Some(context) = tr.0.version_context.as_ref() {
context.release_snapshot();
}
return Err(KVTableTrError::new_transaction_error(
ErrorLevel::Fatal,
format!("Commit meta table failed, table: {:?}, source: {:?}, transaction_uid: {:?}, expected_mode: {:?}, reason: prepared actions missing after entering non-rollbackable commit",
tr.0.table.name().as_str(),
tr.0.source,
transaction_uid,
expected_mode)));
},
};
let has_writes = actions.values().any(|action| {
matches!(action, KVActionLog::Write(_) | KVActionLog::DirtyWrite(_))
});
if has_writes {
let revision = match tr.0.version_context.as_ref() {
Some(context) => {
match context.versions().checked_next_revision() {
Some(revision) => Some(revision),
None => {
drop(publication);
context.release_snapshot();
return Err(KVTableTrError::new_transaction_error(
ErrorLevel::Fatal,
format!("Commit meta table failed, table: {:?}, source: {:?}, transaction_uid: {:?}, reason: key version revision exhausted",
tr.0.table.name().as_str(),
tr.0.source,
transaction_uid)));
},
}
},
None => None,
};
let mut committed_versions = Vec::new();
let mut root = tr.0.table.0.root.lock();
if root.ptr_eq(&tr.0.root_ref) {
*root = tr.0.root_mut.lock().clone();
} else {
for (key, action) in &actions {
match action {
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
let _ = root.delete(key, false);
},
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
let _ = root.upsert(key.clone(), value.clone(), false);
},
KVActionLog::Read => (),
}
}
}
if let (Some(context), Some(revision)) =
(tr.0.version_context.as_ref(), revision) {
for (key, action) in &actions {
let value = match action {
KVActionLog::Write(value) | KVActionLog::DirtyWrite(value) => value,
KVActionLog::Read => continue,
};
committed_versions.push(context.versions().publish(
tr.0.table.name(),
key.clone(),
value.as_ref(),
transaction_uid.clone(),
revision));
}
context.versions().complete_revision(revision);
if let Some(receipt) = context.receipt() {
receipt.append(committed_versions);
}
}
}
drop(publication);
if let Some(context) = tr.0.version_context.as_ref() {
context.release_snapshot();
}
if tr.is_require_persistence() {
let table_copy = tr.0.table.clone();
let _ = self.0.table.0.rt.spawn(async move {
let mut size = 0;
for (key, action) in &actions {
match action {
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
size += key.len() + value.len();
},
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
size += key.len();
},
KVActionLog::Read => (),
}
}
table_copy.0.waits.lock().await.push_back((tr, actions, confirm));
let last_waits_size = table_copy.0.waits_size.fetch_add(size, Ordering::SeqCst); if last_waits_size + size >= table_copy.0.waits_limit {
table_copy.0.waits_size.store(0, Ordering::Relaxed);
match collect_waits(&table_copy,
None).await {
Err((collect_time, statistics)) => {
error!("Collect meta table failed, table: {:?}, time: {:?}, statistics: {:?}, reason: out of size",
table_copy.name().as_str(),
collect_time,
statistics);
},
Ok((collect_time, statistics)) => {
info!("Collect meta table succeeded, table: {:?}, time: {:?}, statistics: {:?}, reason: out of size",
table_copy.name().as_str(),
collect_time,
statistics);
},
}
}
});
}
Ok(())
}.boxed()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> Transaction2PcAllConflicts for MetaTabTr<C, Log> {
fn precheck_all_conflicts(&self)
-> BoxFuture<'_, Result<(), <Self as Transaction2Pc>::PrepareError>> {
let tr = self.clone();
async move {
tr.precheck_versions().await
}.boxed()
}
fn prepare_all_conflicts(&self)
-> BoxFuture<'_, Result<Option<<Self as Transaction2Pc>::PrepareOutput>, <Self as Transaction2Pc>::PrepareError>> {
let tr = self.clone();
async move {
tr.prepare_registered(PrepareConflictKind::All).await
}.boxed()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> UnitTransaction for MetaTabTr<C, Log> {
type Status = Transaction2PcStatus;
type Qos = TableTrQos;
fn is_unit(&self) -> bool {
true
}
fn get_status(&self) -> <Self as UnitTransaction>::Status {
self.0.status.lock().clone()
}
fn set_status(&self, status: <Self as UnitTransaction>::Status) {
*self.0.status.lock() = status;
}
fn qos(&self) -> <Self as UnitTransaction>::Qos {
if self.is_require_persistence() {
TableTrQos::Safe
} else {
TableTrQos::ThreadSafe
}
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> SequenceTransaction for MetaTabTr<C, Log> {
type Item = Self;
fn is_sequence(&self) -> bool {
false
}
fn prev_item(&self) -> Option<<Self as SequenceTransaction>::Item> {
None
}
fn next_item(&self) -> Option<<Self as SequenceTransaction>::Item> {
None
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> TransactionTree for MetaTabTr<C, Log> {
type Node = KVDBTransaction<C, Log>;
type NodeInterator = KVDBChildTrList<C, Log>;
fn is_tree(&self) -> bool {
false
}
fn children_len(&self) -> usize {
0
}
fn to_children(&self) -> Self::NodeInterator {
KVDBChildTrList::new()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> KVAction for MetaTabTr<C, Log> {
type Key = Binary;
type Value = Binary;
type Error = KVTableTrError;
fn dirty_query(&self, key: <Self as KVAction>::Key)
-> BoxFuture<Option<<Self as KVAction>::Value>> {
let tr = self.clone();
async move {
if let Some(value) = tr.0.root_mut.lock().get(&key) {
return Some(value.clone());
}
None
}.boxed()
}
fn query(&self, key: <Self as KVAction>::Key)
-> BoxFuture<Option<<Self as KVAction>::Value>> {
let tr = self.clone();
async move {
let mut actions_locked = tr.0.actions.lock();
if let None = actions_locked.get(&key) {
let _ = actions_locked.insert(key.clone(), KVActionLog::Read);
}
if let Some(value) = tr.0.root_mut.lock().get(&key) {
return Some(value.clone());
}
None
}.boxed()
}
fn dirty_upsert(&self,
key: <Self as KVAction>::Key,
value: <Self as KVAction>::Value)
-> BoxFuture<Result<(), <Self as KVAction>::Error>> {
let tr = self.clone();
async move {
let _ = tr.0.actions.lock().insert(key.clone(), KVActionLog::DirtyWrite(Some(value.clone())));
let _ = tr.0.root_mut.lock().upsert(key, value, false);
Ok(())
}.boxed()
}
fn upsert(&self,
key: <Self as KVAction>::Key,
value: <Self as KVAction>::Value)
-> BoxFuture<Result<(), <Self as KVAction>::Error>> {
let tr = self.clone();
async move {
let _ = tr.0.actions.lock().insert(key.clone(), KVActionLog::Write(Some(value.clone())));
let _ = tr.0.root_mut.lock().upsert(key, value, false);
Ok(())
}.boxed()
}
fn dirty_delete(&self, key: <Self as KVAction>::Key)
-> BoxFuture<Result<Option<<Self as KVAction>::Value>, <Self as KVAction>::Error>> {
let tr = self.clone();
async move {
let _ = tr.0.actions.lock().insert(key.clone(), KVActionLog::DirtyWrite(None));
if let Some(Some(value)) = tr.0.root_mut.lock().delete(&key, false) {
return Ok(Some(value));
}
Ok(None)
}.boxed()
}
fn delete(&self, key: <Self as KVAction>::Key)
-> BoxFuture<Result<Option<<Self as KVAction>::Value>, <Self as KVAction>::Error>> {
let tr = self.clone();
async move {
let _ = tr.0.actions.lock().insert(key.clone(), KVActionLog::Write(None));
if let Some(Some(value)) = tr.0.root_mut.lock().delete(&key, false) {
return Ok(Some(value));
}
Ok(None)
}.boxed()
}
fn keys<'a>(&self,
key: Option<<Self as KVAction>::Key>,
descending: bool)
-> BoxStream<'a, <Self as KVAction>::Key> {
let root = self.0.root_mut.lock().clone();
let mut iterator = OrdMapSnapshot::new(root, key.as_ref(), descending);
let stream = stream! {
while let Some(key) = iterator.next_key() {
yield key;
}
};
stream.boxed()
}
fn values<'a>(&self,
key: Option<<Self as KVAction>::Key>,
descending: bool)
-> BoxStream<'a, (<Self as KVAction>::Key, <Self as KVAction>::Value)> {
let root = self.0.root_mut.lock().clone();
let mut iterator = OrdMapSnapshot::new(root, key.as_ref(), descending);
let stream = stream! {
while let Some((key, value)) = iterator.next_entry() {
yield (key, value);
}
};
stream.boxed()
}
fn lock_key(&self, _key: <Self as KVAction>::Key)
-> BoxFuture<Result<(), <Self as KVAction>::Error>> {
async move {
Ok(())
}.boxed()
}
fn unlock_key(&self, _key: <Self as KVAction>::Key)
-> BoxFuture<Result<(), <Self as KVAction>::Error>> {
async move {
Ok(())
}.boxed()
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> MetaTabTr<C, Log> {
pub(crate) fn prepare_mode(&self) -> PrepareMode {
self
.0
.version_context
.as_ref()
.map(TableVersionContext::mode)
.unwrap_or(PrepareMode::Ordinary)
}
#[inline]
fn new(source: Atom,
is_writable: bool,
is_persistent: bool,
prepare_timeout: u64,
commit_timeout: u64,
table: MetaTable<C, Log>) -> Self {
let root_ref = table.0.root.lock().clone();
let inner = InnerMetaTabTr {
source,
tid: SpinLock::new(None),
cid: SpinLock::new(None),
status: SpinLock::new(Transaction2PcStatus::default()),
writable: is_writable,
persistence: AtomicBool::new(is_persistent),
prepare_timeout,
commit_timeout,
root_mut: SpinLock::new(root_ref.clone()),
root_ref,
table,
actions: SpinLock::new(XHashMap::default()),
version_context: None,
};
MetaTabTr(Arc::new(inner))
}
pub(crate) fn new_managed(source: Atom,
is_writable: bool,
is_persistent: bool,
prepare_timeout: u64,
commit_timeout: u64,
table: MetaTable<C, Log>,
versions: KeyVersions,
mode: PrepareMode,
expected: XHashMap<Binary, Version>,
receipt: Option<VersionReceipt>,
actions: XHashMap<Binary, KVActionLog>) -> Self {
let root_locked = table.0.root.lock();
let root_ref = root_locked.clone();
let snapshot = versions.lease_current();
drop(root_locked);
let mut root_mut = root_ref.clone();
for (key, action) in &actions {
match action {
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
let _ = root_mut.upsert(key.clone(), value.clone(), false);
},
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
let _ = root_mut.delete(key, false);
},
KVActionLog::Read => (),
}
}
let version_context = TableVersionContext::new(versions,
snapshot,
mode,
expected,
receipt);
let inner = InnerMetaTabTr {
source,
tid: SpinLock::new(None),
cid: SpinLock::new(None),
status: SpinLock::new(Transaction2PcStatus::default()),
writable: is_writable,
persistence: AtomicBool::new(is_persistent),
prepare_timeout,
commit_timeout,
root_mut: SpinLock::new(root_mut),
root_ref,
table,
actions: SpinLock::new(actions),
version_context: Some(version_context),
};
MetaTabTr(Arc::new(inner))
}
async fn precheck_versions(&self) -> Result<(), KVTableTrError> {
let Some(context) = self.0.version_context.as_ref() else {
return Ok(());
};
if context.mode() != PrepareMode::Versioned {
return Ok(());
}
let _publication = context.versions().publication().read().await;
let mut conflicts = Vec::new();
for (key, expected) in context.expected() {
if context.versions().current_version(key).as_ref() != Some(expected) {
conflicts.push(TableKeyConflict {
table: self.0.table.name(),
key: key.clone(),
kind: VersionConflictKind::ReadSetVersionMismatch,
});
}
}
if conflicts.is_empty() {
Ok(())
} else {
Err(KVTableTrError::new_all_conflicts_error(conflicts))
}
}
async fn prepare_registered(&self,
conflict_kind: PrepareConflictKind)
-> Result<Option<Vec<u8>>, KVTableTrError> {
if !self.is_writable() {
return Ok(None);
}
let _publication = match self.0.version_context.as_ref() {
Some(context) => Some(context.versions().publication().read().await),
None => None,
};
let actions = self.0.actions.lock().clone();
let mode = self
.0
.version_context
.as_ref()
.map(TableVersionContext::mode)
.unwrap_or(PrepareMode::Ordinary);
let mut conflict_keys = Vec::new();
if let Some(context) = self.0.version_context.as_ref() {
if context.mode() == PrepareMode::Versioned {
for (key, expected) in context.expected() {
if context.versions().current_version(key).as_ref() != Some(expected) {
conflict_keys.push((key.clone(),
VersionConflictKind::ReadSetVersionMismatch));
}
}
}
}
let current_root = self.0.table.0.root.lock().clone();
for (key, action) in &actions {
if action.is_dirty_writed() {
continue;
}
if let Some(context) = self.0.version_context.as_ref() {
if context
.versions()
.has_committed_after(key, context.snapshot_revision()) {
conflict_keys.push((key.clone(),
VersionConflictKind::TransactionConflict));
continue;
}
}
if !binary_state_equal(self.0.root_ref.get(key), current_root.get(key)) {
conflict_keys.push((key.clone(),
VersionConflictKind::TransactionConflict));
}
}
let write_buf = self.prepare_output(&actions);
let mut prepare = self.0.table.0.prepare.lock();
let transaction_uid = self.get_transaction_uid().unwrap();
if has_prepared_transaction(&prepare, &transaction_uid) {
return Err(KVTableTrError::new_transaction_error(
ErrorLevel::Normal,
format!("Prepare meta table failed, table: {:?}, source: {:?}, transaction_uid: {:?}, reason: duplicate prepared transaction uid",
self.0.table.name().as_str(),
self.0.source,
transaction_uid)));
}
for (key, action) in &actions {
if has_prepared_conflict(&prepare, key, mode, action) {
conflict_keys.push((key.clone(),
VersionConflictKind::TransactionConflict));
}
}
if !conflict_keys.is_empty() {
return Err(self.prepare_conflict_error(conflict_kind, conflict_keys));
}
let _ = mem::replace(&mut *self.0.actions.lock(), XHashMap::default());
prepare.insert(transaction_uid, PreparedActions {
mode,
actions,
});
Ok(write_buf)
}
fn prepare_output(&self,
actions: &XHashMap<Binary, KVActionLog>) -> Option<Vec<u8>> {
let writed_count = actions
.values()
.filter(|action| matches!(action,
KVActionLog::Write(_) | KVActionLog::DirtyWrite(_)))
.count() as u64;
if writed_count == 0 {
return None;
}
let mut buf = Vec::new();
self.0.table.init_table_prepare_output(&mut buf, writed_count);
for (key, action) in actions {
match action {
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
self.0.table.append_key_value_to_table_prepare_output(&mut buf, key, None);
},
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
self.0.table.append_key_value_to_table_prepare_output(&mut buf,
key,
Some(value));
},
KVActionLog::Read => (),
}
}
Some(buf)
}
fn prepare_conflict_error(&self,
conflict_kind: PrepareConflictKind,
keys: Vec<(Binary, VersionConflictKind)>) -> KVTableTrError {
let key = keys[0].0.clone();
match conflict_kind {
PrepareConflictKind::Common => {
KVTableTrError::new_transaction_error(
ErrorLevel::Normal,
format!("Prepare meta table conflicted, table: {:?}, key: {:?}, source: {:?}, transaction_uid: {:?}, reason: committed state or prepared reservation changed",
self.0.table.name().as_str(),
key,
self.0.source,
self.get_transaction_uid()))
},
PrepareConflictKind::First => {
KVTableTrError::new_conflicts_error(self.0.table.name(), key)
},
PrepareConflictKind::All => {
KVTableTrError::new_all_conflicts_error(keys
.into_iter()
.map(|(key, kind)| TableKeyConflict {
table: self.0.table.name(),
key,
kind,
})
.collect())
},
}
}
pub(crate) fn prepare_repair(&self, transaction_uid: Guid) {
let actions = mem::replace(&mut *self.0.actions.lock(), XHashMap::default());
for (key, action) in &actions {
match action {
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
self
.0
.table
.0
.root
.lock()
.upsert(key.clone(), value.clone(), false);
},
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
self.0.table.0.root.lock().delete(key, false);
},
KVActionLog::Read => (), }
}
self.0.table.0.prepare.lock().insert(transaction_uid, PreparedActions {
mode: PrepareMode::Ordinary,
actions,
});
}
}
struct InnerMetaTabTr<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> {
source: Atom, tid: SpinLock<Option<Guid>>, cid: SpinLock<Option<Guid>>, status: SpinLock<Transaction2PcStatus>, writable: bool, persistence: AtomicBool, prepare_timeout: u64, commit_timeout: u64, root_mut: SpinLock<OrdMap<Tree<Binary, Binary>>>, root_ref: OrdMap<Tree<Binary, Binary>>, table: MetaTable<C, Log>, actions: SpinLock<XHashMap<Binary, KVActionLog>>, version_context: Option<TableVersionContext>, }
struct MetaTableLoader<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> {
statistics: XHashMap<PathBuf, (u64, u64)>, removed: XHashMap<Vec<u8>, ()>, table: MetaTable<C, Log>, }
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> PairLoader for MetaTableLoader<C, Log> {
fn is_require(&self, _log_file: Option<&PathBuf>, key: &Vec<u8>) -> bool {
!self
.removed
.contains_key(key)
&&
self
.table
.0
.root
.lock()
.get(&Binary::new(key.clone()))
.is_none()
}
fn load(&mut self,
log_file: Option<&PathBuf>,
_method: LogMethod,
key: Vec<u8>,
value: Option<Vec<u8>>) {
if let Some(value) = value {
if let Some(path) = log_file {
match self.statistics.entry(path.clone()) {
HashMapEntry::Occupied(mut o) => {
let statistics = o.get_mut();
statistics.0 += 1;
statistics.1 += (key.len() + value.len()) as u64;
},
HashMapEntry::Vacant(v) => {
v.insert((1, (key.len() + value.len()) as u64));
},
}
}
self.table.0.root.lock().insert(Binary::new(key), Binary::new(value));
} else {
self.removed.insert(key, ());
}
}
}
impl<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
> MetaTableLoader<C, Log> {
pub fn new(table: MetaTable<C, Log>) -> Self {
MetaTableLoader {
statistics: XHashMap::default(),
removed: XHashMap::default(),
table,
}
}
pub fn log_files_len(&self) -> usize {
self.statistics.len()
}
pub fn keys_len(&self) -> u64 {
let mut len = 0;
for statistics in self.statistics.values() {
len += statistics.0;
}
len
}
pub fn bytes_len(&self) -> u64 {
let mut len = 0;
for statistics in self.statistics.values() {
len += statistics.1;
}
len
}
}
async fn collect_waits<
C: Clone + Send + 'static,
Log: AsyncCommitLog<C = C, Cid = Guid>,
>(table: &MetaTable<C, Log>,
timeout: Option<usize>) -> Result<(Duration, (usize, usize, usize)), (Duration, (usize, usize, usize))> {
if let Some(timeout) = timeout {
table.0.rt.timeout(timeout).await;
}
if let Err(_) = table.0.collecting.compare_exchange(false,
true,
Ordering::Acquire,
Ordering::Relaxed) {
return Ok((Instant::now().elapsed(), (0, 0, 0)));
}
let mut waits = VecDeque::new();
let mut log_uid = 0;
let mut trs_len = 0;
let mut keys_len = 0;
let mut bytes_len = 0;
let now = Instant::now();
{
let mut locked = table
.0
.waits
.lock()
.await;
while let Some((wait_tr, actions, confirm)) = locked.pop_front() {
for (key, actions) in actions.iter() {
match actions {
KVActionLog::Write(None) | KVActionLog::DirtyWrite(None) => {
log_uid = table
.0
.log_file
.append(LogMethod::Remove,
key.as_ref(),
&[]);
keys_len += 1;
bytes_len += key.len();
},
KVActionLog::Write(Some(value)) | KVActionLog::DirtyWrite(Some(value)) => {
log_uid = table
.0
.log_file
.append(LogMethod::PlainAppend,
key.as_ref(),
value.as_ref());
keys_len += 1;
bytes_len += key.len() + value.len();
},
KVActionLog::Read => (), }
}
trs_len += 1;
waits.push_back((wait_tr, confirm));
}
if let Err(e) = table
.0
.log_file
.delay_commit(log_uid,
false,
DEFAULT_LOG_FILE_COMMIT_DELAY_TIMEOUT)
.await {
table.0.collecting.store(false, Ordering::Release); error!("Collect meta table failed, table: {:?}, transactions: {}, keys: {}, bytes: {}, reason: {:?}",
table.name().as_str(),
trs_len,
keys_len,
bytes_len,
e);
return Err((now.elapsed(), (trs_len, keys_len, bytes_len)));
}
}
if let Some(notifier) = table.0.notifier.as_ref() {
for (wait_tr, confirm) in waits {
if let Err(e) = confirm(wait_tr.get_transaction_uid().unwrap(),
wait_tr.get_commit_uid().unwrap(),
Ok(())) {
notifier.send(KVDBEvent::CommitFailed(wait_tr.get_source(),
wait_tr.0.table.name(),
KVDBTableType::BtreeOrdTab,
wait_tr.get_transaction_uid().unwrap(),
wait_tr.get_commit_uid().unwrap()))
.await;
error!("Collect meta table failed, table: {:?}, transactions: {}, keys: {}, bytes: {}, reason: {:?}",
table.name().as_str(),
trs_len,
keys_len,
bytes_len,
e);
} else {
notifier.send(KVDBEvent::ConfirmCommited(wait_tr.get_source(),
wait_tr.0.table.name(),
KVDBTableType::BtreeOrdTab,
wait_tr.get_transaction_uid().unwrap(),
wait_tr.get_commit_uid().unwrap()))
.await;
}
}
} else {
for (wait_tr, confirm) in waits {
if let Err(e) = confirm(wait_tr.get_transaction_uid().unwrap(),
wait_tr.get_commit_uid().unwrap(),
Ok(())) {
error!("Collect meta table failed, table: {:?}, transactions: {}, keys: {}, bytes: {}, reason: {:?}",
table.name().as_str(),
trs_len,
keys_len,
bytes_len,
e);
}
}
}
table.0.collecting.store(false, Ordering::Release);
Ok((now.elapsed(), (trs_len, keys_len, bytes_len)))
}
#[cfg(test)]
mod meta_local_contract_tests {
use std::{fs,
path::PathBuf,
sync::{mpsc::sync_channel,
atomic::{AtomicU64, Ordering as AtomicOrdering}},
time::{SystemTime, UNIX_EPOCH}};
use futures::executor::block_on;
use pi_async_rt::{prelude::AsyncRuntimeExt,
rt::multi_thread::MultiTaskRuntimeBuilder};
use pi_bon::{Encode, WriteBuffer};
use pi_store::commit_logger::CommitLogger;
use super::*;
type TestTable = MetaTable<usize, CommitLogger>;
static NEXT_TEST_ROOT: AtomicU64 = AtomicU64::new(0);
struct LocalMetaFixture {
table: Option<TestTable>,
path: PathBuf,
}
impl LocalMetaFixture {
fn new(label: &str) -> Self {
let sequence = NEXT_TEST_ROOT.fetch_add(1, AtomicOrdering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time must be after the Unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"pi_db_meta_local_{label}_{}_{}_{}",
std::process::id(),
nanos,
sequence,
));
let rt = MultiTaskRuntimeBuilder::default()
.init_worker_size(1)
.build();
let open_rt = rt.clone();
let open_path = path.clone();
let (sender, receiver) = sync_channel(1);
rt.block_on(async move {
let result = LogFile::open(open_rt,
open_path,
2 * 1024 * 1024,
64 * 1024 * 1024,
None).await;
sender.send(result).expect("Meta local LogFile receiver must remain alive");
})
.expect("Meta local runtime must complete LogFile::open");
let log_file = receiver
.recv()
.expect("Meta local LogFile result must be returned")
.expect("Meta local LogFile must open");
let table = MetaTable(Arc::new(InnerMetaTable {
name: Atom::from(".tables_meta"),
root: Mutex::new(OrdMap::new(None)),
prepare: Mutex::new(XHashMap::default()),
rt,
waits: AsyncMutex::new(VecDeque::new()),
waits_size: AtomicUsize::new(0),
waits_limit: 16 * 1024 * 1024,
wait_timeout: 60 * 1000,
collecting: AtomicBool::new(false),
log_file,
notifier: None,
}));
Self {
table: Some(table),
path,
}
}
fn table(&self) -> TestTable {
self.table.as_ref().expect("fixture table must exist").clone()
}
}
impl Drop for LocalMetaFixture {
fn drop(&mut self) {
drop(self.table.take());
let _ = fs::remove_dir_all(&self.path);
}
}
fn bon_usize(value: usize) -> Binary {
let mut buffer = WriteBuffer::new();
value.encode(&mut buffer);
Binary::new(buffer.bytes)
}
fn table_key(name: &str) -> Binary {
crate::db::table_to_binary(&Atom::from(name))
}
fn assert_binary(actual: Option<Binary>, expected: Option<&Binary>, label: &str) {
match (actual, expected) {
(Some(actual), Some(expected)) => {
assert_eq!(actual.as_ref(), expected.as_ref(), "{label}: value mismatch");
},
(None, None) => (),
(actual, expected) => {
panic!("{label}: presence mismatch, actual: {}, expected: {}",
actual.is_some(),
expected.is_some());
},
}
}
#[test]
fn test_meta_metadata_leaf_identity_and_qos_contract() {
let fixture = LocalMetaFixture::new("identity");
let table = fixture.table();
assert_eq!(table.name().as_str(), ".tables_meta");
assert_eq!(table.path(), Some(fixture.path.as_path()));
assert!(table.is_persistent());
assert!(table.is_ordered());
assert_eq!(table.len(), 0);
assert_eq!(table.size(), 0);
let transaction = table.transaction(Atom::from("Meta local identity source"),
true,
false,
1_234,
5_678);
assert!(transaction.is_writable());
assert!(!transaction.is_concurrent_prepare());
assert!(!transaction.is_concurrent_commit());
assert!(!transaction.is_concurrent_rollback());
assert!(transaction.is_enable_inherit_uid());
assert_eq!(transaction.get_source().as_str(), "Meta local identity source");
assert_eq!(transaction.get_prepare_timeout(), 1_234);
assert_eq!(transaction.get_commit_timeout(), 5_678);
assert_eq!(transaction.get_status(), Transaction2PcStatus::Start);
assert!(transaction.is_unit());
assert!(!transaction.is_sequence());
assert!(!transaction.is_tree());
assert!(transaction.prev_item().is_none());
assert!(transaction.next_item().is_none());
assert_eq!(transaction.children_len(), 0);
assert_eq!(transaction.to_children().count(), 0);
assert_eq!(transaction.qos(), TableTrQos::ThreadSafe);
assert!(block_on(transaction.init()).is_ok());
let tid = Guid(101);
let cid = Guid(102);
transaction.set_transaction_uid(tid.clone());
transaction.set_commit_uid(cid.clone());
transaction.set_prepare_uid(Guid(103));
assert_eq!(transaction.get_transaction_uid(), Some(tid));
assert_eq!(transaction.get_commit_uid(), Some(cid));
assert!(transaction.get_prepare_uid().is_none());
transaction.set_status(Transaction2PcStatus::Actioning);
assert_eq!(transaction.get_status(), Transaction2PcStatus::Actioning);
transaction.require_persistence();
transaction.require_persistence();
assert!(transaction.is_require_persistence());
assert_eq!(transaction.qos(), TableTrQos::Safe);
let read_only = table.transaction(Atom::from("Meta local read only"),
false,
true,
7,
9);
assert!(matches!(block_on(read_only.prepare()), Ok(None)));
assert!(table.0.prepare.lock().is_empty());
}
#[test]
fn test_meta_private_cow_final_action_and_snapshot_contract() {
let fixture = LocalMetaFixture::new("actions");
let table = fixture.table();
let retained_key = table_key("meta_local_retained");
let deleted_key = table_key("meta_local_deleted");
let committed_value = bon_usize(10);
let first_value = bon_usize(11);
let final_value = bon_usize(12);
table.0.root.lock().upsert(deleted_key.clone(), committed_value.clone(), false);
let transaction = table.transaction(Atom::from("Meta local actions source"),
true,
true,
100,
200);
block_on(transaction.upsert(retained_key.clone(), first_value.clone()))
.expect("first private Meta upsert must succeed");
let snapshot = transaction.values(None, false);
block_on(transaction.upsert(retained_key.clone(), final_value.clone()))
.expect("final private Meta upsert must succeed");
let removed = block_on(transaction.delete(deleted_key.clone()))
.expect("private Meta delete must succeed");
assert!(removed.is_none(), "Meta delete must not expose the old value");
assert_binary(block_on(transaction.query(retained_key.clone())),
Some(&final_value),
"transaction must observe final private upsert");
assert_binary(table.query_committed(&retained_key),
None,
"uncommitted Meta upsert must not reach shared root");
assert_binary(table.query_committed(&deleted_key),
Some(&committed_value),
"uncommitted Meta delete must not reach shared root");
let snapshot_entries = block_on(snapshot.collect::<Vec<_>>());
assert_eq!(snapshot_entries.len(), 2);
assert!(snapshot_entries.iter().any(|(key, value)| {
key.as_ref() == retained_key.as_ref() && value.as_ref() == first_value.as_ref()
}));
assert!(snapshot_entries.iter().any(|(key, value)| {
key.as_ref() == deleted_key.as_ref() && value.as_ref() == committed_value.as_ref()
}));
let actions = transaction.0.actions.lock();
assert_eq!(actions.len(), 2);
assert!(matches!(actions.get(&retained_key),
Some(KVActionLog::Write(Some(value)))
if value.as_ref() == final_value.as_ref()));
assert!(matches!(actions.get(&deleted_key), Some(KVActionLog::Write(None))));
}
#[test]
fn test_meta_prepare_wal_conflict_ownership_and_rollback_contract() {
let fixture = LocalMetaFixture::new("prepare");
let table = fixture.table();
let upsert_key = table_key("meta_local_prepare_upsert");
let delete_key = table_key("meta_local_prepare_delete");
let read_key = table_key("meta_local_prepare_read");
let old_value = bon_usize(210);
let new_value = bon_usize(211);
table.0.root.lock().upsert(delete_key.clone(), old_value.clone(), false);
let transaction = table.transaction(Atom::from("Meta local prepare source"),
true,
true,
300,
400);
let tid = Guid(201);
transaction.set_transaction_uid(tid.clone());
block_on(transaction.upsert(upsert_key.clone(), new_value.clone()))
.expect("private Meta upsert before prepare must succeed");
block_on(transaction.delete(delete_key.clone()))
.expect("private Meta delete before prepare must succeed");
assert!(block_on(transaction.query(read_key.clone())).is_none());
let output = block_on(transaction.prepare_conflicts())
.expect("Meta prepare must succeed")
.expect("Meta writes must produce a WAL fragment");
let (table_name, write_count, offset) =
<TestTable as KVTable>::get_init_table_prepare_output(&output, 0);
let (writes, end) =
<TestTable as KVTable>::get_all_key_value_from_table_prepare_output(
&output,
&table_name,
write_count,
offset);
assert_eq!(table_name.as_str(), ".tables_meta");
assert_eq!(write_count, 2, "Read must not enter the Meta WAL fragment");
assert_eq!(writes.len(), 2);
assert_eq!(end, output.len());
assert!(writes.iter().any(|entry| {
entry.key.as_ref() == upsert_key.as_ref()
&& entry.value.as_ref().map(Binary::as_ref) == Some(new_value.as_ref())
}));
assert!(writes.iter().any(|entry| {
entry.key.as_ref() == delete_key.as_ref() && entry.value.is_none()
}));
assert!(transaction.0.actions.lock().is_empty());
{
let prepared = table.0.prepare.lock();
let item = prepared.get(&tid).expect("Meta prepare map must reserve the root TID");
assert_eq!(item.mode, PrepareMode::Ordinary);
assert_eq!(item.actions.len(), 3);
assert!(matches!(item.actions.get(&read_key), Some(KVActionLog::Read)));
}
let contender = table.transaction(Atom::from("Meta local prepared contender"),
true,
true,
500,
600);
contender.set_transaction_uid(Guid(202));
block_on(contender.upsert(upsert_key.clone(), bon_usize(212)))
.expect("Meta contender action must succeed locally");
let conflict = block_on(contender.prepare_conflicts())
.expect_err("same-Key prepared Meta contender must conflict");
assert!(conflict.is_conflicts());
block_on(contender.rollback()).expect("Meta contender rollback must succeed");
assert_binary(table.query_committed(&upsert_key),
None,
"prepare must not publish Meta upsert");
assert_binary(table.query_committed(&delete_key),
Some(&old_value),
"prepare must not publish Meta delete");
block_on(transaction.rollback()).expect("Meta rollback must release prepared state");
assert!(table.0.prepare.lock().is_empty());
}
#[test]
fn test_meta_dirty_prepare_current_conflict_branch() {
let fixture = LocalMetaFixture::new("dirty");
let table = fixture.table();
let key = table_key("meta_local_dirty");
let initial_value = bon_usize(310);
let private_value = bon_usize(311);
let concurrent_value = bon_usize(312);
table.0.root.lock().upsert(key.clone(), initial_value, false);
let transaction = table.transaction(Atom::from("Meta local dirty source"),
true,
true,
700,
800);
let tid = Guid(301);
transaction.set_transaction_uid(tid.clone());
block_on(transaction.dirty_upsert(key.clone(), private_value))
.expect("Meta dirty upsert must succeed locally");
table.0.root.lock().upsert(key.clone(), concurrent_value.clone(), false);
assert!(block_on(transaction.prepare_conflicts())
.expect("Meta DirtyWrite currently skips committed value comparison")
.is_some());
assert!(table.0.prepare.lock().contains_key(&tid));
assert_binary(table.query_committed(&key),
Some(&concurrent_value),
"Meta dirty prepare must not publish its private value");
block_on(transaction.rollback()).expect("Meta dirty rollback must succeed");
assert!(table.0.prepare.lock().is_empty());
}
#[test]
fn test_meta_repair_final_state_and_local_idempotence() {
let fixture = LocalMetaFixture::new("repair");
let table = fixture.table();
let upsert_key = table_key("meta_local_repair_upsert");
let delete_key = table_key("meta_local_repair_delete");
let old_value = bon_usize(410);
let repaired_value = bon_usize(411);
table.0.root.lock().upsert(delete_key.clone(), old_value, false);
let first = table.transaction(Atom::from("Meta local repair first"),
true,
true,
900,
1_000);
block_on(first.upsert(upsert_key.clone(), repaired_value.clone()))
.expect("first Meta repair upsert action must be staged");
block_on(first.delete(delete_key.clone()))
.expect("first Meta repair delete action must be staged");
let first_tid = Guid(401);
first.prepare_repair(first_tid.clone());
assert_binary(table.query_committed(&upsert_key),
Some(&repaired_value),
"Meta repair must apply upsert directly");
assert_binary(table.query_committed(&delete_key),
None,
"Meta repair must apply delete directly");
assert!(first.0.actions.lock().is_empty());
let second = table.transaction(Atom::from("Meta local repair second"),
true,
true,
1_100,
1_200);
block_on(second.upsert(upsert_key.clone(), repaired_value.clone()))
.expect("second Meta repair upsert action must be staged");
block_on(second.delete(delete_key.clone()))
.expect("second Meta repair delete action must be staged");
let second_tid = Guid(402);
second.prepare_repair(second_tid.clone());
assert_eq!(table.len(), 1);
assert_binary(table.query_committed(&upsert_key),
Some(&repaired_value),
"repeated Meta repair must retain final upsert");
assert_binary(table.query_committed(&delete_key),
None,
"repeated Meta repair must retain final delete");
let mut prepared = table.0.prepare.lock();
assert_eq!(prepared.len(), 2);
assert_eq!(prepared.remove(&first_tid).map(|item| item.mode),
Some(PrepareMode::Ordinary));
assert_eq!(prepared.remove(&second_tid).map(|item| item.mode),
Some(PrepareMode::Ordinary));
assert!(prepared.is_empty());
}
#[test]
fn test_meta_loader_newest_tombstone_and_statistics_contract() {
let fixture = LocalMetaFixture::new("loader");
let table = fixture.table();
let newest_key = table_key("meta_local_loader_newest");
let removed_key = table_key("meta_local_loader_removed");
let older_key = table_key("meta_local_loader_older");
let newest_value = bon_usize(510);
let ignored_older_value = bon_usize(511);
let older_value = bon_usize(512);
let newer_path = PathBuf::from("meta-newer.log");
let older_path = PathBuf::from("meta-older.log");
let mut loader = MetaTableLoader::new(table.clone());
assert!(loader.is_require(Some(&newer_path), &newest_key.as_ref().to_vec()));
loader.load(Some(&newer_path),
LogMethod::PlainAppend,
newest_key.as_ref().to_vec(),
Some(newest_value.as_ref().to_vec()));
assert!(!loader.is_require(Some(&older_path), &newest_key.as_ref().to_vec()));
assert!(loader.is_require(Some(&newer_path), &removed_key.as_ref().to_vec()));
loader.load(Some(&newer_path),
LogMethod::Remove,
removed_key.as_ref().to_vec(),
None);
assert!(!loader.is_require(Some(&older_path), &removed_key.as_ref().to_vec()));
assert!(loader.is_require(Some(&older_path), &older_key.as_ref().to_vec()));
loader.load(Some(&older_path),
LogMethod::PlainAppend,
older_key.as_ref().to_vec(),
Some(older_value.as_ref().to_vec()));
assert_binary(table.query_committed(&newest_key),
Some(&newest_value),
"newest Meta loader value must win");
assert_binary(table.query_committed(&removed_key),
None,
"Meta loader tombstone must suppress older value");
assert_binary(table.query_committed(&older_key),
Some(&older_value),
"unshadowed older Meta value must load");
assert_ne!(table.query_committed(&newest_key), Some(ignored_older_value));
assert_eq!(loader.log_files_len(), 2);
assert_eq!(loader.keys_len(), 2);
assert_eq!(loader.bytes_len(),
(newest_key.len() + newest_value.len()
+ older_key.len() + older_value.len()) as u64);
}
}