#![allow(dead_code)]
use std::{
fs,
future::Future,
path::{Path, PathBuf},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use crossbeam_channel::bounded;
use pi_async_rt::rt::{
multi_thread::{MultiTaskRuntime, MultiTaskRuntimeBuilder},
startup_global_time_loop, AsyncRuntime,
};
use pi_async_transaction::{
AsyncCommitLog,
manager_2pc::Transaction2PcManager,
};
use pi_atom::Atom;
use pi_bon::{Encode, WriteBuffer};
use pi_db::{
db::{KVDBManager, KVDBManagerBuilder, KVDBTransaction},
tables::TableKV,
utils::CreateTableOptions,
Binary, KVDBTableType, KVTableMeta, TableKeyVersion, Version,
};
use pi_guid::{Guid, GuidGen};
use pi_sinfo::EnumType;
use pi_store::commit_logger::{CommitLogger, CommitLoggerBuilder};
pub type TestResult<T = ()> = Result<T, String>;
pub type RealDb = KVDBManager<usize, CommitLogger>;
pub type RealTransaction = KVDBTransaction<usize, CommitLogger>;
pub type RealTrManager = Transaction2PcManager<usize, CommitLogger>;
pub const META_TABLE: &str = ".tables_meta";
pub const MEMORY_TABLE: &str = "key_version_memory";
pub const LOG_ORDERED_TABLE: &str = "key_version_log_ordered";
pub const BTREE_TABLE: &str = "key_version_btree";
pub struct Fixture {
pub db: RealDb,
pub tr_manager: RealTrManager,
pub logger: CommitLogger,
}
pub fn run_on_runtime<T, F, Fut>(timeout: Duration, build: F) -> TestResult<T>
where
T: Send + 'static,
F: FnOnce(MultiTaskRuntime<()>) -> Fut,
Fut: Future<Output = TestResult<T>> + Send + 'static,
{
let _time_loop = startup_global_time_loop(1);
let rt = MultiTaskRuntimeBuilder::default()
.init_worker_size(4)
.build();
let future = build(rt.clone());
let (result_tx, result_rx) = bounded(1);
rt.spawn(async move {
let _ = result_tx.send(future.await);
})
.map_err(|error| format!("spawning key-version target failed: {error:?}"))?;
result_rx
.recv_timeout(timeout)
.map_err(|error| format!("key-version target exceeded {timeout:?}: {error}"))?
}
pub async fn build_database(
rt: &MultiTaskRuntime<()>,
root: &Path,
ttl: Duration,
poll_interval: Duration,
) -> TestResult<Fixture> {
fs::create_dir_all(root)
.map_err(|error| format!("creating key-version fixture root {root:?} failed: {error}"))?;
let wal_path = root.join("root-wal");
let logger = CommitLoggerBuilder::new(rt.clone(), &wal_path)
.log_file_limit(64 * 1024 * 1024)
.collect_interval(5 * 60 * 1000)
.build()
.await
.map_err(|error| format!("building CommitLogger at {wal_path:?} failed: {error}"))?;
let tr_manager = Transaction2PcManager::new(
rt.clone(),
GuidGen::new(0, std::process::id() as u16),
logger.clone(),
);
let db_path = root.join("database");
let db = KVDBManagerBuilder::new(rt.clone(), tr_manager.clone(), &db_path)
.key_version_ttl(ttl)
.key_version_ttl_poll_interval(poll_interval)
.startup(false)
.await
.map_err(|error| format!("starting key-version database at {db_path:?} failed: {error}"))?;
Ok(Fixture {
db,
tr_manager,
logger,
})
}
pub async fn create_active_tables(fixture: &Fixture) -> TestResult<()> {
let transaction = writable_transaction(&fixture.db, "key-version active-table DDL")?;
transaction
.create_table(
Atom::from(MEMORY_TABLE),
table_meta(KVDBTableType::MemOrdTab, true),
false,
)
.await
.map_err(|error| format!("creating key-version Memory table failed: {error}"))?;
transaction
.create_table_with_options(
Atom::from(LOG_ORDERED_TABLE),
table_meta(KVDBTableType::LogOrdTab, true),
CreateTableOptions::LogOrdTab(64 * 1024 * 1024, 1024 * 1024, 1024 * 1024),
false,
)
.await
.map_err(|error| format!("creating key-version LogOrdered table failed: {error}"))?;
transaction
.create_table_with_options(
Atom::from(BTREE_TABLE),
table_meta(KVDBTableType::BtreeOrdTab, true),
CreateTableOptions::BtreeOrdTab(4 * 1024 * 1024, false),
false,
)
.await
.map_err(|error| format!("creating key-version Btree table failed: {error}"))?;
commit_ordinary(&transaction, "key-version active-table DDL").await?;
expect_eq("registered table count", &fixture.db.table_size().await, &4usize)
}
pub fn writable_transaction(db: &RealDb, source: &str) -> TestResult<RealTransaction> {
db.transaction(Atom::from(source), true, 10_000, 10_000)
.ok_or_else(|| format!("database rejected writable transaction {source}"))
}
pub fn read_only_transaction(db: &RealDb, source: &str) -> TestResult<RealTransaction> {
db.transaction(Atom::from(source), false, 10_000, 10_000)
.ok_or_else(|| format!("database rejected read-only transaction {source}"))
}
pub async fn commit_ordinary(transaction: &RealTransaction, label: &str) -> TestResult<()> {
let prepare = transaction
.prepare_modified_conflicts()
.await
.map_err(|error| format!("preparing {label} failed: {error:?}"))?;
transaction
.commit_modified(prepare)
.await
.map_err(|error| format!("committing {label} failed: {error:?}"))
}
pub async fn query_ordinary(
db: &RealDb,
table: &str,
key: Binary,
label: &str,
) -> TestResult<Option<Binary>> {
let transaction = read_only_transaction(db, label)?;
let mut values = transaction
.query(vec![TableKV::new(Atom::from(table), key, None)])
.await;
if values.len() != 1 {
return Err(format!(
"{label}: expected one ordinary query slot, observed {}",
values.len(),
));
}
Ok(values.pop().expect("ordinary query result length was checked"))
}
pub fn expect_single_version_receipt(
receipts: &[TableKeyVersion],
table: &Atom,
key: &Binary,
value: Option<&Binary>,
transaction_uid: &Guid,
label: &str,
) -> TestResult<()> {
expect_eq(&format!("{label} receipt count"), &receipts.len(), &1usize)?;
let receipt = &receipts[0];
expect_eq(&format!("{label} receipt table"), &receipt.table, table)?;
expect_binary(&format!("{label} receipt key"), Some(&receipt.key), Some(key))?;
let expected = if value.is_some() {
Version::Upsert(transaction_uid.clone())
} else {
Version::Delete(transaction_uid.clone())
};
expect_eq(&format!("{label} receipt version"), &receipt.version, &expected)
}
pub async fn wait_for_all_confirmed(
rt: &MultiTaskRuntime<()>,
fixture: &Fixture,
timeout: Duration,
label: &str,
) -> TestResult<()> {
let started = Instant::now();
loop {
let waiting = fixture.logger.waiting_confirm_count().await;
let appended = fixture.logger.append_total_count();
let confirmed = fixture.logger.confirm_total_count();
if waiting == 0 && appended == confirmed {
return Ok(());
}
if started.elapsed() >= timeout {
return Err(format!(
"{label}: WAL confirmation did not close within {timeout:?}, waiting={waiting}, appended={appended}, confirmed={confirmed}",
));
}
rt.timeout(5).await;
}
}
pub fn encode_usize(value: usize) -> Binary {
let mut buffer = WriteBuffer::new();
value.encode(&mut buffer);
Binary::new(buffer.bytes)
}
pub fn encode_atom(value: &str) -> Binary {
let mut buffer = WriteBuffer::new();
Atom::from(value).encode(&mut buffer);
Binary::new(buffer.bytes)
}
pub fn table_meta(table_type: KVDBTableType, persistence: bool) -> KVTableMeta {
KVTableMeta::new(table_type, persistence, EnumType::Usize, EnumType::Usize)
}
pub fn expect_eq<T: std::fmt::Debug + PartialEq>(
label: &str,
actual: &T,
expected: &T,
) -> TestResult<()> {
if actual == expected {
Ok(())
} else {
Err(format!(
"{label}: expected {expected:?}, observed {actual:?}",
))
}
}
pub fn expect_binary(
label: &str,
actual: Option<&Binary>,
expected: Option<&Binary>,
) -> TestResult<()> {
let equal = match (actual, expected) {
(None, None) => true,
(Some(actual), Some(expected)) => actual.as_ref() == expected.as_ref(),
_ => false,
};
if equal {
Ok(())
} else {
Err(format!(
"{label}: expected {:?}, observed {:?}",
expected.map(AsRef::<[u8]>::as_ref),
actual.map(AsRef::<[u8]>::as_ref),
))
}
}
pub struct TempRoot {
path: PathBuf,
}
impl TempRoot {
pub fn new(label: &str) -> TestResult<Self> {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| format!("system time is before UNIX_EPOCH: {error}"))?
.as_nanos();
let path = std::env::temp_dir().join(format!(
"pi_db_key_version_{label}_{}_{}",
std::process::id(),
nanos,
));
fs::create_dir_all(&path)
.map_err(|error| format!("creating temporary root {path:?} failed: {error}"))?;
Ok(Self { path })
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}