#![allow(clippy::type_complexity)]
use std::hash::Hash;
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
use std::{collections::BTreeMap, path::PathBuf};
use crate::sync::{Mutex, lock};
use armour_core::persist::Persist;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::compaction::Compactor;
use crate::error::SchemaMismatchKind;
use crate::flusher::Flusher;
use crate::hook::TypedWriteHook;
#[cfg(all(feature = "typed-tree", feature = "armour"))]
use crate::queue::wait::QueueWakeService;
use crate::shutdown::ShutdownSignal;
use crate::sweeper::Sweeper;
use crate::ttl::{SweepOutcome, log_sweep_failures};
use crate::{
Codec, CollectionMeta, Config, DbError, DbResult, FixedConfig, Key, NoHook, TreeMeta, TtlHook,
};
use crate::{
TypedMap, TypedTree, UniqueIndex, UniqueNamespace, UniqueRow, UniqueTargets, ZeroMap, ZeroTree,
};
use super::collection::Collection;
use super::inspection::InspectionHandler;
use super::migration::TypedMigration;
use super::seq::SeqGen;
#[cfg(all(feature = "typed-tree", feature = "armour"))]
use crate::queue::{Queue, QueueConfig, QueueSpec, retention::QueueRetentionPass};
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct DbInfo {
#[serde(default)]
pub version: u32,
pub collections: BTreeMap<String, CollectionInfo>,
#[serde(default)]
pub clean: bool,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)]
pub struct CollectionInfo {
pub version: u16,
pub typ_hash: u64,
#[serde(default)]
pub seq: u64,
#[serde(default)]
pub projections_dirty: bool,
#[serde(default)]
pub counters_dirty: bool,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
struct UserMeta {
db: Option<serde_json::Value>,
collections: BTreeMap<String, serde_json::Value>,
}
const DEFAULT_COMPACTION_INTERVAL: Duration = Duration::from_secs(60);
const DEFAULT_FLUSH_INTERVAL: Duration = Duration::from_secs(1);
const DEFAULT_TTL_SWEEP_INTERVAL: Duration = Duration::from_secs(60);
const DEFAULT_TTL_BUDGET: usize = 4096;
#[derive(Debug, Clone, Copy)]
pub struct WorkerIntervals {
pub compaction: Option<Duration>,
pub flush: Option<Duration>,
pub ttl_sweep: Option<Duration>,
pub ttl_budget: usize,
}
impl Default for WorkerIntervals {
fn default() -> Self {
Self {
compaction: Some(DEFAULT_COMPACTION_INTERVAL),
flush: Some(DEFAULT_FLUSH_INTERVAL),
ttl_sweep: Some(DEFAULT_TTL_SWEEP_INTERVAL),
ttl_budget: DEFAULT_TTL_BUDGET,
}
}
}
const RESERVED_COLLECTION_NAMES: [&str; 5] = [
"__seq",
"db.info",
"db.user",
crate::lock::LOCK_FILE_NAME,
"txlog",
];
pub const TYP_HASH_MIGRATING: u64 = 0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingTx {
pub tx_id: u64,
pub collections: Vec<String>,
}
pub struct Db {
pub path: PathBuf,
db_info: Persist<DbInfo>,
db_info_arc: Arc<Persist<DbInfo>>,
user_meta: Persist<UserMeta>,
pub seq: Arc<SeqGen>,
pub(crate) shutdown: ShutdownSignal,
#[cfg(all(feature = "typed-tree", feature = "armour"))]
queue_wake_service: Arc<QueueWakeService>,
pub(crate) collections: Arc<Mutex<Vec<Arc<dyn Collection>>>>,
#[cfg(all(feature = "typed-tree", feature = "armour"))]
queue_retentions: Arc<Mutex<Vec<Arc<dyn QueueRetentionPass>>>>,
pub(crate) inspection_handlers: Arc<Mutex<BTreeMap<&'static str, Arc<dyn InspectionHandler>>>>,
reserved_names: Mutex<std::collections::HashSet<&'static str>>,
#[cfg(feature = "rpc")]
pub(crate) handlers: Arc<Mutex<Vec<(u64, Arc<dyn super::handler::RpcHandler>)>>>,
#[cfg(feature = "rpc")]
pub(crate) rpc_handles: Mutex<Vec<super::rpc::RpcHandle>>,
compactor: Option<Compactor>,
flusher: Option<Flusher>,
sweeper: Option<Sweeper>,
ttl_budget: usize,
#[cfg(feature = "fjall")]
lsm_db: Mutex<Option<(fjall::Database, crate::lsm::LsmDbConfig)>>,
#[cfg(feature = "fjall")]
opened_clean: bool,
tx_config: crate::txn::TxConfig,
txlog: Arc<crate::txlog::TxLog>,
tx_replay_done: Mutex<std::collections::HashSet<&'static str>>,
tx_ranks: Arc<Mutex<std::collections::HashMap<&'static str, u8>>>,
claim_owner_tags: Mutex<std::collections::HashMap<(&'static str, u64), &'static str>>,
counter_targets_consumed: Mutex<std::collections::HashSet<&'static str>>,
_lock: Arc<std::fs::File>,
pub(crate) tx_runtime: Arc<crate::txn::TxRuntime>,
}
struct NameReservation<'a> {
db: &'a Db,
name: &'static str,
}
struct StagedManaged<'a, S, W> {
meta: TreeMeta,
collection: Arc<S>,
wire: W,
_name_guard: NameReservation<'a>,
}
struct StagedAdapter<'a> {
name: &'static str,
collection: Arc<dyn Collection>,
inspection: Arc<dyn InspectionHandler>,
#[cfg(feature = "rpc")]
rpc: Option<(u64, Arc<dyn super::handler::RpcHandler>)>,
_name_guard: NameReservation<'a>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ManagedCollectionAccess {
Public,
Internal,
}
impl Drop for NameReservation<'_> {
fn drop(&mut self) {
lock(&self.db.reserved_names).remove(self.name);
}
}
fn ttl_sweep_collection(
c: &Arc<dyn Collection>,
now_ms: u64,
budget: usize,
) -> DbResult<SweepOutcome> {
let name = c.name();
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
c.sweep_expired(now_ms, budget)
})) {
Ok(Ok(out)) => {
log_sweep_failures(Some(name), &out);
Ok(out)
}
Ok(Err(e)) => Err(e),
Err(_) => {
tracing::error!(
collection = name,
"ttl sweep panicked in deadline extractor; skipping collection for this pass"
);
Ok(SweepOutcome::default())
}
}
}
impl Db {
pub(crate) fn queue_wake_service(&self) -> Arc<QueueWakeService> {
Arc::clone(&self.queue_wake_service)
}
pub fn open_queue<N>(&self, config: QueueConfig) -> DbResult<Arc<Queue<N>>>
where
N: QueueSpec,
N::Mode: crate::queue::QueueOpen<N>,
{
<N::Mode as crate::queue::QueueOpen<N>>::open(self, config.build()?)
}
pub fn builder(path: impl AsRef<std::path::Path>) -> DbBuilder {
DbBuilder {
path: path.as_ref().to_path_buf(),
workers: WorkerIntervals::default(),
sync_persist: true,
tx_config: crate::txn::TxConfig::default(),
}
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open(path: impl AsRef<std::path::Path>) -> DbResult<Self> {
Self::builder(path).open()
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open_with_workers(
path: impl AsRef<std::path::Path>,
workers: WorkerIntervals,
) -> DbResult<Self> {
Self::builder(path).workers(workers).open()
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open_test(path: impl AsRef<std::path::Path>) -> DbResult<Self> {
Self::builder(path).for_tests().open()
}
pub fn open_with_tx_config(
path: impl AsRef<std::path::Path>,
tx_config: crate::txn::TxConfig,
) -> DbResult<Self> {
Self::builder(path).tx_config(tx_config).open()
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open_with_compaction(
path: impl AsRef<std::path::Path>,
compaction_interval: Option<Duration>,
) -> DbResult<Self> {
Self::builder(path)
.workers(WorkerIntervals {
compaction: compaction_interval,
..WorkerIntervals::default()
})
.open()
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open_with_intervals(
path: impl AsRef<std::path::Path>,
compaction_interval: Option<Duration>,
flush_interval: Option<Duration>,
) -> DbResult<Self> {
Self::builder(path)
.workers(WorkerIntervals {
compaction: compaction_interval,
flush: flush_interval,
ttl_sweep: Some(DEFAULT_TTL_SWEEP_INTERVAL),
ttl_budget: DEFAULT_TTL_BUDGET,
})
.open()
}
fn open_with_options(
path: impl AsRef<std::path::Path>,
workers: WorkerIntervals,
sync_persist: bool,
tx_config: crate::txn::TxConfig,
) -> DbResult<Self> {
let WorkerIntervals {
compaction: compaction_interval,
flush: flush_interval,
ttl_sweep: ttl_sweep_interval,
ttl_budget,
} = workers;
let path = path.as_ref().to_path_buf();
std::fs::create_dir_all(&path).map_err(crate::DbError::Io)?;
let _lock = Arc::new(crate::lock::acquire(&path)?);
let txlog = Arc::new(crate::txlog::TxLog::open(
&path,
crate::txlog::TxLogConfig {
segment_bytes: tx_config.segment_bytes,
id_window: 4096,
#[cfg(feature = "encryption")]
log_encryption_key: tx_config.log_encryption_key,
},
)?);
let db_info: Persist<DbInfo> = if sync_persist {
Persist::open(path.join("db.info"))?
} else {
Persist::open_no_sync(path.join("db.info"))?
};
let db_info_arc = Arc::new(db_info.clone());
let user_meta = if sync_persist {
Persist::open(path.join("db.user"))?
} else {
Persist::open_no_sync(path.join("db.user"))?
};
let seq = SeqGen::open(path.join("__seq"))?;
let collections: Arc<Mutex<Vec<Arc<dyn Collection>>>> = Arc::new(Mutex::new(Vec::new()));
#[cfg(all(feature = "typed-tree", feature = "armour"))]
let queue_retentions: Arc<Mutex<Vec<Arc<dyn QueueRetentionPass>>>> =
Arc::new(Mutex::new(Vec::new()));
let shutdown = ShutdownSignal::new();
#[cfg(all(feature = "typed-tree", feature = "armour"))]
let queue_wake_service = QueueWakeService::new(shutdown.clone());
let compactor = compaction_interval.map(|interval| {
let cols = collections.clone();
Compactor::start_with_signal(
move || {
let snapshot = lock(&cols).clone();
let mut total = 0;
for c in &snapshot {
total += c.compact()?;
}
Ok(total)
},
interval,
shutdown.clone(),
)
});
let flusher = flush_interval.map(|interval| {
let cols = collections.clone();
Flusher::start_with_signal(
move || {
let snapshot = lock(&cols).clone();
let mut first_err = None;
for c in &snapshot {
if let Err(e) = c.periodic_flush()
&& first_err.is_none()
{
first_err = Some(e);
}
}
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
},
interval,
shutdown.clone(),
)
});
let sweeper = ttl_sweep_interval.map(|interval| {
let cols = collections.clone();
#[cfg(all(feature = "typed-tree", feature = "armour"))]
let retentions = queue_retentions.clone();
Sweeper::start_with_signal(
move |budget| {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| DbError::Internal("system clock before unix epoch"))?
.as_millis() as u64;
let snapshot = lock(&cols).clone();
let mut deleted = 0;
let mut more = false;
let mut first_err = None;
for c in &snapshot {
match ttl_sweep_collection(c, now_ms, budget) {
Ok(out) => {
deleted += out.deleted;
if out.more {
more = true;
}
}
Err(e) if first_err.is_none() => first_err = Some(e),
Err(_) => {}
}
}
#[cfg(all(feature = "typed-tree", feature = "armour"))]
for retention in &*lock(&retentions) {
if let Err(e) = retention.run_retention()
&& first_err.is_none()
{
first_err = Some(e);
}
}
match first_err {
Some(e) => Err(e),
None => Ok(crate::sweeper::SweepPassResult { deleted, more }),
}
},
interval,
ttl_budget,
shutdown.clone(),
)
});
#[cfg(feature = "fjall")]
let opened_clean = {
let was_clean = db_info.cloned().clean;
db_info.update(|i| i.clean = false)?;
was_clean
};
let tx_ranks = Arc::new(Mutex::new(std::collections::HashMap::new()));
let tx_runtime = Arc::new(crate::txn::TxRuntime {
config: tx_config,
log: Arc::clone(&txlog),
collections: Arc::clone(&collections),
ranks: Arc::clone(&tx_ranks),
post_log_queue_producer_permits: Mutex::new(Vec::new()),
_root_lease: Arc::clone(&_lock),
});
Ok(Self {
path,
db_info,
db_info_arc,
user_meta,
seq,
shutdown,
#[cfg(all(feature = "typed-tree", feature = "armour"))]
queue_wake_service,
collections,
#[cfg(all(feature = "typed-tree", feature = "armour"))]
queue_retentions,
inspection_handlers: Arc::new(Mutex::new(BTreeMap::new())),
reserved_names: Mutex::new(std::collections::HashSet::new()),
counter_targets_consumed: Mutex::new(std::collections::HashSet::new()),
#[cfg(feature = "rpc")]
handlers: Arc::new(Mutex::new(Vec::new())),
#[cfg(feature = "rpc")]
rpc_handles: Mutex::new(Vec::new()),
compactor,
flusher,
sweeper,
ttl_budget,
#[cfg(feature = "fjall")]
lsm_db: Mutex::new(None),
#[cfg(feature = "fjall")]
opened_clean,
tx_config,
txlog,
tx_replay_done: Mutex::new(std::collections::HashSet::new()),
tx_ranks,
claim_owner_tags: Mutex::new(std::collections::HashMap::new()),
_lock,
tx_runtime,
})
}
pub fn tx(&self) -> crate::txn::Tx<'_> {
crate::txn::Tx {
context: crate::txn::TxContext::Borrowed(self),
ops: Vec::new(),
claim_ops: Vec::new(),
durable: self.tx_config.durable_by_default,
after_commit: Vec::new(),
queue_producer_permits: Vec::new(),
queue_depth_admissions: crate::queue::QueueDepthAdmissions::default(),
}
}
pub fn tx_config(&self) -> &crate::txn::TxConfig {
&self.tx_config
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-faults"))]
pub fn tx_debug_skip_apply(on: bool) {
crate::txn::SKIP_APPLY.store(on, std::sync::atomic::Ordering::SeqCst);
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-faults"))]
pub fn tx_debug_skip_projection_reconcile(on: bool) {
crate::txn::SKIP_PROJECTION_RECONCILE.store(on, std::sync::atomic::Ordering::SeqCst);
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-faults"))]
pub fn debug_fail_reconcile_value_read(on: bool) {
crate::projection::FAIL_RECONCILE_VALUE_READ.store(on, std::sync::atomic::Ordering::SeqCst);
}
pub(crate) fn txlog(&self) -> &crate::txlog::TxLog {
&self.txlog
}
pub(crate) fn tx_collection_id(
&self,
participant: &dyn crate::txn::participant::TxParticipant,
) -> DbResult<u32> {
let needle = participant as *const dyn crate::txn::participant::TxParticipant as *const ();
let collections = lock(&self.collections);
collections
.iter()
.position(|c| std::sync::Arc::as_ptr(c) as *const () == needle)
.map(|i| i as u32)
.ok_or(DbError::Client("collection was not opened through this Db"))
}
pub(crate) fn tx_rank_of(
&self,
participant: &dyn crate::txn::participant::TxParticipant,
) -> u8 {
lock(&self.tx_ranks)
.get(participant.name())
.copied()
.unwrap_or(0)
}
pub(crate) fn tx_register_projection_targets(
&self,
targets: &crate::projection::IndexTargets,
) -> DbResult<()> {
for handle in targets.erased_targets() {
let participant =
targets
.resolve_tx_participant(handle.name)
.ok_or(DbError::Internal(
"declared index target missing participant",
))?;
self.tx_collection_id(participant.as_ref()).map_err(|_| {
DbError::Client("secondary index target is not published on this Db")
})?;
lock(&self.tx_ranks).insert(handle.name, 1);
}
Ok(())
}
pub(crate) fn tx_register_counter_targets(
&self,
targets: &crate::counter::CounterTargets,
) -> DbResult<()> {
for member in targets.all() {
let participant = member.as_tx_participant();
self.tx_collection_id(participant.as_ref())
.map_err(|_| DbError::Client("counter target is not published on this Db"))?;
lock(&self.tx_ranks).insert(member.collection_name(), 1);
}
Ok(())
}
pub(crate) fn tx_register_claim_targets(&self, targets: &UniqueTargets) -> DbResult<()> {
for target in targets.all() {
let participant = target.as_tx_participant();
self.tx_collection_id(participant.as_ref()).map_err(|_| {
DbError::Client("unique namespace target is not published on this Db")
})?;
lock(&self.tx_ranks).insert(target.collection_name(), 1);
}
Ok(())
}
pub(crate) fn tx_register_stream_targets(&self, targets: &crate::QueueTargets) -> DbResult<()> {
for participant in targets.tx_participants() {
self.tx_collection_id(participant.as_ref()).map_err(|_| {
DbError::Client("change stream queue target is not published on this Db")
})?;
lock(&self.tx_ranks).insert(participant.name(), 1);
}
Ok(())
}
fn check_stream_open<T, H>(&self, hook: &H) -> DbResult<()>
where
T: CollectionMeta,
H: TypedWriteHook<T::SelfId, T>,
{
if !H::EMITS {
return Ok(());
}
if T::TTL.is_some() {
return Err(DbError::Client("change stream source cannot use TTL"));
}
let targets = hook
.stream_targets()
.ok_or(DbError::Internal("stream carrier missing targets"))?;
targets.check_managed(self).map_err(|_| {
DbError::Client("change stream queue target is not published on this Db")
})?;
Ok(())
}
fn check_claim_owner_tags<T, H>(&self, hook: &H) -> DbResult<()>
where
T: CollectionMeta,
H: TypedWriteHook<T::SelfId, T>,
{
let Some(targets) = hook.claim_targets() else {
return Ok(());
};
let tag = crate::unique::collection_tag(T::NAME);
self.register_claim_owner_tag(tag, T::NAME, targets)
}
fn register_claim_owner_tag(
&self,
tag: u64,
name: &'static str,
targets: &UniqueTargets,
) -> DbResult<()> {
let mut map = lock(&self.claim_owner_tags);
for target in targets.all() {
let namespace = target.collection_name();
match map.entry((namespace, tag)) {
std::collections::hash_map::Entry::Occupied(e) => {
if *e.get() != name {
return Err(DbError::ClaimOwnerTagCollision {
namespace,
a: e.get(),
b: name,
});
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(name);
}
}
}
Ok(())
}
pub fn pending_transactions(&self) -> Vec<PendingTx> {
self.txlog
.pending()
.into_iter()
.map(|r| PendingTx {
tx_id: r.tx_id,
collections: r.ops.iter().map(|o| o.name.clone()).collect(),
})
.collect()
}
pub(crate) fn note_tx_replay_done(&self, name: &'static str) {
lock(&self.tx_replay_done).insert(name);
self.settle_pending_transactions();
}
fn settle_pending_transactions(&self) {
let done = lock(&self.tx_replay_done);
for record in self.txlog.pending() {
if !self.txlog.is_recovered(record.tx_id) {
continue;
}
let names: std::collections::HashSet<&str> =
record.ops.iter().map(|o| o.name.as_str()).collect();
if names.is_empty() {
continue;
}
if names.iter().all(|n| done.iter().any(|d| d == n)) {
self.txlog.mark_applied(record.tx_id);
}
}
}
pub(crate) fn checkpoint_now(&self) -> DbResult<()> {
let _ = crate::txn::checkpoint::run(self)?;
Ok(())
}
pub fn version(&self) -> u32 {
self.db_info.cloned().version
}
pub fn set_version(&self, f: impl FnOnce(u32) -> u32) -> DbResult<u32> {
let mut new_version = 0;
self.db_info.update(|info| {
new_version = f(info.version);
info.version = new_version;
})?;
Ok(new_version)
}
pub fn db_info(&self) -> DbInfo {
self.db_info.cloned()
}
#[doc(hidden)]
pub fn debug_counters_dirty(&self, name: &str) -> bool {
self.db_info
.cloned()
.collections
.get(name)
.is_some_and(|c| c.counters_dirty)
}
pub(crate) fn counter_dirty_weak(&self) -> Weak<Persist<DbInfo>> {
Arc::downgrade(&self.db_info_arc)
}
#[doc(hidden)]
pub fn db_info_update(&self, f: impl FnOnce(&mut DbInfo)) -> DbResult<()> {
self.db_info.update(f).map_err(Into::into)
}
pub fn metadata<M: DeserializeOwned>(&self) -> Option<M> {
let um = self.user_meta.cloned();
um.db.and_then(|v| serde_json::from_value(v).ok())
}
pub fn set_metadata<M: Serialize + DeserializeOwned>(
&self,
f: impl FnOnce(Option<M>) -> M,
) -> DbResult<M> {
let mut result = None;
let mut ser_err = None;
self.user_meta.update(|um| {
let current = um
.db
.as_ref()
.and_then(|v| serde_json::from_value(v.clone()).ok());
let new_val = f(current);
match serde_json::to_value(&new_val) {
Ok(v) => {
um.db = Some(v);
result = Some(new_val);
}
Err(e) => ser_err = Some(e),
}
})?;
if let Some(e) = ser_err {
return Err(DbError::FormatMismatch(e.to_string()));
}
Ok(result.expect("update executed"))
}
pub fn collection_metadata<M: DeserializeOwned>(&self, name: &str) -> Option<M> {
let um = self.user_meta.cloned();
um.collections
.get(name)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn set_collection_metadata<M: Serialize + DeserializeOwned>(
&self,
name: &str,
f: impl FnOnce(Option<M>) -> M,
) -> DbResult<M> {
let mut result = None;
let mut ser_err = None;
self.user_meta.update(|um| {
let current = um
.collections
.get(name)
.and_then(|v| serde_json::from_value(v.clone()).ok());
let new_val = f(current);
match serde_json::to_value(&new_val) {
Ok(v) => {
um.collections.insert(name.to_owned(), v);
result = Some(new_val);
}
Err(e) => ser_err = Some(e),
}
})?;
if let Some(e) = ser_err {
return Err(DbError::FormatMismatch(e.to_string()));
}
Ok(result.expect("update executed"))
}
pub fn tree_path(&self, name: &str) -> PathBuf {
self.path.join(name)
}
pub fn next_id(&self, name: &str) -> DbResult<u64> {
self.seq.next_id(name)
}
pub fn collection_len<T: CollectionMeta>(&self) -> u64 {
self.stored_info(T::NAME).map_or(0, |ci| ci.seq)
}
pub fn collection_names(&self) -> Vec<String> {
lock(&self.collections)
.iter()
.map(|c| c.name().to_string())
.collect()
}
pub fn close(&self) -> DbResult<()> {
self.disable_all_ttl_sweeps();
let mut first_err: Option<DbError> = None;
let collections = lock(&self.collections).clone();
for c in collections.iter() {
if let Err(e) = c.flush() {
tracing::warn!(collection = c.name(), error = %e, "close flush failed");
first_err.get_or_insert(e);
continue;
}
if let Err(e) = self.save_collection_len(c.name(), c.len() as u64) {
tracing::warn!(collection = c.name(), error = %e, "close save_collection_len failed");
first_err.get_or_insert(e);
}
}
if first_err.is_none()
&& let Err(e) = self.clear_consumed_counter_flags(&collections)
{
first_err.get_or_insert(e);
}
if let Err(e) = self.seq.flush() {
tracing::warn!(error = %e, "close seq flush failed");
first_err.get_or_insert(e);
}
#[cfg(feature = "fjall")]
if let Err(e) = self.db_info.update(|i| i.clean = true) {
tracing::warn!(error = %e, "close db_info clean flag failed");
first_err.get_or_insert(e.into());
}
let pending = self.pending_transactions();
if !pending.is_empty() {
tracing::warn!(
count = pending.len(),
"closing with pending transactions still in the log"
);
}
if let Err(e) = self.checkpoint_now() {
tracing::warn!(error = %e, "close checkpoint failed");
first_err.get_or_insert(e);
}
first_err.map_or(Ok(()), Err)
}
pub fn clean_shutdown(&self) -> DbResult<()> {
self.disable_all_ttl_sweeps();
let start = Instant::now();
let mut first_err: Option<DbError> = None;
let mut failed = 0usize;
let collections = lock(&self.collections).clone();
for c in collections.iter() {
if let Err(e) = c.clean_shutdown() {
tracing::warn!(collection = c.name(), error = %e, "clean_shutdown failed");
failed += 1;
first_err.get_or_insert(e);
continue;
}
if let Err(e) = self.save_collection_len(c.name(), c.len() as u64) {
tracing::warn!(collection = c.name(), error = %e, "save_collection_len failed");
failed += 1;
first_err.get_or_insert(e);
}
}
if first_err.is_none()
&& let Err(e) = self.clear_consumed_counter_flags(&collections)
{
failed += 1;
first_err.get_or_insert(e);
}
if let Err(e) = self.seq.flush() {
tracing::warn!(error = %e, "seq flush failed");
failed += 1;
first_err.get_or_insert(e);
}
#[cfg(feature = "fjall")]
if let Err(e) = self.db_info.update(|i| i.clean = true) {
tracing::warn!(error = %e, "clean_shutdown db_info clean flag failed");
failed += 1;
first_err.get_or_insert(e.into());
}
tracing::info!(
collections = collections.len(),
failed,
elapsed_ms = start.elapsed().as_millis() as u64,
"db clean shutdown"
);
first_err.map_or(Ok(()), Err)
}
pub fn shutdown(&mut self) -> DbResult<()> {
self.shutdown.shutdown();
#[cfg(all(feature = "typed-tree", feature = "armour"))]
self.queue_wake_service.join();
#[cfg(feature = "rpc")]
for h in lock(&self.rpc_handles).drain(..) {
drop(h); }
if let Some(ref mut c) = self.compactor {
c.stop();
}
if let Some(ref mut f) = self.flusher {
f.stop();
}
if let Some(ref mut s) = self.sweeper {
s.stop();
}
let close_result = self.close();
let collections = lock(&self.collections).clone();
for collection in collections {
collection.close_watches();
}
close_result
}
fn disable_all_ttl_sweeps(&self) {
let collections = lock(&self.collections).clone();
if collections.is_empty() {
return;
}
tracing::info!(
collections = collections.len(),
"ttl sweep disabled for finalization; re-enable per collection with \
Db::set_ttl_sweep(name, true) if this Db keeps running"
);
for c in collections.iter() {
if let Err(e) = c.set_ttl_sweep(false) {
tracing::warn!(
collection = c.name(),
error = %e,
"disable ttl sweep failed"
);
}
}
}
pub fn shutdown_signal(&self) -> ShutdownSignal {
self.shutdown.clone()
}
pub fn compact(&self) -> DbResult<usize> {
let snapshot = lock(&self.collections).clone();
let mut total = 0;
for c in &snapshot {
total += c.compact()?;
}
Ok(total)
}
#[cfg(all(feature = "typed-tree", feature = "armour"))]
pub(crate) fn register_queue_retention(&self, retention: Arc<dyn QueueRetentionPass>) {
lock(&self.queue_retentions).push(retention);
}
pub fn sweep_expired_now(&self) -> DbResult<usize> {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| DbError::Internal("system clock before unix epoch"))?
.as_millis() as u64;
let snapshot = lock(&self.collections).clone();
let mut total_deleted = 0;
let mut first_err = None;
for c in &snapshot {
match ttl_sweep_collection(c, now_ms, self.ttl_budget) {
Ok(out) => total_deleted += out.deleted,
Err(e) if first_err.is_none() => first_err = Some(e),
Err(_) => {}
}
}
#[cfg(all(feature = "typed-tree", feature = "armour"))]
for retention in &*lock(&self.queue_retentions) {
if let Err(e) = retention.run_retention()
&& first_err.is_none()
{
first_err = Some(e);
}
}
match first_err {
Some(e) => Err(e),
None => Ok(total_deleted),
}
}
pub fn set_ttl_sweep(&self, name: &str, enabled: bool) -> DbResult<()> {
self.collection_by_name(name)?.set_ttl_sweep(enabled)
}
pub fn rebuild_ttl_index(&self, name: &str) -> DbResult<()> {
self.collection_by_name(name)?.rebuild_ttl_index()
}
pub fn recount_counters(&self, collection: &str) -> DbResult<u64> {
let coll = self.collection_by_name(collection)?;
if coll.counter_target_names().is_empty() {
return Err(DbError::NotACounterSource {
collection: collection.to_string(),
});
}
coll.reconcile_counters()
}
fn collection_by_name(&self, name: &str) -> DbResult<Arc<dyn Collection>> {
let cols = lock(&self.collections);
cols.iter()
.find(|c| c.name() == name)
.cloned()
.ok_or(DbError::Client("unknown collection"))
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_typed_tree<T, C, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<TypedTree<T::SelfId, T, C, TtlHook<T, H>>>>
where
T: CollectionMeta + Clone + Send + Sync + 'static,
T::SelfId: Key + Ord + Send + Sync,
C: Codec<T> + Default + 'static,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| TypedTree::open_inner(self.tree_path(meta.name), config, C::default(), hook),
crate::access::CodecWire(C::default()),
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_typed_map<T, C, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<TypedMap<T::SelfId, T, C, TtlHook<T, H>>>>
where
T: CollectionMeta + Clone + Send + Sync + 'static,
T::SelfId: Key + Send + Sync + Hash + Eq,
C: Codec<T> + Default + 'static,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| TypedMap::open_inner(self.tree_path(meta.name), config, C::default(), hook),
crate::access::CodecWire(C::default()),
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[cfg(feature = "var-collections")]
pub fn open_var_typed_tree<T, C, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<crate::VarTypedTree<T::SelfId, T, C, TtlHook<T, H>>>>
where
T: CollectionMeta + Clone + Send + Sync + 'static,
T::SelfId: Key + Ord + Send + Sync,
C: Codec<T> + Clone + Default + 'static,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| {
crate::VarTypedTree::open_hooked_inner(
self.tree_path(meta.name),
config,
C::default(),
hook,
)
},
crate::access::CodecWire(C::default()),
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[cfg(feature = "var-collections")]
pub fn open_var_typed_map<T, C, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<crate::VarTypedMap<T::SelfId, T, C, TtlHook<T, H>>>>
where
T: CollectionMeta + Clone + Send + Sync + 'static,
T::SelfId: Key + Send + Sync + Hash + Eq,
C: Codec<T> + Clone + Default + 'static,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| {
crate::VarTypedMap::open_hooked_inner(
self.tree_path(meta.name),
config,
C::default(),
hook,
)
},
crate::access::CodecWire(C::default()),
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_zero_tree<T, const V: usize, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Bitcask>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Ord + Send + Sync,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| ZeroTree::open_hooked_inner(self.tree_path(meta.name), config, hook),
crate::access::ZeroWire::<V>,
migrations,
)
}
pub(crate) fn open_zero_tree_fixed_internal<T, const V: usize, H>(
&self,
config: FixedConfig,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
after_stage: impl FnOnce(
&Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>,
) -> DbResult<()>,
) -> DbResult<Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Ord + Send + Sync,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.open_zero_tree_fixed_with_access(
config,
hook,
migrations,
ManagedCollectionAccess::Internal,
after_stage,
)
}
fn open_zero_tree_fixed_with_access<T, const V: usize, H>(
&self,
config: FixedConfig,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
access: ManagedCollectionAccess,
after_stage: impl FnOnce(
&Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>,
) -> DbResult<()>,
) -> DbResult<Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Ord + Send + Sync,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed_with_access_after_stage(
|meta| ZeroTree::open_with_hook_inner(self.tree_path(meta.name), config, hook),
crate::access::ZeroWire::<V>,
migrations,
access,
after_stage,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_zero_tree_fixed<T, const V: usize, H>(
&self,
config: FixedConfig,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<ZeroTree<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Ord + Send + Sync,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.open_zero_tree_fixed_with_access(
config,
hook,
migrations,
ManagedCollectionAccess::Public,
|_| Ok(()),
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_zero_map<T, const V: usize, H>(
&self,
config: Config,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<ZeroMap<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Bitcask>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Send + Sync + Hash + Eq,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| ZeroMap::open_hooked_inner(self.tree_path(meta.name), config, hook),
crate::access::ZeroWire::<V>,
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_zero_map_fixed<T, const V: usize, H>(
&self,
config: FixedConfig,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<ZeroMap<T::SelfId, V, T, TtlHook<T, H>, crate::durability::Fixed>>>
where
T: CollectionMeta
+ Copy
+ zerocopy::IntoBytes
+ zerocopy::FromBytes
+ zerocopy::Immutable
+ Send
+ Sync
+ 'static,
T::SelfId: Key + Send + Sync + Hash + Eq,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
crate::unique::check_no_ttl_claims::<T, H>(&hook)?;
crate::unique::check_claim_key_width::<T, H>(&hook)?;
self.check_claim_owner_tags::<T, H>(&hook)?;
let hook = TtlHook::<T, H>::new(hook);
self.open_managed(
|meta| ZeroMap::open_with_hook_inner(self.tree_path(meta.name), config, hook),
crate::access::ZeroWire::<V>,
migrations,
)
}
#[doc = include_str!("../../docs/exclusive-access.md")]
pub fn open_unique_index<N: UniqueNamespace>(
&self,
config: FixedConfig,
) -> DbResult<Arc<UniqueIndex<N>>>
where
N::SelfId: Key + Send + Sync + Hash + Eq,
{
let inner =
self.open_zero_map_fixed::<N, { size_of::<UniqueRow>() }, NoHook>(config, NoHook, &[])?;
Ok(Arc::new(UniqueIndex::from_inner(inner)))
}
#[cfg(feature = "fjall")]
fn lsm_database(
&self,
db_config: Option<crate::lsm::LsmDbConfig>,
) -> DbResult<(fjall::Database, crate::lsm::PersistPolicy)> {
let mut guard = lock(&self.lsm_db);
if let Some((db, existing)) = guard.as_ref() {
if let Some(req) = db_config
&& *existing != req
{
return Err(DbError::Config(
"LsmDbConfig differs from the already-open shared fjall Database (first-open-wins)",
));
}
return Ok((db.clone(), existing.persist));
}
let cfg = db_config.unwrap_or_default();
let root = self.path.join("lsm");
let db = crate::lsm::map_fjall(
fjall::Database::builder(&root)
.cache_size(cfg.cache_size)
.open(),
)?;
*guard = Some((db.clone(), cfg));
Ok((db, cfg.persist))
}
#[cfg(feature = "fjall")]
pub fn open_lsm_tree<T, C, H>(
&self,
db_config: Option<crate::lsm::LsmDbConfig>,
config: crate::lsm::LsmConfig,
hook: H,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<crate::lsm::LsmTree<T::SelfId, T, C, H>>>
where
T: CollectionMeta + Clone + Send + Sync + 'static,
T::SelfId: Key + Ord + Send + Sync + 'static,
C: Codec<T> + Clone + Default + 'static,
H: TypedWriteHook<T::SelfId, T> + 'static,
{
self.check_stream_open::<T, H>(&hook)?;
if H::WATCHES {
return Err(DbError::WatchUnsupported);
}
self.open_managed(
|meta| {
let (fdb, persist) = self.lsm_database(db_config)?;
let initial_count = if self.opened_clean {
self.stored_info(meta.name).as_ref().map(|i| i.seq)
} else {
None
};
crate::lsm::LsmTree::<T::SelfId, T, C, H>::open_with_persist(
fdb,
config,
persist,
C::default(),
hook,
initial_count,
)
},
crate::access::CodecWire(C::default()),
migrations,
)
}
fn stored_info(&self, name: &str) -> Option<CollectionInfo> {
self.db_info.cloned().collections.get(name).copied()
}
#[cfg(all(test, feature = "fjall"))]
pub(crate) fn has_lsm_database_for_test(&self) -> bool {
lock(&self.lsm_db).is_some()
}
fn reserve_name(&self, name: &'static str) -> DbResult<NameReservation<'_>> {
if RESERVED_COLLECTION_NAMES.contains(&name) {
return Err(DbError::DuplicateCollection(name));
}
let mut reserved = lock(&self.reserved_names);
if lock(&self.collections).iter().any(|c| c.name() == name) {
return Err(DbError::DuplicateCollection(name));
}
if !reserved.insert(name) {
return Err(DbError::DuplicateCollection(name));
}
Ok(NameReservation { db: self, name })
}
fn open_managed<T, S, W>(
&self,
open: impl FnOnce(&TreeMeta) -> DbResult<S>,
wire: W,
migrations: &[TypedMigration<T::SelfId, T>],
) -> DbResult<Arc<S>>
where
T: CollectionMeta + Send + Sync + 'static,
S: crate::access::Access<Key = T::SelfId, Val = T>
+ crate::access::ManagedOpen<Key = T::SelfId, Val = T>
+ Collection
+ 'static,
W: crate::access::Wire<T>,
{
self.open_managed_with_access(open, wire, migrations, ManagedCollectionAccess::Public)
}
fn open_managed_with_access<T, S, W>(
&self,
open: impl FnOnce(&TreeMeta) -> DbResult<S>,
wire: W,
migrations: &[TypedMigration<T::SelfId, T>],
access: ManagedCollectionAccess,
) -> DbResult<Arc<S>>
where
T: CollectionMeta + Send + Sync + 'static,
S: crate::access::Access<Key = T::SelfId, Val = T>
+ crate::access::ManagedOpen<Key = T::SelfId, Val = T>
+ Collection
+ 'static,
W: crate::access::Wire<T>,
{
self.open_managed_with_access_after_stage(open, wire, migrations, access, |_| Ok(()))
}
fn open_managed_with_access_after_stage<T, S, W>(
&self,
open: impl FnOnce(&TreeMeta) -> DbResult<S>,
wire: W,
migrations: &[TypedMigration<T::SelfId, T>],
access: ManagedCollectionAccess,
after_stage: impl FnOnce(&Arc<S>) -> DbResult<()>,
) -> DbResult<Arc<S>>
where
T: CollectionMeta + Send + Sync + 'static,
S: crate::access::Access<Key = T::SelfId, Val = T>
+ crate::access::ManagedOpen<Key = T::SelfId, Val = T>
+ Collection
+ 'static,
W: crate::access::Wire<T>,
{
let meta = TreeMeta::of::<T>();
let staged = self.stage_managed(open, wire, migrations, self.reserve_name(meta.name)?)?;
after_stage(&staged.collection)?;
let collection = Arc::clone(&staged.collection);
self.publish_staged_adapters(vec![self.stage_to_adapter(staged, access)])?;
Ok(collection)
}
#[allow(clippy::too_many_arguments)] pub(crate) fn open_managed_pair<T1, S1, W1, T2, S2, W2>(
&self,
open_first: impl FnOnce(&TreeMeta) -> DbResult<S1>,
wire_first: W1,
migrations_first: &[TypedMigration<T1::SelfId, T1>],
open_second: impl FnOnce(&TreeMeta) -> DbResult<S2>,
wire_second: W2,
migrations_second: &[TypedMigration<T2::SelfId, T2>],
after_stage: impl FnOnce(&Arc<S1>, &Arc<S2>) -> DbResult<()>,
first_access: ManagedCollectionAccess,
second_access: ManagedCollectionAccess,
) -> DbResult<(Arc<S1>, Arc<S2>)>
where
T1: CollectionMeta + Send + Sync + 'static,
S1: crate::access::Access<Key = T1::SelfId, Val = T1>
+ crate::access::ManagedOpen<Key = T1::SelfId, Val = T1>
+ Collection
+ 'static,
W1: crate::access::Wire<T1>,
T2: CollectionMeta + Send + Sync + 'static,
S2: crate::access::Access<Key = T2::SelfId, Val = T2>
+ crate::access::ManagedOpen<Key = T2::SelfId, Val = T2>
+ Collection
+ 'static,
W2: crate::access::Wire<T2>,
{
let first_meta = TreeMeta::of::<T1>();
let second_meta = TreeMeta::of::<T2>();
let first_guard = self.reserve_name(first_meta.name)?;
let second_guard = self.reserve_name(second_meta.name)?;
let first = self.stage_managed(open_first, wire_first, migrations_first, first_guard)?;
let second =
self.stage_managed(open_second, wire_second, migrations_second, second_guard)?;
after_stage(&first.collection, &second.collection)?;
let collections = (
Arc::clone(&first.collection),
Arc::clone(&second.collection),
);
self.publish_staged_adapters(vec![
self.stage_to_adapter(first, first_access),
self.stage_to_adapter(second, second_access),
])?;
Ok(collections)
}
fn stage_managed<'a, T, S, W>(
&self,
open: impl FnOnce(&TreeMeta) -> DbResult<S>,
wire: W,
migrations: &[TypedMigration<T::SelfId, T>],
name_guard: NameReservation<'a>,
) -> DbResult<StagedManaged<'a, S, W>>
where
T: CollectionMeta + Send + Sync + 'static,
S: crate::access::Access<Key = T::SelfId, Val = T>
+ crate::access::ManagedOpen<Key = T::SelfId, Val = T>
+ Collection
+ 'static,
W: crate::access::Wire<T>,
{
let meta = TreeMeta::of::<T>();
let stored = self.stored_info(meta.name);
let coll = open(&meta)?;
let coll = Arc::new(coll);
S::bind_tx_arc(&coll);
let replayed =
coll.replay_pending_tx_log(self, stored.as_ref().map(|info| info.typ_hash))?;
if replayed > 0 {
tracing::info!(
collection = meta.name,
replayed,
"replayed pending transaction operations"
);
}
let mut projections_dirty = stored.as_ref().is_some_and(|info| info.projections_dirty);
if Self::migration_will_run(stored.as_ref(), &meta) && coll.projections_enabled() {
projections_dirty = true;
self.set_projections_dirty(meta.name, true)?;
}
self.run_migration(
&meta,
stored.as_ref(),
migrations,
|mfn| coll.migrate_inner(mfn, false),
|| coll.flush_for_migration(),
)?;
let skip_reconcile = {
#[cfg(any(test, feature = "test-faults"))]
{
crate::txn::SKIP_PROJECTION_RECONCILE.load(std::sync::atomic::Ordering::SeqCst)
}
#[cfg(not(any(test, feature = "test-faults")))]
{
false
}
};
if projections_dirty && !skip_reconcile {
coll.reconcile_projections()?;
projections_dirty = false;
}
coll.replay_init();
coll.build_ttl_index()?;
let target_names = crate::access::ManagedOpen::counter_target_names(&*coll);
let mut counters_dirty = self
.stored_info(meta.name)
.is_some_and(|i| i.counters_dirty);
if !target_names.is_empty() {
let dirty_targets: Vec<&'static str> = target_names
.iter()
.copied()
.filter(|n| self.stored_info(n).is_some_and(|i| i.counters_dirty))
.collect();
if counters_dirty || !dirty_targets.is_empty() {
let repaired = crate::access::ManagedOpen::reconcile_counters(&*coll)?;
tracing::info!(collection = meta.name, repaired, "counters recounted");
self.clear_counters_dirty(meta.name)?;
counters_dirty = false;
lock(&self.counter_targets_consumed).extend(dirty_targets);
}
}
self.save_info(&meta, coll.len() as u64, projections_dirty, counters_dirty)?;
coll.bind_counter_dirty(self, meta.name);
if !target_names.is_empty() {
crate::access::ManagedOpen::mark_counter_dirty(&*coll)?;
}
coll.register_tx_metadata(self)?;
Ok(StagedManaged {
meta,
collection: coll,
wire,
_name_guard: name_guard,
})
}
fn run_migration<K, T>(
&self,
meta: &TreeMeta,
stored: Option<&CollectionInfo>,
migrations: &[TypedMigration<K, T>],
mut migrate_fn: impl FnMut(&super::migration::TypedMigrationFn<K, T>) -> DbResult<usize>,
flush_fn: impl Fn() -> DbResult<()>,
) -> DbResult<bool> {
let Some(stored) = stored else {
return Ok(false);
};
if stored.version == meta.version && stored.typ_hash != meta.ty.h() {
if stored.typ_hash == TYP_HASH_MIGRATING {
tracing::warn!(
name = meta.name,
version = stored.version,
"typ_hash is the migration sentinel — skipping V-005 (interrupted multi-step migration)"
);
} else {
return Err(DbError::SchemaMismatch {
name: meta.name.into(),
kind: SchemaMismatchKind::TypHash {
stored: stored.typ_hash,
expected: meta.ty.h(),
},
});
}
}
if stored.version > meta.version {
return Err(DbError::SchemaMismatch {
name: meta.name.into(),
kind: SchemaMismatchKind::Downgrade {
stored: stored.version,
requested: meta.version,
},
});
}
if stored.version == meta.version {
return Ok(false);
}
let mut current = stored.version;
let mut mutated_any = false;
while current < meta.version {
let (_, mfn) = migrations
.iter()
.find(|(v, _)| *v == current)
.ok_or_else(|| DbError::SchemaMismatch {
name: meta.name.into(),
kind: SchemaMismatchKind::MissingStep { from: current },
})?;
let _stream_apply_guard = crate::txn_depth::StreamApplyGuard::enter();
let mutated = migrate_fn(mfn)?;
flush_fn()?;
let step_hash = if current + 1 == meta.version {
meta.ty.h()
} else {
TYP_HASH_MIGRATING
};
self.update_schema_version(meta.name, current + 1, step_hash)?;
tracing::info!(mutated, from = current, to = current + 1, "migration step");
current += 1;
mutated_any = true;
}
if mutated_any {
self.set_counters_dirty(meta.name, true)?;
}
Ok(mutated_any)
}
fn update_schema_version(&self, name: &str, version: u16, typ_hash: u64) -> DbResult<()> {
self.db_info.update(|info| {
let entry = info.collections.entry(name.to_owned()).or_default();
entry.version = version;
entry.typ_hash = typ_hash;
})?;
Ok(())
}
fn save_info(
&self,
meta: &TreeMeta,
seq: u64,
projections_dirty: bool,
counters_dirty: bool,
) -> DbResult<()> {
let typ_hash = meta.ty.h();
self.db_info.update(|info| {
info.collections.insert(
meta.name.to_owned(),
CollectionInfo {
version: meta.version,
typ_hash,
seq,
projections_dirty,
counters_dirty,
},
);
})?;
Ok(())
}
fn set_projections_dirty(&self, name: &str, dirty: bool) -> DbResult<()> {
self.db_info.update(|info| {
let entry = info.collections.entry(name.to_owned()).or_default();
entry.projections_dirty = dirty;
})?;
Ok(())
}
fn clear_consumed_counter_flags(&self, collections: &[Arc<dyn Collection>]) -> DbResult<()> {
let mut first_err: Option<DbError> = None;
let consumed = std::mem::take(&mut *lock(&self.counter_targets_consumed));
for c in collections.iter() {
if !c.reset_counter_dirty_marker() {
continue;
}
if let Err(e) = self.clear_counters_dirty(c.name()) {
tracing::warn!(collection = c.name(), error = %e, "clear counters_dirty failed");
first_err.get_or_insert(e);
}
}
for name in consumed {
if let Err(e) = self.clear_counters_dirty(name) {
tracing::warn!(collection = name, error = %e, "clear consumed target counters_dirty failed");
first_err.get_or_insert(e);
}
}
first_err.map_or(Ok(()), Err)
}
fn clear_counters_dirty(&self, name: &str) -> DbResult<()> {
self.db_info.update(|info| {
let entry = info.collections.entry(name.to_owned()).or_default();
entry.counters_dirty = false;
})?;
Ok(())
}
fn set_counters_dirty(&self, name: &str, dirty: bool) -> DbResult<()> {
self.db_info.update(|info| {
let entry = info.collections.entry(name.to_owned()).or_default();
entry.counters_dirty = dirty;
})?;
Ok(())
}
fn migration_will_run(stored: Option<&CollectionInfo>, meta: &TreeMeta) -> bool {
let Some(stored) = stored else {
return false;
};
stored.version < meta.version
}
fn save_collection_len(&self, name: &str, len: u64) -> DbResult<()> {
self.db_info.update(|info| {
if let Some(ci) = info.collections.get_mut(name) {
ci.seq = len;
}
})?;
Ok(())
}
pub fn inspector(&self) -> super::inspection::Inspector<'_> {
super::inspection::Inspector { db: self }
}
#[cfg(test)]
fn publish_collection(
&self,
name: &'static str,
collection: Arc<dyn Collection>,
inspection: Arc<dyn InspectionHandler>,
#[cfg(feature = "rpc")] rpc: Option<(u64, Arc<dyn super::handler::RpcHandler>)>,
) -> DbResult<()> {
let mut collections = lock(&self.collections);
let mut inspections = lock(&self.inspection_handlers);
#[cfg(feature = "rpc")]
let mut handlers = lock(&self.handlers);
if collections.iter().any(|c| c.name() == name) {
return Err(DbError::DuplicateCollection(name));
}
if inspections.contains_key(name) {
return Err(DbError::DuplicateCollection(name));
}
#[cfg(feature = "rpc")]
if let Some((hash, ref handler)) = rpc
&& let Some((_, existing)) = handlers.iter().find(|(k, _)| *k == hash)
{
tracing::error!(
collection = name,
conflicting = handler.name(),
existing = existing.name(),
hash,
"xxh3-64 collection name collision — refusing to register a second \
handler under the same hash"
);
return Err(DbError::DuplicateCollection(
"rpc handler hash collision (see log for the two collection names)",
));
}
collections.push(collection);
inspections.insert(name, inspection);
#[cfg(feature = "rpc")]
if let Some((hash, handler)) = rpc {
handlers.push((hash, handler));
}
Ok(())
}
#[cfg(test)]
pub(crate) fn publish_collection_for_test(
&self,
name: &'static str,
collection: Arc<dyn Collection>,
inspection: Arc<dyn InspectionHandler>,
#[cfg(feature = "rpc")] rpc: Option<Arc<dyn super::handler::RpcHandler>>,
forced_hash: Option<u64>,
) -> DbResult<()> {
#[cfg(feature = "rpc")]
let rpc = rpc.map(|handler| {
let hash = forced_hash.unwrap_or_else(|| xxhash_rust::xxh3::xxh3_64(name.as_bytes()));
(hash, handler)
});
#[cfg(not(feature = "rpc"))]
let _ = forced_hash;
self.publish_collection(
name,
collection,
inspection,
#[cfg(feature = "rpc")]
rpc,
)
}
fn stage_to_adapter<'a, S, W>(
&self,
staged: StagedManaged<'a, S, W>,
access: ManagedCollectionAccess,
) -> StagedAdapter<'a>
where
S: crate::access::Access + Collection + 'static,
W: crate::access::Wire<S::Val>,
{
let StagedManaged {
meta,
collection,
wire,
_name_guard,
} = staged;
let adapter = Arc::new(crate::access::Adapter::new(
&meta,
collection.clone(),
wire,
self.seq.clone(),
));
let inspection: Arc<dyn InspectionHandler> = adapter.clone();
let collection: Arc<dyn Collection> = collection;
#[cfg(feature = "rpc")]
let rpc = (access == ManagedCollectionAccess::Public).then(|| {
(
xxhash_rust::xxh3::xxh3_64(meta.name.as_bytes()),
adapter as Arc<dyn super::handler::RpcHandler>,
)
});
StagedAdapter {
name: meta.name,
collection,
inspection,
#[cfg(feature = "rpc")]
rpc,
_name_guard,
}
}
fn publish_staged_adapters(&self, staged: Vec<StagedAdapter<'_>>) -> DbResult<()> {
let mut collections = lock(&self.collections);
let mut inspections = lock(&self.inspection_handlers);
#[cfg(feature = "rpc")]
let mut handlers = lock(&self.handlers);
for adapter in &staged {
if collections.iter().any(|c| c.name() == adapter.name)
|| inspections.contains_key(adapter.name)
|| staged
.iter()
.filter(|other| other.name == adapter.name)
.count()
!= 1
{
return Err(DbError::DuplicateCollection(adapter.name));
}
#[cfg(feature = "rpc")]
if let Some((hash, ref handler)) = adapter.rpc
&& (handlers.iter().any(|(existing, _)| *existing == hash)
|| staged
.iter()
.filter(|other| {
other
.rpc
.as_ref()
.is_some_and(|(other_hash, _)| *other_hash == hash)
})
.count()
!= 1)
{
tracing::error!(
collection = adapter.name,
conflicting = handler.name(),
hash,
"RPC handler collision while publishing managed collection group"
);
return Err(DbError::DuplicateCollection(
"rpc handler hash collision (see log for the two collection names)",
));
}
}
let names: Vec<_> = staged.iter().map(|adapter| adapter.name).collect();
for adapter in staged {
collections.push(adapter.collection);
inspections.insert(adapter.name, adapter.inspection);
#[cfg(feature = "rpc")]
if let Some((hash, handler)) = adapter.rpc {
handlers.push((hash, handler));
}
}
#[cfg(feature = "rpc")]
drop(handlers);
drop(inspections);
drop(collections);
for name in names {
self.note_tx_replay_done(name);
}
Ok(())
}
}
pub struct DbBuilder {
path: std::path::PathBuf,
workers: WorkerIntervals,
sync_persist: bool,
tx_config: crate::txn::TxConfig,
}
impl DbBuilder {
#[must_use]
pub fn workers(mut self, workers: WorkerIntervals) -> Self {
self.workers = workers;
self
}
#[must_use]
pub fn tx_config(mut self, tx_config: crate::txn::TxConfig) -> Self {
self.tx_config = tx_config;
self
}
#[must_use]
pub fn for_tests(mut self) -> Self {
self.workers = WorkerIntervals {
compaction: None,
flush: None,
ttl_sweep: None,
ttl_budget: DEFAULT_TTL_BUDGET,
};
self.sync_persist = false;
self
}
#[doc = include_str!("../../docs/exclusive-access.md")]
#[doc = include_str!("../../docs/exclusive-access-root.md")]
pub fn open(self) -> DbResult<Db> {
Db::open_with_options(self.path, self.workers, self.sync_persist, self.tx_config)
}
}
#[cfg(feature = "rpc")]
impl Db {
pub fn rpc_handler_for(&self, name: &str) -> Option<Arc<dyn super::handler::RpcHandler>> {
let h = xxhash_rust::xxh3::xxh3_64(name.as_bytes());
lock(&self.handlers)
.iter()
.find(|(k, _)| *k == h)
.map(|(_, v)| v.clone())
}
pub fn build_tree_map(&self) -> super::rpc::TreeMap {
Arc::new(
lock(&self.handlers)
.iter()
.map(|(h, handler)| (*h, handler.clone()))
.collect(),
)
}
}
impl Drop for Db {
fn drop(&mut self) {
let _ = self.shutdown();
}
}
#[cfg(all(test, feature = "rapira-codec"))]
mod tests {
use super::*;
use armour_core::GetType;
use rapira::Rapira;
use tempfile::tempdir;
use crate::armour::migration::TypedMigration;
use crate::{MigrateAction, NoHook, RapiraCodec};
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct User {
id: u64,
name: String,
}
impl CollectionMeta for User {
type SelfId = [u8; 8];
const NAME: &'static str = "iterable_toggle_users";
const VERSION: u16 = 1;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct StagedFirst;
impl CollectionMeta for StagedFirst {
type SelfId = [u8; 8];
const NAME: &'static str = "staged_pair_first";
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct StagedSecond;
impl CollectionMeta for StagedSecond {
type SelfId = [u8; 8];
const NAME: &'static str = "staged_pair_second";
}
fn key(id: u64) -> [u8; 8] {
id.to_be_bytes()
}
fn demo_user() -> User {
User {
id: 1,
name: "alice".into(),
}
}
#[test]
fn open_same_collection_twice_errors() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let _first = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), NoHook, &[])
.expect("first open succeeds");
let err = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), NoHook, &[])
.err()
.expect("second open of the same name must fail");
assert!(
matches!(err, DbError::DuplicateCollection("iterable_toggle_users")),
"got {err:?}"
);
}
#[test]
fn failed_after_stage_callback_leaves_pair_unpublished_and_retryable() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let result = db.open_managed_pair(
|meta| {
TypedMap::<[u8; 8], StagedFirst, RapiraCodec, TtlHook<StagedFirst, NoHook>>::open_inner(
db.tree_path(meta.name), Config::test(), RapiraCodec, TtlHook::new(NoHook),
)
},
crate::access::CodecWire(RapiraCodec),
&[],
|meta| {
TypedMap::<[u8; 8], StagedSecond, RapiraCodec, TtlHook<StagedSecond, NoHook>>::open_inner(
db.tree_path(meta.name), Config::test(), RapiraCodec, TtlHook::new(NoHook),
)
},
crate::access::CodecWire(RapiraCodec),
&[],
|_, _| Err(DbError::Client("staged cleanup failed")),
ManagedCollectionAccess::Public,
ManagedCollectionAccess::Public,
);
assert!(matches!(
result,
Err(DbError::Client("staged cleanup failed"))
));
assert!(lock(&db.collections).is_empty());
db.open_typed_map::<StagedFirst, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
db.open_typed_map::<StagedSecond, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
zerocopy::KnownLayout,
)]
#[repr(C)]
struct OverCapKey([u8; crate::unique::UNIQUE_OWNER_KEY_CAP + 8]);
crate::impl_key_zerocopy!(
OverCapKey,
armour_core::KeyScheme::Typed(&[armour_core::KeyType::Array(
crate::unique::UNIQUE_OWNER_KEY_CAP + 8
)])
);
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct WideKeyUser {
name: String,
}
impl CollectionMeta for WideKeyUser {
type SelfId = OverCapKey;
const NAME: &'static str = "f3_wide_key_users";
}
#[test]
fn open_managed_rejects_a_hand_assembled_carrier_whose_key_is_too_wide() {
let (_dir, db, ns) = crate::unique_tests::open_claim_ns();
let hook = crate::projection::Projected::new(crate::IndexTargets::new(), |_, _, _| Ok(()))
.and_unique(
crate::UniqueTargets::new().with(&ns),
move |_k, _v, sink| sink.claim(&ns, crate::unique_tests::ClaimFwdKey::new(1)),
);
let result = db.open_typed_map::<WideKeyUser, RapiraCodec, _>(Config::test(), hook, &[]);
let Err(err) = result else {
panic!("a claim carrier whose source key exceeds the owner capacity must be rejected");
};
assert!(
matches!(
err,
DbError::ClaimingSourceKeyTooWide {
collection: "f3_wide_key_users"
}
),
"expected ClaimingSourceKeyTooWide, got {err:?}"
);
}
#[test]
fn claim_owner_tag_collision_between_two_sources_is_rejected() {
let (_dir, db, ns) = crate::unique_tests::open_claim_ns();
let targets = crate::UniqueTargets::new().with(&ns);
db.register_claim_owner_tag(42, "src_a", &targets)
.expect("first source registers cleanly");
db.register_claim_owner_tag(42, "src_a", &targets)
.expect("re-registering the same source is not a collision");
let err = db
.register_claim_owner_tag(42, "src_b", &targets)
.expect_err("a different source under the same tag must collide");
assert!(
matches!(
err,
DbError::ClaimOwnerTagCollision {
namespace: "claim_forwarding_test_ns",
a: "src_a",
b: "src_b",
}
),
"expected ClaimOwnerTagCollision, got {err:?}"
);
}
#[test]
fn concurrent_open_same_name_exactly_one_wins() {
use std::sync::{Arc, Barrier};
let dir = tempdir().unwrap();
let db = Arc::new(Db::open_test(dir.path()).unwrap());
const N: usize = 8;
let barrier = Arc::new(Barrier::new(N));
let handles: Vec<_> = (0..N)
.map(|_| {
let db = Arc::clone(&db);
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
db.open_typed_map::<User, RapiraCodec, _>(Config::test(), NoHook, &[])
.is_ok()
})
})
.collect();
let wins = handles
.into_iter()
.map(|h| h.join().unwrap())
.filter(|&ok| ok)
.count();
assert_eq!(
wins, 1,
"expected exactly one concurrent open to win, got {wins}"
);
}
#[test]
fn open_reserved_name_errors() {
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct SeqNamed {
x: u64,
}
impl CollectionMeta for SeqNamed {
type SelfId = [u8; 8];
const NAME: &'static str = "__seq";
const VERSION: u16 = 1;
}
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let err = db
.open_typed_map::<SeqNamed, RapiraCodec, _>(Config::test(), NoHook, &[])
.err()
.expect("reserved name must be rejected");
assert!(
matches!(err, DbError::DuplicateCollection("__seq")),
"got {err:?}"
);
}
#[test]
fn set_metadata_serialize_failure_returns_err_not_panic() {
use serde::de::{Deserialize, Deserializer};
use serde::ser::{Error as _, Serialize, Serializer};
#[derive(Debug)]
struct FailSer;
impl Serialize for FailSer {
fn serialize<S: Serializer>(&self, _s: S) -> std::result::Result<S::Ok, S::Error> {
Err(S::Error::custom("intentional serialize failure"))
}
}
impl<'de> Deserialize<'de> for FailSer {
fn deserialize<D: Deserializer<'de>>(_d: D) -> std::result::Result<Self, D::Error> {
Ok(FailSer)
}
}
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let res = db.set_metadata::<FailSer>(|_current| FailSer);
assert!(
matches!(res, Err(DbError::FormatMismatch(_))),
"expected FormatMismatch, got {res:?}"
);
}
#[test]
fn db_map_iterable_toggles_across_reopen() {
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let users = db
.open_typed_map::<User, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
assert!(users.iter_view().is_none());
users.put(&key(1), demo_user()).unwrap();
db.close().unwrap();
}
let db = Db::open_test(dir.path()).unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.build();
let users = db
.open_typed_map::<User, RapiraCodec, _>(cfg, NoHook, &[])
.unwrap();
assert!(users.iter_view().is_some());
assert_eq!(users.iter_view().unwrap().iter().count(), 1);
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
)]
#[repr(C)]
struct FxCounter {
n: u32,
}
impl CollectionMeta for FxCounter {
type SelfId = [u8; 8];
const NAME: &'static str = "fixed_reversed_db";
const VERSION: u16 = 1;
}
impl CollectionMeta for FxCounterMap {
type SelfId = [u8; 8];
const NAME: &'static str = "fixed_reversed_db_map";
const VERSION: u16 = 1;
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
)]
#[repr(C)]
struct FxCounterMap {
n: u32,
}
#[test]
fn db_fixed_openers_honor_reversed() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_zero_tree_fixed::<FxCounter, 4, _>(FixedConfig::test(), NoHook, &[])
.unwrap();
for i in 1u64..=3 {
tree.put(&i.to_be_bytes(), &FxCounter { n: i as u32 })
.unwrap();
}
let tk: Vec<u64> = tree.iter().map(|(k, _)| u64::from_be_bytes(k)).collect();
assert_eq!(tk, [3, 2, 1], "Db fixed tree default DESC");
let map_cfg = FixedConfig {
iterable: true,
reversed: false,
..FixedConfig::test()
};
let map = db
.open_zero_map_fixed::<FxCounterMap, 4, _>(map_cfg, NoHook, &[])
.unwrap();
for i in 1u64..=3 {
map.put(&i.to_be_bytes(), &FxCounterMap { n: i as u32 })
.unwrap();
}
let mk: Vec<u64> = map
.iter_view()
.unwrap()
.keys()
.map(u64::from_be_bytes)
.collect();
assert_eq!(mk, [1, 2, 3], "Db fixed map reversed=false ASC");
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct FlushMigItem {
value: i64,
}
impl CollectionMeta for FlushMigItem {
type SelfId = [u8; 8];
const NAME: &'static str = "flush_mig_items";
const VERSION: u16 = 2;
}
fn keep_flush(_k: &[u8; 8], _v: &FlushMigItem) -> MigrateAction<FlushMigItem> {
MigrateAction::Keep
}
#[test]
fn migration_flush_fn_reaches_engine_durable_flush() {
use std::sync::atomic::Ordering;
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_typed_tree::<FlushMigItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
tree.put(&1u64.to_be_bytes(), FlushMigItem { value: 7 })
.unwrap();
db.db_info
.update(|info| {
info.collections
.get_mut(FlushMigItem::NAME)
.unwrap()
.version = 1;
})
.unwrap();
}
let before = crate::engine::ENGINE_DURABLE_FLUSHES.load(Ordering::Relaxed);
{
let db = Db::open_test(dir.path()).unwrap();
let migrations: &[TypedMigration<[u8; 8], FlushMigItem>] = &[(1u16, keep_flush)];
let _tree = db
.open_typed_tree::<FlushMigItem, RapiraCodec, _>(Config::test(), NoHook, migrations)
.unwrap();
}
let after = crate::engine::ENGINE_DURABLE_FLUSHES.load(Ordering::Relaxed);
assert!(
after > before,
"migration flush_fn must hit Engine::flush (fsync)"
);
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
GetType,
zerocopy::FromBytes,
zerocopy::IntoBytes,
zerocopy::Immutable,
)]
#[repr(C)]
struct FlushMigZero {
n: u32,
}
impl CollectionMeta for FlushMigZero {
type SelfId = [u8; 8];
const NAME: &'static str = "flush_mig_zero";
const VERSION: u16 = 2;
}
fn keep_flush_zero(_k: &[u8; 8], _v: &FlushMigZero) -> MigrateAction<FlushMigZero> {
MigrateAction::Keep
}
#[test]
fn zero_opener_migration_flush_fn_reaches_engine_durable_flush() {
use std::sync::atomic::Ordering;
const V: usize = std::mem::size_of::<FlushMigZero>();
let dir = tempdir().unwrap();
{
let db = Db::open_test(dir.path()).unwrap();
let tree = db
.open_zero_tree::<FlushMigZero, V, _>(Config::test(), NoHook, &[])
.unwrap();
tree.put(&1u64.to_be_bytes(), &FlushMigZero { n: 7 })
.unwrap();
db.db_info
.update(|info| {
info.collections
.get_mut(FlushMigZero::NAME)
.unwrap()
.version = 1;
})
.unwrap();
}
let before = crate::engine::ENGINE_DURABLE_FLUSHES.load(Ordering::Relaxed);
{
let db = Db::open_test(dir.path()).unwrap();
let migrations: &[TypedMigration<[u8; 8], FlushMigZero>] = &[(1u16, keep_flush_zero)];
let _tree = db
.open_zero_tree::<FlushMigZero, V, _>(Config::test(), NoHook, migrations)
.unwrap();
}
let after = crate::engine::ENGINE_DURABLE_FLUSHES.load(Ordering::Relaxed);
assert!(
after > before,
"zero opener flush_fn must resolve to the durable inherent ZeroTree::flush (Engine::flush fsync)"
);
}
#[test]
fn run_migration_final_step_writes_real_typ_hash() {
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct TwoStep {
value: i64,
}
impl CollectionMeta for TwoStep {
type SelfId = [u8; 8];
const NAME: &'static str = "run_mig_two_step";
const VERSION: u16 = 3;
}
fn keep(_k: &[u8; 8], _v: &TwoStep) -> MigrateAction<TwoStep> {
MigrateAction::Keep
}
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let meta = TreeMeta::of::<TwoStep>();
let stored = CollectionInfo {
version: 1,
typ_hash: meta.ty.h(),
seq: 0,
projections_dirty: false,
counters_dirty: false,
};
let migrations: &[TypedMigration<[u8; 8], TwoStep>] = &[(1, keep), (2, keep)];
let mut seen = Vec::new();
let mutated = db
.run_migration(
&meta,
Some(&stored),
migrations,
|_mfn| {
if let Some(ci) = db.db_info.cloned().collections.get(TwoStep::NAME) {
seen.push(ci.typ_hash);
}
Ok(1)
},
|| Ok(()),
)
.unwrap();
assert!(mutated);
assert_eq!(seen, vec![TYP_HASH_MIGRATING]);
let info = db.db_info.cloned();
let ci = info.collections.get(TwoStep::NAME).unwrap();
assert_eq!(ci.version, 3);
assert_eq!(
ci.typ_hash,
meta.ty.h(),
"финальный шаг обязан записать реальный typ_hash, не sentinel"
);
assert_ne!(ci.typ_hash, TYP_HASH_MIGRATING);
}
#[test]
fn db_open_twice_same_path_errors() {
let dir = tempdir().unwrap();
let _first = Db::open_test(dir.path()).expect("first open");
let err = Db::open_test(dir.path())
.err()
.expect("second open of a live root must fail");
assert!(matches!(err, DbError::Locked { .. }), "got {err:?}");
}
#[test]
fn db_lock_released_on_drop() {
let dir = tempdir().unwrap();
let first = Db::open_test(dir.path()).expect("first open");
drop(first);
Db::open_test(dir.path()).expect("reopen after drop must succeed");
}
#[test]
fn db_lock_released_after_background_workers() {
let dir = tempdir().unwrap();
let db = Db::open_with_intervals(
dir.path(),
Some(std::time::Duration::from_millis(10)),
Some(std::time::Duration::from_millis(10)),
)
.expect("open with background workers");
drop(db);
Db::open_test(dir.path()).expect("reopen immediately after workers stop");
}
#[test]
fn db_lock_collection_name_reserved() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let err = db
.reserve_name("db.lock")
.err()
.expect("db.lock must be reserved: tree_path would alias the root lock file");
assert!(
matches!(err, DbError::DuplicateCollection("db.lock")),
"got {err:?}"
);
}
}
#[cfg(test)]
mod clean_shutdown_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct MockCollection {
name: &'static str,
fail: bool,
calls: Arc<AtomicUsize>,
}
impl Collection for MockCollection {
fn name(&self) -> &str {
self.name
}
fn len(&self) -> usize {
0
}
fn compact(&self) -> DbResult<usize> {
Ok(0)
}
fn flush(&self) -> DbResult<()> {
Ok(())
}
fn periodic_flush(&self) -> DbResult<()> {
Ok(())
}
fn durable_flush(&self) -> DbResult<()> {
Ok(())
}
fn clean_shutdown(&self) -> DbResult<()> {
self.calls.fetch_add(1, Ordering::SeqCst);
if self.fail {
Err(DbError::Io(std::io::Error::other(
"mock clean_shutdown failure",
)))
} else {
Ok(())
}
}
}
#[test]
fn test_db_clean_shutdown_attempt_all() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let calls2 = Arc::clone(&calls);
lock(&db.collections).push(Arc::new(MockCollection {
name: "fail_first",
fail: true,
calls,
}));
lock(&db.collections).push(Arc::new(MockCollection {
name: "ok_second",
fail: false,
calls: calls2.clone(),
}));
let err = db.clean_shutdown().unwrap_err();
assert!(
matches!(err, DbError::Io(_)),
"expected first collection error, got {err:?}"
);
assert_eq!(
calls2.load(Ordering::SeqCst),
2,
"both collections must be attempted"
);
}
}
#[cfg(test)]
mod close_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct FlushMock {
name: &'static str,
flush_fail: bool,
flush_calls: Arc<AtomicUsize>,
}
impl Collection for FlushMock {
fn name(&self) -> &str {
self.name
}
fn len(&self) -> usize {
0
}
fn compact(&self) -> DbResult<usize> {
Ok(0)
}
fn flush(&self) -> DbResult<()> {
self.flush_calls.fetch_add(1, Ordering::SeqCst);
if self.flush_fail {
Err(DbError::Io(std::io::Error::other("mock flush failure")))
} else {
Ok(())
}
}
fn periodic_flush(&self) -> DbResult<()> {
Ok(())
}
fn durable_flush(&self) -> DbResult<()> {
Ok(())
}
fn clean_shutdown(&self) -> DbResult<()> {
Ok(())
}
}
#[test]
fn test_db_close_flushes_all_despite_first_failure() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let calls = Arc::new(AtomicUsize::new(0));
lock(&db.collections).push(Arc::new(FlushMock {
name: "fail_first",
flush_fail: true,
flush_calls: calls.clone(),
}));
lock(&db.collections).push(Arc::new(FlushMock {
name: "ok_second",
flush_fail: false,
flush_calls: calls.clone(),
}));
let err = db.close().unwrap_err();
assert!(
matches!(err, DbError::Io(_)),
"expected the first flush error to propagate, got {err:?}"
);
assert_eq!(
calls.load(Ordering::SeqCst),
2,
"both collections must be flushed even though the first failed"
);
}
}
#[cfg(all(test, feature = "rapira-codec"))]
mod inspection_registry_tests {
use super::*;
use armour_core::GetType;
use rapira::Rapira;
use tempfile::tempdir;
use crate::{NoHook, RapiraCodec, TypedTree};
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct AlphaItem {
n: u64,
}
impl CollectionMeta for AlphaItem {
type SelfId = [u8; 8];
const NAME: &'static str = "alpha_items";
const VERSION: u16 = 1;
}
#[derive(Clone, Debug, PartialEq, Rapira, GetType)]
struct ZuluItem {
n: u64,
}
impl CollectionMeta for ZuluItem {
type SelfId = [u8; 8];
const NAME: &'static str = "zulu_items";
const VERSION: u16 = 1;
}
#[test]
fn inspection_registry_only_db_opened_collections() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let _via_db = db
.open_typed_tree::<AlphaItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let _direct = TypedTree::<[u8; 8], AlphaItem, RapiraCodec, NoHook>::open_hooked(
dir.path().join("direct_only"),
Config::test(),
RapiraCodec,
NoHook,
)
.unwrap();
let names: Vec<_> = lock(&db.inspection_handlers).keys().copied().collect();
assert_eq!(names, vec![AlphaItem::NAME]);
assert!(!names.contains(&"direct_only"));
}
#[test]
fn inspection_registry_collections_sorted_by_name() {
let dir = tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
let _zulu = db
.open_typed_tree::<ZuluItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let _alpha = db
.open_typed_tree::<AlphaItem, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let names: Vec<_> = db
.inspector()
.collections()
.unwrap()
.into_iter()
.map(|c| c.name)
.collect();
assert_eq!(
names,
vec![AlphaItem::NAME.to_owned(), ZuluItem::NAME.to_owned()]
);
}
}
#[cfg(test)]
mod inspection_rollback_tests {
use super::*;
use std::num::NonZeroU32;
use crate::armour::inspection::{
InspectionCollection, InspectionEntry, InspectionError, InspectionHandler, InspectionPage,
};
struct StubCollection(&'static str);
impl Collection for StubCollection {
fn name(&self) -> &str {
self.0
}
fn len(&self) -> usize {
0
}
fn compact(&self) -> DbResult<usize> {
Ok(0)
}
fn flush(&self) -> DbResult<()> {
Ok(())
}
fn periodic_flush(&self) -> DbResult<()> {
Ok(())
}
fn durable_flush(&self) -> DbResult<()> {
Ok(())
}
fn clean_shutdown(&self) -> DbResult<()> {
Ok(())
}
}
struct StubInspection;
impl InspectionHandler for StubInspection {
fn metadata(&self) -> Result<InspectionCollection, InspectionError> {
Err(InspectionError::CollectionNotFound {
name: "metadata-fail".into(),
})
}
fn get_raw(&self, _: &[u8]) -> DbResult<Option<Vec<u8>>> {
Ok(None)
}
fn exact_count(&self) -> DbResult<u64> {
Ok(0)
}
fn ordered_page(
&self,
_: &crate::armour::inspection::NormalizedBounds,
_: NonZeroU32,
) -> DbResult<InspectionPage> {
Ok(InspectionPage {
entries: vec![],
next_key: None,
})
}
fn unordered_snapshot(
&self,
_: NonZeroU32,
) -> Result<Vec<InspectionEntry>, InspectionError> {
Ok(vec![])
}
}
#[cfg(feature = "rpc")]
mod rpc_stubs {
use super::*;
use std::ops::Bound;
use crate::armour::handler::{KeyBytes, RpcHandler, ValueBytes};
use armour_rpc::protocol::WriteKey;
pub(super) struct StubRpc(pub &'static str);
impl RpcHandler for StubRpc {
fn name(&self) -> &str {
self.0
}
fn info(&self) -> (u64, u16) {
(0, 0)
}
fn schema(&self) -> armour_rpc::SchemaResponse {
armour_rpc::SchemaResponse {
name: self.0.to_owned(),
version: 0,
typ: <u8 as armour_core::GetType>::TYPE,
key_scheme: armour_core::KeyScheme::Bytes,
}
}
fn get(&self, _: &[u8]) -> DbResult<Option<ValueBytes>> {
Ok(None)
}
fn contains(&self, _: &[u8]) -> DbResult<bool> {
Ok(false)
}
fn entry_len(&self, _: &[u8]) -> DbResult<Option<u32>> {
Ok(None)
}
fn first(&self) -> DbResult<Option<(KeyBytes, ValueBytes)>> {
Ok(None)
}
fn last(&self) -> DbResult<Option<(KeyBytes, ValueBytes)>> {
Ok(None)
}
fn range(
&self,
_: Bound<KeyBytes>,
_: Bound<KeyBytes>,
_: u32,
) -> DbResult<Vec<(KeyBytes, ValueBytes)>> {
Ok(vec![])
}
fn range_keys(
&self,
_: Bound<KeyBytes>,
_: Bound<KeyBytes>,
_: u32,
) -> DbResult<Vec<ValueBytes>> {
Ok(vec![])
}
fn write(&self, _: WriteKey, _: Option<bool>, _: ValueBytes) -> DbResult<ValueBytes> {
Ok(vec![])
}
fn remove(&self, _: &[u8], _: bool) -> DbResult<()> {
Ok(())
}
fn take(&self, _: &[u8], _: bool) -> DbResult<Option<ValueBytes>> {
Ok(None)
}
fn count(&self, _: bool) -> DbResult<u64> {
Ok(0)
}
fn apply_batch(&self, _: Vec<(KeyBytes, Option<ValueBytes>)>) -> DbResult<()> {
Ok(())
}
}
}
fn publish_stub(db: &Db, name: &'static str, forced_hash: Option<u64>) -> DbResult<()> {
let collection: Arc<dyn Collection> = Arc::new(StubCollection(name));
let inspection: Arc<dyn InspectionHandler> = Arc::new(StubInspection);
#[cfg(feature = "rpc")]
let rpc =
Some(Arc::new(rpc_stubs::StubRpc(name)) as Arc<dyn crate::armour::handler::RpcHandler>);
db.publish_collection_for_test(
name,
collection,
inspection,
#[cfg(feature = "rpc")]
rpc,
forced_hash,
)
}
#[test]
fn inspection_rollback_hash_collision_leaves_registries_unchanged() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
publish_stub(&db, "alpha", None).unwrap();
let before_collections = lock(&db.collections).len();
let before_inspections = lock(&db.inspection_handlers).len();
#[cfg(feature = "rpc")]
let before_rpc = lock(&db.handlers).len();
let forced = xxhash_rust::xxh3::xxh3_64(b"alpha");
let error = publish_stub(&db, "beta", Some(forced)).unwrap_err();
assert!(matches!(error, DbError::DuplicateCollection(_)));
assert_eq!(lock(&db.collections).len(), before_collections);
assert_eq!(lock(&db.inspection_handlers).len(), before_inspections);
#[cfg(feature = "rpc")]
assert_eq!(lock(&db.handlers).len(), before_rpc);
}
#[test]
fn inspection_rollback_exact_name_collision_leaves_registries_unchanged() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open_test(dir.path()).unwrap();
publish_stub(&db, "alpha", None).unwrap();
let before_collections = lock(&db.collections).len();
let before_inspections = lock(&db.inspection_handlers).len();
#[cfg(feature = "rpc")]
let before_rpc = lock(&db.handlers).len();
let error = publish_stub(&db, "alpha", None).unwrap_err();
assert!(matches!(error, DbError::DuplicateCollection(_)));
assert_eq!(lock(&db.collections).len(), before_collections);
assert_eq!(lock(&db.inspection_handlers).len(), before_inspections);
#[cfg(feature = "rpc")]
assert_eq!(lock(&db.handlers).len(), before_rpc);
}
}
#[cfg(all(test, feature = "rapira-codec"))]
mod db_builder_tests {
use std::sync::Arc;
use super::*;
use crate::{CollectionMeta, Config, NoHook, RapiraCodec, TxConfig};
use armour_core::GetType;
use rapira::Rapira;
#[derive(Clone, Copy, Debug, PartialEq, Eq, GetType, Rapira)]
struct Row {
n: u32,
}
impl CollectionMeta for Row {
type SelfId = [u8; 8];
const NAME: &'static str = "rows";
}
#[test]
fn builder_defaults_match_open() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(Db::builder(dir.path()).open().unwrap());
let rows = db
.open_typed_map::<Row, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
rows.put(&1u64.to_be_bytes(), Row { n: 7 }).unwrap();
assert_eq!(rows.get(&1u64.to_be_bytes()).map(|r| r.n), Some(7));
}
#[test]
fn builder_carries_workers_and_tx_config_together() {
let dir = tempfile::tempdir().unwrap();
let db = Arc::new(
Db::builder(dir.path())
.for_tests()
.tx_config(TxConfig {
max_ops: 2,
..TxConfig::default()
})
.open()
.unwrap(),
);
let rows = db
.open_typed_map::<Row, RapiraCodec, _>(Config::test(), NoHook, &[])
.unwrap();
let err = match db
.tx()
.put(&rows, &1u64.to_be_bytes(), &Row { n: 1 })
.unwrap()
.put(&rows, &2u64.to_be_bytes(), &Row { n: 2 })
.unwrap()
.put(&rows, &3u64.to_be_bytes(), &Row { n: 3 })
{
Ok(_) => panic!("expected the third put to exceed max_ops"),
Err(e) => e,
};
assert!(matches!(err, crate::DbError::Client(_)), "{err}");
assert!(rows.get(&1u64.to_be_bytes()).is_none(), "nothing written");
}
#[test]
fn builder_carries_discard_pending_on_schema_mismatch() {
let dir = tempfile::tempdir().unwrap();
let db = Db::builder(dir.path())
.for_tests()
.tx_config(TxConfig {
discard_pending_on_schema_mismatch: true,
..TxConfig::default()
})
.open()
.unwrap();
assert!(db.tx_config().discard_pending_on_schema_mismatch);
}
#[test]
fn for_tests_is_a_preset_and_later_calls_override_it() {
let dir = tempfile::tempdir().unwrap();
let db = Db::builder(dir.path())
.for_tests()
.workers(WorkerIntervals::default())
.open()
.unwrap();
assert!(db.compactor.is_some(), "workers() re-enabled compaction");
assert!(
db.flusher.is_some(),
"workers() re-enabled the flush worker"
);
assert!(db.sweeper.is_some(), "workers() re-enabled the TTL sweeper");
drop(db);
}
}