use crate::BackendError;
use crate::io;
use crate::transaction_tracker::{TransactionId, TransactionTracker};
#[cfg(feature = "experimental-multiprocess")]
use crate::transactions::AllocatorStateLatch;
#[cfg(feature = "experimental-multiprocess")]
use crate::tree_store::HeaderGuard;
#[cfg(not(redb_no_std))]
use crate::tree_store::ReadOnlyBackend;
#[cfg(feature = "experimental-multiprocess")]
use crate::tree_store::WriterLock;
use crate::tree_store::{
AllocationPolicy, BtreeHeader, InternalTableDefinition, PAGE_SIZE, PageHint, PageNumber,
PageResolver, ShrinkPolicy, TableTree, TableType, TransactionalMemory,
};
use crate::types::{Key, Value};
use crate::{
CompactionError, DatabaseError, ReadOnlyTable, ReadableTable, StorageError, TableError,
};
use crate::{ReadTransaction, Result, WriteTransaction};
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use core::fmt::{Debug, Display, Formatter};
use alloc::sync::Arc;
use core::marker::PhantomData;
use core::ops::{Bound, Range};
#[cfg(not(redb_no_std))]
use std::fs::{File, OpenOptions};
#[cfg(not(redb_no_std))]
use std::path::Path;
use crate::error::TransactionError;
use crate::sealed::{Sealed, SealedInApi5};
use crate::transactions::{
ALLOCATOR_STATE_TABLE_NAME, AllocatorStateKey, AllocatorStateTree, DATA_ALLOCATED_TABLE,
DATA_FREED_TABLE, PageList, SYSTEM_FREED_TABLE, SystemTableDefinition,
TransactionIdWithPagination,
};
#[cfg(not(redb_no_std))]
use crate::tree_store::file_backend::FileBackend;
#[cfg(feature = "logging")]
use log::{debug, warn};
#[allow(clippy::len_without_is_empty)]
pub trait StorageBackend: 'static + Debug + Send + Sync {
fn len(&self) -> core::result::Result<u64, io::Error>;
fn read(&self, offset: u64, out: &mut [u8]) -> core::result::Result<(), io::Error>;
fn set_len(&self, len: u64) -> core::result::Result<(), io::Error>;
fn sync_data(&self) -> core::result::Result<(), io::Error>;
fn write(&self, offset: u64, data: &[u8]) -> core::result::Result<(), io::Error>;
fn close(&self) -> core::result::Result<(), io::Error> {
Ok(())
}
fn try_lock_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<bool, BackendError> {
Err(BackendError::Unsupported)
}
fn try_lock_shared_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<bool, BackendError> {
Err(BackendError::Unsupported)
}
fn lock_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<(), BackendError> {
Err(BackendError::Unsupported)
}
fn lock_shared_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<(), BackendError> {
Err(BackendError::Unsupported)
}
fn unlock_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<(), BackendError> {
Err(BackendError::Unsupported)
}
fn query_lock_range(
&self,
_start: Bound<u64>,
_end: Bound<u64>,
) -> core::result::Result<bool, BackendError> {
Err(BackendError::Unsupported)
}
}
#[cfg_attr(redb_no_std, allow(dead_code))]
pub(crate) const FULL_RANGE: (Bound<u64>, Bound<u64>) = (Bound::Unbounded, Bound::Unbounded);
#[cfg_attr(not(any(windows, unix, target_os = "wasi")), allow(dead_code))]
const LOCK_BASE: u64 = 1 << 62;
pub(crate) const BACKEND_LOCK_RANGE: Range<u64> = LOCK_BASE + 896..LOCK_BASE + 1024;
#[cfg(feature = "experimental-multiprocess")]
pub(crate) const WRITER_BYTE: u64 = LOCK_BASE;
#[cfg_attr(not(any(windows, unix, target_os = "wasi")), allow(dead_code))]
pub(crate) const SHARED_WRITER_BYTE: u64 = LOCK_BASE + 1;
#[cfg(feature = "experimental-multiprocess")]
pub(crate) const SHARED_READER_BYTE: u64 = LOCK_BASE + 2;
#[cfg(feature = "experimental-multiprocess")]
pub(crate) const WHOLE_FILE_READER_BYTE: u64 = LOCK_BASE + 3;
#[cfg(feature = "experimental-multiprocess")]
pub(crate) const CONSISTENT_BYTE: u64 = LOCK_BASE + 4;
#[cfg(feature = "experimental-multiprocess")]
pub(crate) const TXN_BASE: u64 = LOCK_BASE + 1024;
pub(crate) fn byte_range(offset: u64) -> (Bound<u64>, Bound<u64>) {
(Bound::Included(offset), Bound::Included(offset))
}
pub trait TableHandle: Sealed {
fn name(&self) -> &str;
}
#[derive(Clone)]
pub struct UntypedTableHandle {
name: String,
}
impl UntypedTableHandle {
pub(crate) fn new(name: String) -> Self {
Self { name }
}
}
impl TableHandle for UntypedTableHandle {
fn name(&self) -> &str {
&self.name
}
}
impl Sealed for UntypedTableHandle {}
pub trait MultimapTableHandle: Sealed {
fn name(&self) -> &str;
}
#[derive(Clone)]
pub struct UntypedMultimapTableHandle {
name: String,
}
impl UntypedMultimapTableHandle {
pub(crate) fn new(name: String) -> Self {
Self { name }
}
}
impl MultimapTableHandle for UntypedMultimapTableHandle {
fn name(&self) -> &str {
&self.name
}
}
impl Sealed for UntypedMultimapTableHandle {}
pub struct TableDefinition<'a, K: Key + 'static, V: Value + 'static> {
name: &'a str,
_key_type: PhantomData<K>,
_value_type: PhantomData<V>,
}
impl<'a, K: Key + 'static, V: Value + 'static> TableDefinition<'a, K, V> {
pub const fn new(name: &'a str) -> Self {
assert!(!name.is_empty());
Self {
name,
_key_type: PhantomData,
_value_type: PhantomData,
}
}
}
impl<K: Key + 'static, V: Value + 'static> TableHandle for TableDefinition<'_, K, V> {
fn name(&self) -> &str {
self.name
}
}
impl<K: Key, V: Value> Sealed for TableDefinition<'_, K, V> {}
impl<K: Key + 'static, V: Value + 'static> Clone for TableDefinition<'_, K, V> {
fn clone(&self) -> Self {
*self
}
}
impl<K: Key + 'static, V: Value + 'static> Copy for TableDefinition<'_, K, V> {}
impl<K: Key + 'static, V: Value + 'static> Display for TableDefinition<'_, K, V> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}<{}, {}>",
self.name,
K::type_name().name(),
V::type_name().name()
)
}
}
pub struct MultimapTableDefinition<'a, K: Key + 'static, V: Key + 'static> {
name: &'a str,
_key_type: PhantomData<K>,
_value_type: PhantomData<V>,
}
impl<'a, K: Key + 'static, V: Key + 'static> MultimapTableDefinition<'a, K, V> {
pub const fn new(name: &'a str) -> Self {
assert!(!name.is_empty());
Self {
name,
_key_type: PhantomData,
_value_type: PhantomData,
}
}
}
impl<K: Key + 'static, V: Key + 'static> MultimapTableHandle for MultimapTableDefinition<'_, K, V> {
fn name(&self) -> &str {
self.name
}
}
impl<K: Key, V: Key> Sealed for MultimapTableDefinition<'_, K, V> {}
impl<K: Key + 'static, V: Key + 'static> Clone for MultimapTableDefinition<'_, K, V> {
fn clone(&self) -> Self {
*self
}
}
impl<K: Key + 'static, V: Key + 'static> Copy for MultimapTableDefinition<'_, K, V> {}
impl<K: Key + 'static, V: Key + 'static> Display for MultimapTableDefinition<'_, K, V> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}<{}, {}>",
self.name,
K::type_name().name(),
V::type_name().name()
)
}
}
#[derive(Debug)]
pub struct CacheStats {
pub(crate) evictions: u64,
pub(crate) read_hits: u64,
pub(crate) read_misses: u64,
pub(crate) write_hits: u64,
pub(crate) write_misses: u64,
pub(crate) used_bytes: usize,
}
impl CacheStats {
pub fn evictions(&self) -> u64 {
self.evictions
}
pub fn read_hits(&self) -> u64 {
self.read_hits
}
pub fn read_misses(&self) -> u64 {
self.read_misses
}
pub fn write_hits(&self) -> u64 {
self.write_hits
}
pub fn write_misses(&self) -> u64 {
self.write_misses
}
pub fn used_bytes(&self) -> usize {
self.used_bytes
}
}
pub(crate) struct WriteSlot {
tracker: Arc<TransactionTracker>,
}
impl WriteSlot {
fn take(tracker: Arc<TransactionTracker>) -> Self {
tracker.take_write_slot();
Self { tracker }
}
}
impl Drop for WriteSlot {
fn drop(&mut self) {
if let Some(mem) = self.tracker.end_write_transaction() {
close_database(&self.tracker, &mem);
}
}
}
pub(crate) enum TransactionGuard {
Read {
tracker: Arc<TransactionTracker>,
transaction_id: TransactionId,
reference: Option<Arc<TransactionalMemory>>,
},
Write {
transaction_id: TransactionId,
#[cfg(feature = "experimental-multiprocess")]
_writer_lock: Arc<WriterLock>,
slot: WriteSlot,
},
Untracked,
}
impl TransactionGuard {
pub(crate) fn new_read(
transaction_id: TransactionId,
tracker: Arc<TransactionTracker>,
mem: &Arc<TransactionalMemory>,
) -> Self {
Self::Read {
tracker,
transaction_id,
reference: Some(mem.clone()),
}
}
pub(crate) fn new_read_unowned(
transaction_id: TransactionId,
tracker: Arc<TransactionTracker>,
) -> Self {
Self::Read {
tracker,
transaction_id,
reference: None,
}
}
pub(crate) fn release_to_database(&mut self) {
let Self::Read { reference, .. } = self else {
unreachable!("only a read transaction's reference may be leaked")
};
drop(reference.take());
}
pub(crate) fn owns_reference(&self) -> bool {
matches!(self, Self::Read { reference, .. } if reference.is_some())
}
pub(crate) fn tracker(&self) -> &Arc<TransactionTracker> {
match self {
Self::Read { tracker, .. } => tracker,
Self::Write { slot, .. } => &slot.tracker,
Self::Untracked => unreachable!("an untracked guard has no tracker"),
}
}
pub(crate) fn allocate_read(
tracker: Arc<TransactionTracker>,
mem: &Arc<TransactionalMemory>,
) -> Result<(Self, Option<BtreeHeader>)> {
let (id, root) = tracker.register_read_transaction(mem)?;
Ok((Self::new_read(id, tracker, mem), root))
}
pub(crate) fn new_write(
transaction_id: TransactionId,
slot: WriteSlot,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Arc<WriterLock>,
) -> Self {
Self::Write {
transaction_id,
#[cfg(feature = "experimental-multiprocess")]
_writer_lock: writer_lock,
slot,
}
}
pub(crate) fn untracked() -> Self {
Self::Untracked
}
pub(crate) fn id(&self) -> TransactionId {
match self {
Self::Read { transaction_id, .. } | Self::Write { transaction_id, .. } => {
*transaction_id
}
Self::Untracked => {
panic!("TransactionGuard::id() called on an untracked guard")
}
}
}
}
impl Drop for TransactionGuard {
fn drop(&mut self) {
match self {
Self::Read {
tracker,
transaction_id,
reference,
} => {
if let Some(mem) = reference {
tracker.deallocate_read_transaction(mem, *transaction_id);
}
}
Self::Write {
#[cfg(feature = "experimental-multiprocess")]
slot,
..
} => {
#[cfg(feature = "experimental-multiprocess")]
slot.tracker.begin_finalizing();
}
Self::Untracked => {}
}
}
}
pub trait ReadableDatabase: SealedInApi5 {
fn begin_read(&self) -> Result<ReadTransaction, TransactionError>;
fn cache_stats(&self) -> CacheStats;
}
#[cfg_attr(
feature = "experimental-multiprocess",
doc = "",
doc = "`SingleWriter` and `MultiWriter` are the exception: a reader shares the file with the writer and follows its commits. See [`Builder::set_concurrency_mode`](crate::Builder::set_concurrency_mode)."
)]
#[cfg(not(redb_no_std))]
pub struct ReadOnlyDatabase {
mem: Arc<TransactionalMemory>,
transaction_tracker: Arc<TransactionTracker>,
}
#[cfg(not(redb_no_std))]
impl Sealed for ReadOnlyDatabase {}
#[cfg(not(redb_no_std))]
impl ReadableDatabase for ReadOnlyDatabase {
fn begin_read(&self) -> Result<ReadTransaction, TransactionError> {
let (guard, root) =
TransactionGuard::allocate_read(self.transaction_tracker.clone(), &self.mem)?;
#[cfg(feature = "logging")]
debug!("Beginning read transaction id={:?}", guard.id());
ReadTransaction::new(self.mem.clone(), guard, root)
}
fn cache_stats(&self) -> CacheStats {
self.mem.cache_stats()
}
}
#[cfg(not(redb_no_std))]
impl ReadOnlyDatabase {
#[cfg(not(redb_no_std))]
pub fn open(path: impl AsRef<Path>) -> Result<ReadOnlyDatabase, DatabaseError> {
Builder::new().open_read_only(path)
}
fn new(
file: Box<dyn StorageBackend>,
page_size: usize,
region_size: Option<u64>,
cache_size: usize,
concurrency_mode: ConcurrencyMode,
) -> Result<Self, DatabaseError> {
#[cfg(feature = "logging")]
let file_path = format!("{:?}", &file);
#[cfg(feature = "logging")]
debug!("Opening database in read-only {:?}", &file_path);
let (mem, _writer_lock) = TransactionalMemory::new(
Box::new(ReadOnlyBackend::new(file)),
false,
page_size,
region_size,
cache_size,
true,
concurrency_mode,
)?;
let mem = Arc::new(mem);
#[cfg(feature = "experimental-multiprocess")]
let multiprocess_writer = concurrency_mode.is_multi_process_writable();
#[cfg(not(feature = "experimental-multiprocess"))]
let multiprocess_writer = false;
if !multiprocess_writer {
if let Some(tree) = Database::get_allocator_state_table(&mem)? {
mem.load_allocator_state(&tree)?;
} else {
#[cfg(feature = "logging")]
warn!(
"Database {:?} not shutdown cleanly. Repair required",
&file_path
);
return Err(DatabaseError::RepairAborted);
}
}
let next_transaction_id = mem.get_last_committed_transaction_id()?.next();
let db = Self {
mem,
transaction_tracker: Arc::new(TransactionTracker::new(next_transaction_id)),
};
Ok(db)
}
}
pub struct Database {
mem: Arc<TransactionalMemory>,
transaction_tracker: Arc<TransactionTracker>,
}
impl Sealed for Database {}
impl ReadableDatabase for Database {
fn begin_read(&self) -> Result<ReadTransaction, TransactionError> {
let (guard, root) =
TransactionGuard::allocate_read(self.transaction_tracker.clone(), &self.mem)?;
#[cfg(feature = "logging")]
debug!("Beginning read transaction id={:?}", guard.id());
ReadTransaction::new(self.get_memory(), guard, root)
}
fn cache_stats(&self) -> CacheStats {
self.mem.cache_stats()
}
}
impl Database {
#[cfg(not(redb_no_std))]
pub fn create(path: impl AsRef<Path>) -> Result<Database, DatabaseError> {
Self::builder().create(path)
}
#[cfg(not(redb_no_std))]
pub fn open(path: impl AsRef<Path>) -> Result<Database, DatabaseError> {
Self::builder().open(path)
}
pub(crate) fn get_memory(&self) -> Arc<TransactionalMemory> {
self.mem.clone()
}
pub(crate) fn verify_primary_checksums(mem: Arc<TransactionalMemory>) -> Result<bool> {
let data_root = mem.get_data_root();
let system_root = mem.get_system_root();
Self::verify_checksums(mem, data_root, system_root)
}
fn verify_checksums(
mem: Arc<TransactionalMemory>,
data_root: Option<BtreeHeader>,
system_root: Option<BtreeHeader>,
) -> Result<bool> {
let resolver = PageResolver::new(mem.clone());
let table_tree = TableTree::new(
data_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver.clone(),
)?;
if !table_tree.verify_checksums()? {
return Ok(false);
}
let system_table_tree = TableTree::new(
system_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver,
)?;
if !system_table_tree.verify_checksums()? {
return Ok(false);
}
Ok(true)
}
#[cfg_attr(
feature = "experimental-multiprocess",
doc = "",
doc = "In the multi-process concurrency modes the check holds the writer lock: it waits for a write transaction in another process to end, and no other process writes to the file until it returns. A read transaction in another process neither blocks the check nor is disturbed by it. See [`Builder::set_concurrency_mode`](crate::Builder::set_concurrency_mode)."
)]
pub fn check_integrity(&mut self) -> Result<bool, DatabaseError> {
if Arc::get_mut(&mut self.mem).is_none() {
return Err(DatabaseError::TransactionInProgress);
}
if self.transaction_tracker.any_ephemeral_savepoint_exists() {
return Err(DatabaseError::TransactionInProgress);
}
self.mem.check_io_errors()?;
if self.mem.allocator_state_invalidated() {
return Err(StorageError::Corrupted(
"Allocator state was discarded by a failed integrity check or commit; reopen the database to repair it".to_string(),
)
.into());
}
#[cfg(feature = "experimental-multiprocess")]
let writer_lock = self.mem.lock_writer()?;
let result = self.check_integrity_inner(
#[cfg(feature = "experimental-multiprocess")]
&writer_lock,
);
if result.is_err() {
self.mem.invalidate_allocator_state();
}
result
}
fn check_integrity_inner(
&mut self,
#[cfg(feature = "experimental-multiprocess")] writer_lock: &Arc<WriterLock>,
) -> Result<bool, DatabaseError> {
let mut rolling_back_non_durable = false;
if self.mem.pending_non_durable_commit() {
self.mem.clear_read_cache();
if self.mem.file_len_matches_layout()?
&& let Some(live_allocator_clean) = self.repair_live_state()?
{
let durable_clean = self.durable_state_clean()?;
let mut txn = self
.begin_write_with(
#[cfg(feature = "experimental-multiprocess")]
Some(writer_lock),
#[cfg(feature = "experimental-multiprocess")]
None,
)
.map_err(|e| DatabaseError::Storage(e.into_storage_error()))?;
txn.disable_post_commit_free();
txn.commit()
.map_err(|e| DatabaseError::Storage(e.into_storage_error()))?;
self.mem.clear_needs_repair();
return Ok(live_allocator_clean && durable_clean);
}
rolling_back_non_durable = true;
}
let own_allocator_hash = self.mem.allocator_hash();
let mem = Arc::get_mut(&mut self.mem).unwrap();
let (mut was_clean, peer_committed) = mem.clear_cache_and_reload()?;
let allocator_hash = if peer_committed {
match Self::get_allocator_state_table(&self.mem)? {
Some(tree) => {
self.mem.load_allocator_state(&tree)?;
Some(self.mem.allocator_hash())
}
None => None,
}
} else {
Some(own_allocator_hash)
};
#[cfg(feature = "experimental-multiprocess")]
if peer_committed
&& allocator_hash.is_none()
&& self.mem.concurrency_mode() == ConcurrencyMode::MultiWriter
{
was_clean = false;
}
let old_roots = [self.mem.get_data_root(), self.mem.get_system_root()];
let new_roots = Self::do_repair(&mut self.mem, &|_| {}).map_err(|err| match err {
DatabaseError::Storage(storage_err) => storage_err,
_ => unreachable!(),
})?;
if old_roots != new_roots
|| allocator_hash.is_some_and(|hash| hash != self.mem.allocator_hash())
|| rolling_back_non_durable
{
was_clean = false;
}
if !was_clean {
let next_transaction_id = self.mem.get_last_committed_transaction_id()?.next();
let [data_root, system_root] = new_roots;
self.mem.commit(
data_root,
system_root,
next_transaction_id,
true,
ShrinkPolicy::Never,
#[cfg(feature = "experimental-multiprocess")]
None,
)?;
self.transaction_tracker
.reserve_repair_transaction_id(next_transaction_id);
}
self.mem.clear_needs_repair();
self.mem.begin_writable()?;
if peer_committed {
Self::sync_persistent_savepoints(
&self.transaction_tracker,
&self.mem,
#[cfg(feature = "experimental-multiprocess")]
Some(writer_lock),
)?;
}
#[cfg(feature = "experimental-multiprocess")]
if !was_clean && self.mem.concurrency_mode() == ConcurrencyMode::MultiWriter {
ensure_allocator_state_table_and_trim(
&self.transaction_tracker,
&self.mem,
Some(writer_lock),
None,
)?;
}
Ok(was_clean)
}
fn sync_persistent_savepoints(
transaction_tracker: &Arc<TransactionTracker>,
mem: &Arc<TransactionalMemory>,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Option<&Arc<WriterLock>>,
) -> Result<(), DatabaseError> {
let txn = begin_write_with_allocation_policy(
transaction_tracker,
mem,
#[cfg(feature = "experimental-multiprocess")]
writer_lock,
#[cfg(feature = "experimental-multiprocess")]
None,
AllocationPolicy::Default,
)
.map_err(|e| e.into_storage_error())?;
sync_persistent_savepoints(
transaction_tracker,
#[cfg(feature = "experimental-multiprocess")]
mem,
&txn,
)?;
txn.abort()?;
Ok(())
}
fn repair_live_state(&mut self) -> Result<Option<bool>, DatabaseError> {
match Self::verify_primary_checksums(self.mem.clone()) {
Ok(true) => {
let live_allocator_hash = self.mem.allocator_hash();
let live_roots = [self.mem.get_data_root(), self.mem.get_system_root()];
match Self::rebuild_allocator_state(&self.mem, &|_| {}) {
Ok(roots) if roots != live_roots => Ok(None),
Ok(_) => Ok(Some(live_allocator_hash == self.mem.allocator_hash())),
Err(DatabaseError::Storage(StorageError::Corrupted(_))) => Ok(None),
Err(err) => Err(err),
}
}
Ok(false) | Err(StorageError::Corrupted(_)) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn durable_state_clean(&self) -> Result<bool, DatabaseError> {
match self.verify_durable_state() {
Ok(clean) => Ok(clean),
Err(DatabaseError::Storage(StorageError::Corrupted(_))) => Ok(false),
Err(err) => Err(err),
}
}
fn verify_durable_state(&self) -> Result<bool, DatabaseError> {
if self.mem.durable_primary_slot_corrupt()? {
return Ok(false);
}
let data_root = self.mem.get_durable_data_root();
let system_root = self.mem.get_durable_system_root();
Ok(Self::verify_checksums(
self.mem.clone(),
data_root,
system_root,
)?)
}
pub fn compact(&mut self) -> Result<bool, CompactionError> {
#[cfg(feature = "experimental-multiprocess")]
if self.mem.concurrency_mode() == ConcurrencyMode::MultiWriter
&& self.transaction_tracker.any_persistent_savepoint_exists()
&& !self.transaction_tracker.write_transaction_live()
{
self.begin_write()
.map_err(|e| e.into_storage_error())?
.abort()?;
}
if self.transaction_tracker.any_persistent_savepoint_exists() {
return Err(CompactionError::PersistentSavepointExists);
}
if self.transaction_tracker.any_savepoint_exists() {
return Err(CompactionError::EphemeralSavepointExists);
}
if self.transaction_tracker.any_user_read_reference_exists() {
return Err(CompactionError::TransactionInProgress);
}
#[cfg(feature = "experimental-multiprocess")]
let (writer_lock, header_lock) = if self.mem.concurrency_mode().is_multi_process_writable()
{
self.begin_write()
.map_err(|e| e.into_storage_error())?
.abort()?;
(
Some(self.mem.lock_writer()?),
Some(self.mem.lock_header_exclusive()?),
)
} else {
(None, None)
};
let txn = self
.begin_write_with(
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
#[cfg(feature = "experimental-multiprocess")]
header_lock.as_ref(),
)
.map_err(|e| e.into_storage_error())?;
if txn.list_persistent_savepoints()?.next().is_some() {
return Err(CompactionError::PersistentSavepointExists);
}
if self.transaction_tracker.any_savepoint_exists() {
return Err(CompactionError::EphemeralSavepointExists);
}
if self.transaction_tracker.any_user_read_reference_exists() {
return Err(CompactionError::TransactionInProgress);
}
#[cfg(feature = "experimental-multiprocess")]
if let Some(header_lock) = &header_lock
&& self
.mem
.oldest_active_transaction(None, header_lock)?
.is_some()
{
return Err(CompactionError::TransactionInProgress);
}
txn.abort()?;
self.drain_pending_free_pages(
ShrinkPolicy::Maximum,
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
#[cfg(feature = "experimental-multiprocess")]
header_lock.as_ref(),
)?;
let mut compacted = false;
loop {
let mut progress = false;
let mut txn = self
.begin_write_with(
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
#[cfg(feature = "experimental-multiprocess")]
header_lock.as_ref(),
)
.map_err(|e| e.into_storage_error())?;
txn.skip_allocator_state_record();
if txn.compact_pages()? {
progress = true;
txn.commit_with(
#[cfg(feature = "experimental-multiprocess")]
header_lock.as_ref(),
)
.map_err(|e| e.into_storage_error())?;
} else {
txn.abort()?;
}
self.drain_pending_free_pages(
ShrinkPolicy::Maximum,
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
#[cfg(feature = "experimental-multiprocess")]
header_lock.as_ref(),
)?;
if !progress {
break;
}
compacted = true;
}
#[cfg(feature = "experimental-multiprocess")]
if self.mem.concurrency_mode() == ConcurrencyMode::MultiWriter {
ensure_allocator_state_table_and_trim(
&self.transaction_tracker,
&self.mem,
writer_lock.as_ref(),
header_lock.as_ref(),
)?;
}
Ok(compacted)
}
fn drain_pending_free_pages(
&self,
shrink_policy: ShrinkPolicy,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Option<&Arc<WriterLock>>,
#[cfg(feature = "experimental-multiprocess")] header_lock: Option<&HeaderGuard<'_>>,
) -> Result {
let mut force_commit = true;
loop {
let mut txn = self
.begin_write_with(
#[cfg(feature = "experimental-multiprocess")]
writer_lock,
#[cfg(feature = "experimental-multiprocess")]
header_lock,
)
.map_err(|e| e.into_storage_error())?;
if !force_commit && !txn.pending_free_pages()? {
txn.abort()?;
return Ok(());
}
force_commit = false;
txn.skip_allocator_state_record();
txn.set_two_phase_commit(true);
txn.set_shrink_policy(shrink_policy);
txn.commit_with(
#[cfg(feature = "experimental-multiprocess")]
header_lock,
)
.map_err(|e| e.into_storage_error())?;
}
}
#[cfg_attr(not(debug_assertions), expect(dead_code))]
fn check_repaired_allocated_pages_table(
system_root: Option<BtreeHeader>,
mem: Arc<TransactionalMemory>,
) -> Result {
let resolver = PageResolver::new(mem.clone());
let table_tree = TableTree::new(
system_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver.clone(),
)?;
if let Some(table_def) = table_tree
.get_table::<TransactionIdWithPagination, PageList>(
DATA_ALLOCATED_TABLE.name(),
TableType::Normal,
)
.map_err(|e| e.into_storage_error_or_corrupted("Allocated pages table corrupted"))?
{
let InternalTableDefinition::Normal { table_root, .. } = table_def else {
unreachable!()
};
let table: ReadOnlyTable<TransactionIdWithPagination, PageList> = ReadOnlyTable::new(
DATA_ALLOCATED_TABLE.name().to_string(),
table_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver,
)?;
for result in ReadableTable::iter(&table)? {
let (_, pages) = result?;
for i in 0..pages.value().len() {
assert!(mem.is_allocated(pages.value().get(i)));
}
}
}
Ok(())
}
fn visit_freed_tree<K: Key, V: Value, F>(
system_root: Option<BtreeHeader>,
table_def: SystemTableDefinition<K, V>,
mem: Arc<TransactionalMemory>,
mut visitor: F,
) -> Result
where
F: FnMut(PageNumber) -> Result,
{
let untracked_guard = Arc::new(TransactionGuard::untracked());
let resolver = PageResolver::new(mem.clone());
let system_tree = TableTree::new(
system_root,
PageHint::None,
untracked_guard,
resolver.clone(),
)?;
let table_name = table_def.name();
let result = match system_tree.get_table::<K, V>(table_name, TableType::Normal) {
Ok(result) => result,
Err(TableError::Storage(err)) => {
return Err(err);
}
Err(TableError::TableDoesNotExist(_)) => {
return Ok(());
}
Err(_) => {
return Err(StorageError::Corrupted(format!(
"Unable to open {table_name}"
)));
}
};
if let Some(definition) = result {
let table_root = match definition {
InternalTableDefinition::Normal { table_root, .. } => table_root,
InternalTableDefinition::Multimap { .. } => unreachable!(),
};
let table: ReadOnlyTable<TransactionIdWithPagination, PageList<'static>> =
ReadOnlyTable::new(
table_name.to_string(),
table_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver,
)?;
for result in ReadableTable::iter(&table)? {
let (_, page_list) = result?;
for i in 0..page_list.value().len() {
visitor(page_list.value().get(i))?;
}
}
}
Ok(())
}
#[cfg(debug_assertions)]
fn mark_allocated_page_for_debug(mem: &Arc<TransactionalMemory>) -> Result {
let data_root = mem.get_data_root();
{
let untracked = Arc::new(TransactionGuard::untracked());
let tables = TableTree::new(
data_root,
PageHint::None,
untracked,
PageResolver::new(mem.clone()),
)?;
tables.visit_all_pages(|path| {
mem.mark_debug_allocated_page(path.page_number());
Ok(())
})?;
}
let system_root = mem.get_system_root();
{
let untracked = Arc::new(TransactionGuard::untracked());
let system_tables = TableTree::new(
system_root,
PageHint::None,
untracked,
PageResolver::new(mem.clone()),
)?;
system_tables.visit_all_pages(|path| {
mem.mark_debug_allocated_page(path.page_number());
Ok(())
})?;
}
Self::visit_freed_tree(system_root, DATA_FREED_TABLE, mem.clone(), |page| {
mem.mark_debug_allocated_page(page);
Ok(())
})?;
Self::visit_freed_tree(system_root, SYSTEM_FREED_TABLE, mem.clone(), |page| {
mem.mark_debug_allocated_page(page);
Ok(())
})?;
Ok(())
}
fn primary_verifies(mem: &Arc<TransactionalMemory>) -> Result<bool> {
match Self::verify_primary_checksums(mem.clone()) {
Ok(verified) => Ok(verified),
Err(StorageError::Corrupted(_)) => Ok(false),
Err(err) => Err(err),
}
}
fn do_repair(
mem: &mut Arc<TransactionalMemory>, repair_callback: &(dyn Fn(&mut RepairSession) + 'static),
) -> Result<[Option<BtreeHeader>; 2], DatabaseError> {
if !Self::primary_verifies(mem)? {
if mem.used_two_phase_commit() {
return Err(DatabaseError::Storage(StorageError::Corrupted(
"Primary is corrupted despite 2-phase commit".to_string(),
)));
}
let mut handle = RepairSession::new(0.3);
repair_callback(&mut handle);
if handle.aborted() {
return Err(DatabaseError::RepairAborted);
}
mem.repair_primary_corrupted();
mem.clear_read_cache();
if !Self::primary_verifies(mem)? {
return Err(DatabaseError::Storage(StorageError::Corrupted(
"Failed to repair database. All roots are corrupted".to_string(),
)));
}
}
let mut handle = RepairSession::new(0.6);
repair_callback(&mut handle);
if handle.aborted() {
return Err(DatabaseError::RepairAborted);
}
let [data_root, system_root] = Self::rebuild_allocator_state(mem, repair_callback)?;
mem.clear_recovery_required()?;
mem.clear_read_cache();
Ok([data_root, system_root])
}
fn rebuild_allocator_state(
mem: &Arc<TransactionalMemory>,
repair_callback: &(dyn Fn(&mut RepairSession) + 'static),
) -> Result<[Option<BtreeHeader>; 2], DatabaseError> {
mem.reset_allocator_state()?;
let data_root = {
let root = mem.get_data_root();
let untracked = Arc::new(TransactionGuard::untracked());
let tables = TableTree::new(
root,
PageHint::None,
untracked,
PageResolver::new(mem.clone()),
)?;
tables.visit_all_pages(|path| mem.mark_page_allocated(path.page_number()))?;
Self::with_recounted_length(root, tables.count_tables()?)
};
let mut handle = RepairSession::new(0.9);
repair_callback(&mut handle);
if handle.aborted() {
return Err(DatabaseError::RepairAborted);
}
let system_root = {
let root = mem.get_system_root();
let untracked = Arc::new(TransactionGuard::untracked());
let system_tables = TableTree::new(
root,
PageHint::None,
untracked,
PageResolver::new(mem.clone()),
)?;
system_tables.visit_all_pages(|path| mem.mark_page_allocated(path.page_number()))?;
Self::with_recounted_length(root, system_tables.count_tables()?)
};
Self::visit_freed_tree(system_root, DATA_FREED_TABLE, mem.clone(), |page| {
mem.mark_page_allocated(page)
})?;
Self::visit_freed_tree(system_root, SYSTEM_FREED_TABLE, mem.clone(), |page| {
mem.mark_page_allocated(page)
})?;
for page in mem.unpersisted_data_freed_pages() {
mem.mark_page_allocated(page)?;
}
#[cfg(debug_assertions)]
{
Self::check_repaired_allocated_pages_table(system_root, mem.clone())?;
}
Ok([data_root, system_root])
}
fn with_recounted_length(root: Option<BtreeHeader>, length: u64) -> Option<BtreeHeader> {
root.map(|header| BtreeHeader::new(header.root, header.checksum, length))
}
#[cfg(feature = "experimental-multiprocess")]
fn load_synced_allocator_state(
mem: &Arc<TransactionalMemory>,
_writer_lock: &WriterLock,
) -> Result {
if let Some(tree) = Self::get_allocator_state_table(mem)? {
mem.load_allocator_state(&tree)?;
#[cfg(debug_assertions)]
Self::mark_allocated_page_for_debug(mem)?;
} else {
if !Self::verify_primary_checksums(mem.clone())? {
return Err(StorageError::Corrupted(
"Cannot rebuild allocator state: committed trees are corrupted".to_string(),
));
}
let roots = Self::rebuild_allocator_state(mem, &|_| {})
.map_err(DatabaseError::into_storage_error_or_corrupted)?;
if roots != [mem.get_data_root(), mem.get_system_root()] {
return Err(StorageError::Corrupted(
"Committed table counts do not match the database trees".to_string(),
));
}
}
mem.clear_needs_repair();
Ok(())
}
fn new(
file: Box<dyn StorageBackend>,
allow_initialize: bool,
page_size: usize,
region_size: Option<u64>,
cache_size: usize,
concurrency_mode: ConcurrencyMode,
repair_callback: &(dyn Fn(&mut RepairSession) + 'static),
) -> Result<Self, DatabaseError> {
#[cfg(feature = "logging")]
let file_path = format!("{:?}", &file);
#[cfg(feature = "logging")]
debug!("Opening database {:?}", &file_path);
let (mem, writer_lock) = TransactionalMemory::new(
file,
allow_initialize,
page_size,
region_size,
cache_size,
false,
concurrency_mode,
)?;
let mut mem = Arc::new(mem);
let repaired = if let Some(tree) = Self::get_allocator_state_table(&mem)? {
#[cfg(feature = "logging")]
debug!("Found valid allocator state, full repair not needed");
mem.load_allocator_state(&tree)?;
#[cfg(debug_assertions)]
Self::mark_allocated_page_for_debug(&mem)?;
false
} else {
#[cfg(feature = "logging")]
warn!("Database {:?} not shutdown cleanly. Repairing", &file_path);
let mut handle = RepairSession::new(0.0);
repair_callback(&mut handle);
if handle.aborted() {
return Err(DatabaseError::RepairAborted);
}
let [data_root, system_root] = Self::do_repair(&mut mem, repair_callback)?;
let next_transaction_id = mem.get_last_committed_transaction_id()?.next();
mem.commit(
data_root,
system_root,
next_transaction_id,
true,
ShrinkPolicy::Never,
#[cfg(feature = "experimental-multiprocess")]
None,
)?;
true
};
mem.begin_writable()?;
#[cfg(feature = "experimental-multiprocess")]
mem.mark_consistent()?;
let next_transaction_id = mem.get_last_committed_transaction_id()?.next();
let transaction_tracker = Arc::new(TransactionTracker::new(next_transaction_id));
Self::sync_persistent_savepoints(
&transaction_tracker,
&mem,
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
)?;
#[cfg(feature = "experimental-multiprocess")]
if repaired && concurrency_mode == ConcurrencyMode::MultiWriter {
ensure_allocator_state_table_and_trim(
&transaction_tracker,
&mem,
writer_lock.as_ref(),
None,
)?;
}
#[cfg(not(feature = "experimental-multiprocess"))]
let _ = (repaired, writer_lock);
Ok(Database {
mem,
transaction_tracker,
})
}
fn get_allocator_state_table(
mem: &Arc<TransactionalMemory>,
) -> Result<Option<AllocatorStateTree>> {
if !mem.used_two_phase_commit() {
return Ok(None);
}
let resolver = PageResolver::new(mem.clone());
let system_table_tree = TableTree::new(
mem.get_system_root(),
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver.clone(),
)?;
let Some(allocator_state_table) = system_table_tree
.get_table::<AllocatorStateKey, &[u8]>(ALLOCATOR_STATE_TABLE_NAME, TableType::Normal)
.map_err(|e| e.into_storage_error_or_corrupted("Unexpected TableError"))?
else {
return Ok(None);
};
let InternalTableDefinition::Normal { table_root, .. } = allocator_state_table else {
unreachable!();
};
let tree = AllocatorStateTree::new(
table_root,
PageHint::None,
Arc::new(TransactionGuard::untracked()),
resolver,
)?;
if !mem.is_valid_allocator_state(&tree)? {
return Ok(None);
}
Ok(Some(tree))
}
pub fn builder() -> Builder {
Builder::new()
}
pub fn begin_write(&self) -> Result<WriteTransaction, TransactionError> {
self.begin_write_with(
#[cfg(feature = "experimental-multiprocess")]
None,
#[cfg(feature = "experimental-multiprocess")]
None,
)
}
fn begin_write_with(
&self,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Option<&Arc<WriterLock>>,
#[cfg(feature = "experimental-multiprocess")] header_lock: Option<&HeaderGuard<'_>>,
) -> Result<WriteTransaction, TransactionError> {
begin_write_with_allocation_policy(
&self.transaction_tracker,
&self.mem,
#[cfg(feature = "experimental-multiprocess")]
writer_lock,
#[cfg(feature = "experimental-multiprocess")]
header_lock,
AllocationPolicy::Default,
)
}
}
fn sync_persistent_savepoints(
transaction_tracker: &TransactionTracker,
#[cfg(feature = "experimental-multiprocess")] mem: &TransactionalMemory,
txn: &WriteTransaction,
) -> Result {
if let Some(next_id) = txn.next_persistent_savepoint_id()? {
transaction_tracker.restore_savepoint_counter_state(next_id);
}
let current = txn.persistent_savepoint_transactions()?;
#[cfg(feature = "experimental-multiprocess")]
for &transaction_id in current.values() {
mem.check_active_transaction_id(transaction_id)?;
}
transaction_tracker.sync_persistent_savepoints(¤t)
}
#[cfg(feature = "experimental-multiprocess")]
fn sync_to_latest_commit(
mem: &Arc<TransactionalMemory>,
writer_lock: &WriterLock,
header_lock: Option<&HeaderGuard<'_>>,
) -> Result<Option<AllocatorStateLatch>> {
if mem.concurrency_mode() != ConcurrencyMode::MultiWriter {
return Ok(None);
}
let synced = {
let hold = mem.header_hold(header_lock)?;
mem.reload_for_write(writer_lock, &hold)?
};
if !synced {
return Ok(None);
}
let latch = AllocatorStateLatch::arm(mem.clone());
Database::load_synced_allocator_state(mem, writer_lock)?;
mem.mark_recovery_required();
Ok(Some(latch))
}
fn begin_write_with_allocation_policy(
transaction_tracker: &Arc<TransactionTracker>,
mem: &Arc<TransactionalMemory>,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Option<&Arc<WriterLock>>,
#[cfg(feature = "experimental-multiprocess")] header_lock: Option<&HeaderGuard<'_>>,
allocation_policy: AllocationPolicy,
) -> Result<WriteTransaction, TransactionError> {
mem.check_io_errors()?;
let slot = WriteSlot::take(transaction_tracker.clone());
#[cfg(feature = "experimental-multiprocess")]
let writer_lock = writer_lock.map_or_else(|| mem.lock_writer(), |lent| Ok(lent.clone()))?;
#[cfg(feature = "experimental-multiprocess")]
let latch = sync_to_latest_commit(mem, &writer_lock, header_lock)?;
mem.check_io_errors()?;
if mem.allocator_state_invalidated() {
return Err(StorageError::Corrupted(
"Allocator state was discarded by a failed integrity check or commit; reopen the database to repair it".to_string(),
)
.into());
}
let transaction_id = transaction_tracker.issue_write_transaction_id(
mem.get_last_committed_transaction_id()?,
#[cfg(feature = "experimental-multiprocess")]
&writer_lock,
);
let guard = TransactionGuard::new_write(
transaction_id,
slot,
#[cfg(feature = "experimental-multiprocess")]
writer_lock,
);
let transaction = WriteTransaction::new(
guard,
transaction_tracker.clone(),
mem.clone(),
allocation_policy,
)?;
#[cfg(feature = "experimental-multiprocess")]
if latch.is_some() {
sync_persistent_savepoints(transaction_tracker, mem, &transaction)?;
}
#[cfg(feature = "experimental-multiprocess")]
if let Some(latch) = latch {
latch.disarm();
}
Ok(transaction)
}
fn ensure_allocator_state_table_and_trim(
transaction_tracker: &Arc<TransactionTracker>,
mem: &Arc<TransactionalMemory>,
#[cfg(feature = "experimental-multiprocess")] writer_lock: Option<&Arc<WriterLock>>,
#[cfg(feature = "experimental-multiprocess")] header_lock: Option<&HeaderGuard<'_>>,
) -> Result {
#[cfg(feature = "logging")]
debug!("Writing allocator state table");
let mut tx = begin_write_with_allocation_policy(
transaction_tracker,
mem,
#[cfg(feature = "experimental-multiprocess")]
writer_lock,
#[cfg(feature = "experimental-multiprocess")]
header_lock,
AllocationPolicy::Lowest,
)
.map_err(|e| e.into_storage_error())?;
tx.set_quick_repair(true);
tx.disable_post_commit_free();
tx.set_shrink_policy(ShrinkPolicy::Maximum);
tx.commit_with(
#[cfg(feature = "experimental-multiprocess")]
header_lock,
)
.map_err(|e| e.into_storage_error())?;
Ok(())
}
fn close_database(transaction_tracker: &Arc<TransactionTracker>, mem: &Arc<TransactionalMemory>) {
let writing = !crate::panicking() && !mem.needs_repair() && mem.check_io_errors().is_ok();
#[cfg(feature = "experimental-multiprocess")]
let writer_lock = if writing {
mem.lock_writer().ok()
} else {
None
};
#[cfg(feature = "experimental-multiprocess")]
let writing = writing && writer_lock.is_some();
let recorded = writing
&& ensure_allocator_state_table_and_trim(
transaction_tracker,
mem,
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
#[cfg(feature = "experimental-multiprocess")]
None,
)
.is_ok();
if writing && !recorded {
#[cfg(feature = "logging")]
warn!("Failed to write allocator state table. Repair may be required at restart.");
}
#[cfg(feature = "experimental-multiprocess")]
let writer_lock = writer_lock.filter(|_| recorded);
if mem
.close(
#[cfg(feature = "experimental-multiprocess")]
writer_lock.as_ref(),
)
.is_err()
{
#[cfg(feature = "logging")]
warn!("Failed to flush database file. Repair may be required at restart.");
}
}
impl Drop for Database {
fn drop(&mut self) {
if self
.transaction_tracker
.defer_close_if_write_transaction_live(&self.mem)
{
#[cfg(feature = "logging")]
warn!(
"Database dropped while a write transaction is in progress. The database will remain open until the write transaction completes."
);
return;
}
close_database(&self.transaction_tracker, &self.mem);
}
}
pub struct RepairSession {
progress: f64,
aborted: bool,
}
impl RepairSession {
pub(crate) fn new(progress: f64) -> Self {
Self {
progress,
aborted: false,
}
}
pub(crate) fn aborted(&self) -> bool {
self.aborted
}
pub fn abort(&mut self) {
self.aborted = true;
}
pub fn progress(&self) -> f64 {
self.progress
}
}
#[cfg_attr(
not(feature = "experimental-multiprocess"),
allow(dead_code, unreachable_pub, clippy::enum_variant_names)
)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub enum ConcurrencyMode {
#[default]
ExclusiveWriter,
SingleWriter,
MultiWriter,
}
#[cfg(feature = "experimental-multiprocess")]
impl ConcurrencyMode {
pub(crate) fn is_multi_process_writable(self) -> bool {
!matches!(self, ConcurrencyMode::ExclusiveWriter)
}
}
pub struct Builder {
page_size: usize,
region_size: Option<u64>,
cache_size: usize,
concurrency_mode: ConcurrencyMode,
repair_callback: Box<dyn Fn(&mut RepairSession)>,
}
impl Builder {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
page_size: PAGE_SIZE,
region_size: None,
concurrency_mode: ConcurrencyMode::ExclusiveWriter,
cache_size: 1024 * 1024 * 1024,
repair_callback: Box::new(|_| {}),
}
}
pub fn set_repair_callback(
&mut self,
callback: impl Fn(&mut RepairSession) + 'static,
) -> &mut Self {
self.repair_callback = Box::new(callback);
self
}
#[cfg(any(fuzzing, test))]
pub fn set_page_size(&mut self, size: usize) -> &mut Self {
assert!(size.is_power_of_two());
self.page_size = core::cmp::max(size, 512);
self
}
pub fn set_cache_size(&mut self, bytes: usize) -> &mut Self {
self.cache_size = bytes;
self
}
#[cfg(feature = "experimental-multiprocess")]
pub fn set_concurrency_mode(&mut self, mode: ConcurrencyMode) -> &mut Self {
self.concurrency_mode = mode;
self
}
#[cfg(any(test, fuzzing))]
pub fn set_region_size(&mut self, size: u64) -> &mut Self {
assert!(size.is_power_of_two());
self.region_size = Some(size);
self
}
#[cfg(not(redb_no_std))]
pub fn create(&self, path: impl AsRef<Path>) -> Result<Database, DatabaseError> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
Database::new(
Box::new(FileBackend::new(file)?),
true,
self.page_size,
self.region_size,
self.cache_size,
self.concurrency_mode,
&self.repair_callback,
)
}
#[cfg(not(redb_no_std))]
pub fn open(&self, path: impl AsRef<Path>) -> Result<Database, DatabaseError> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
Database::new(
Box::new(FileBackend::new(file)?),
false,
self.page_size,
None,
self.cache_size,
self.concurrency_mode,
&self.repair_callback,
)
}
#[cfg_attr(
feature = "experimental-multiprocess",
doc = "",
doc = "Only an `ExclusiveWriter` writer is refused: in `SingleWriter` and `MultiWriter` a reader opened in the same mode as the writer shares the file with it and picks up its commits. See [`Self::set_concurrency_mode`]."
)]
#[cfg(not(redb_no_std))]
pub fn open_read_only(
&self,
path: impl AsRef<Path>,
) -> Result<ReadOnlyDatabase, DatabaseError> {
let file = OpenOptions::new().read(true).open(path)?;
ReadOnlyDatabase::new(
Box::new(FileBackend::new(file)?),
self.page_size,
None,
self.cache_size,
self.concurrency_mode,
)
}
#[cfg(not(redb_no_std))]
pub fn create_file(&self, file: File) -> Result<Database, DatabaseError> {
Database::new(
Box::new(FileBackend::new(file)?),
true,
self.page_size,
self.region_size,
self.cache_size,
self.concurrency_mode,
&self.repair_callback,
)
}
pub fn create_with_backend(
&self,
backend: impl StorageBackend,
) -> Result<Database, DatabaseError> {
Database::new(
Box::new(backend),
true,
self.page_size,
self.region_size,
self.cache_size,
self.concurrency_mode,
&self.repair_callback,
)
}
}
impl core::fmt::Debug for Database {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Database").finish()
}
}
#[cfg(test)]
mod test {
use crate::backends::FileBackend;
use crate::{
CommitError, Database, DatabaseError, Durability, ReadableTable, StorageBackend,
StorageError, TableDefinition, TransactionError,
};
use alloc::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
use std::fs::File;
use std::io::{ErrorKind, Read, Seek, SeekFrom};
#[derive(Debug)]
struct FailingBackend {
inner: FileBackend,
countdown: Arc<AtomicU64>,
}
impl FailingBackend {
fn new(backend: FileBackend, countdown: u64) -> Self {
Self {
inner: backend,
countdown: Arc::new(AtomicU64::new(countdown)),
}
}
fn check_countdown(&self) -> Result<(), std::io::Error> {
if self.countdown.load(Ordering::SeqCst) == 0 {
return Err(std::io::Error::from(ErrorKind::Other));
}
Ok(())
}
fn decrement_countdown(&self) -> Result<(), std::io::Error> {
if self
.countdown
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
if x > 0 { Some(x - 1) } else { None }
})
.is_err()
{
return Err(std::io::Error::from(ErrorKind::Other));
}
Ok(())
}
}
impl StorageBackend for FailingBackend {
fn len(&self) -> Result<u64, std::io::Error> {
self.inner.len()
}
fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), std::io::Error> {
self.check_countdown()?;
self.inner.read(offset, out)
}
fn set_len(&self, len: u64) -> Result<(), std::io::Error> {
self.inner.set_len(len)
}
fn sync_data(&self) -> Result<(), std::io::Error> {
self.check_countdown()?;
self.inner.sync_data()
}
fn write(&self, offset: u64, data: &[u8]) -> Result<(), std::io::Error> {
self.decrement_countdown()?;
self.inner.write(offset, data)
}
}
#[test]
fn read_snapshot_survives_commits_before_construction() {
use super::{ReadTransaction, TransactionGuard};
const TABLE: TableDefinition<u64, u64> = TableDefinition::new("snapshot");
for durability in [Durability::Immediate, Durability::None] {
let tmpfile = crate::create_tempfile();
let db = Database::create(tmpfile.path()).unwrap();
let mut write = db.begin_write().unwrap();
write.set_durability(durability).unwrap();
write.open_table(TABLE).unwrap().insert(0, 0).unwrap();
write.commit().unwrap();
let (guard, root) =
TransactionGuard::allocate_read(db.transaction_tracker.clone(), &db.mem).unwrap();
assert_eq!(
guard.id(),
db.mem.get_last_committed_transaction_id().unwrap()
);
for value in 1..4 {
let mut write = db.begin_write().unwrap();
write.set_durability(durability).unwrap();
write.open_table(TABLE).unwrap().insert(0, value).unwrap();
write.commit().unwrap();
}
let read = ReadTransaction::new(db.mem.clone(), guard, root).unwrap();
assert_eq!(
read.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
0
);
}
}
#[test]
fn crash_regression4() {
let tmpfile = crate::create_tempfile();
let (file, path) = tmpfile.into_parts();
let backend = FailingBackend::new(FileBackend::new(file).unwrap(), 20);
let db = Database::builder()
.set_cache_size(12686)
.set_page_size(8 * 1024)
.set_region_size(32 * 4096)
.create_with_backend(backend)
.unwrap();
let table_def: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let tx = db.begin_write().unwrap();
let _savepoint = tx.ephemeral_savepoint().unwrap();
let _persistent_savepoint = tx.persistent_savepoint().unwrap();
tx.commit().unwrap();
let tx = db.begin_write().unwrap();
{
let mut table = tx.open_table(table_def).unwrap();
let _ = table.insert_reserve(118821, 360).unwrap();
}
let result = tx.commit();
assert!(result.is_err());
drop(db);
Database::builder()
.set_cache_size(1024 * 1024)
.set_page_size(8 * 1024)
.set_region_size(32 * 4096)
.create(&path)
.unwrap();
}
#[test]
fn transient_io_error() {
let tmpfile = crate::create_tempfile();
let (file, path) = tmpfile.into_parts();
let backend = FailingBackend::new(FileBackend::new(file).unwrap(), u64::MAX);
let countdown = backend.countdown.clone();
let db = Database::builder()
.set_cache_size(0)
.create_with_backend(backend)
.unwrap();
let table_def: TableDefinition<u64, u64> = TableDefinition::new("x");
let tx = db.begin_write().unwrap();
{
let mut table = tx.open_table(table_def).unwrap();
table.insert(0, 0).unwrap();
}
tx.commit().unwrap();
let tx = db.begin_write().unwrap();
{
let mut table = tx.open_table(table_def).unwrap();
table.insert(0, 1).unwrap();
}
tx.commit().unwrap();
let tx = db.begin_write().unwrap();
countdown.store(0, Ordering::SeqCst);
let result = tx.commit().err().unwrap();
assert!(matches!(result, CommitError::Storage(StorageError::Io(_))));
let result = db.begin_write().err().unwrap();
assert!(matches!(
result,
TransactionError::Storage(StorageError::PreviousIo)
));
countdown.store(u64::MAX, Ordering::SeqCst);
drop(db);
let mut file = File::open(&path).unwrap();
file.seek(SeekFrom::Start(9)).unwrap();
let mut god_byte = vec![0u8];
assert_eq!(file.read(&mut god_byte).unwrap(), 1);
assert_ne!(god_byte[0] & 2, 0);
}
#[test]
fn small_pages() {
let tmpfile = crate::create_tempfile();
let db = Database::builder()
.set_page_size(512)
.create(tmpfile.path())
.unwrap();
let table_definition: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let txn = db.begin_write().unwrap();
{
txn.open_table(table_definition).unwrap();
}
txn.commit().unwrap();
}
#[test]
fn small_pages2() {
let tmpfile = crate::create_tempfile();
let db = Database::builder()
.set_page_size(512)
.create(tmpfile.path())
.unwrap();
let table_def: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let savepoint0 = tx.ephemeral_savepoint().unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let savepoint1 = tx.ephemeral_savepoint().unwrap();
tx.restore_savepoint(&savepoint0).unwrap();
tx.set_durability(Durability::None).unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
t.insert_reserve(&660503, 489).unwrap().as_mut().fill(0xFF);
assert!(t.remove(&291295).unwrap().is_none());
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
tx.restore_savepoint(&savepoint0).unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let savepoint2 = tx.ephemeral_savepoint().unwrap();
drop(savepoint0);
tx.restore_savepoint(&savepoint2).unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
assert!(t.get(&2059).unwrap().is_none());
assert!(t.remove(&145227).unwrap().is_none());
assert!(t.remove(&145227).unwrap().is_none());
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let savepoint3 = tx.ephemeral_savepoint().unwrap();
drop(savepoint1);
tx.restore_savepoint(&savepoint3).unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let savepoint4 = tx.ephemeral_savepoint().unwrap();
drop(savepoint2);
tx.restore_savepoint(&savepoint3).unwrap();
tx.set_durability(Durability::None).unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
assert!(t.remove(&207936).unwrap().is_none());
}
tx.abort().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
let _savepoint5 = tx.ephemeral_savepoint().unwrap();
drop(savepoint3);
tx.restore_savepoint(&savepoint4).unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
let mut tx = db.begin_write().unwrap();
tx.set_two_phase_commit(true);
tx.restore_savepoint(&savepoint4).unwrap();
tx.set_durability(Durability::None).unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
}
#[test]
fn small_pages3() {
let tmpfile = crate::create_tempfile();
let db = Database::builder()
.set_page_size(1024)
.create(tmpfile.path())
.unwrap();
let table_def: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let mut tx = db.begin_write().unwrap();
let _savepoint0 = tx.ephemeral_savepoint().unwrap();
tx.set_durability(Durability::None).unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
let value = vec![0; 306];
t.insert(&539717, value.as_slice()).unwrap();
}
tx.abort().unwrap();
let mut tx = db.begin_write().unwrap();
let savepoint1 = tx.ephemeral_savepoint().unwrap();
tx.restore_savepoint(&savepoint1).unwrap();
tx.set_durability(Durability::None).unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
let value = vec![0; 2008];
t.insert(&784384, value.as_slice()).unwrap();
}
tx.abort().unwrap();
}
#[test]
fn small_pages4() {
let tmpfile = crate::create_tempfile();
let db = Database::builder()
.set_cache_size(1024 * 1024)
.set_page_size(1024)
.create(tmpfile.path())
.unwrap();
let table_def: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let tx = db.begin_write().unwrap();
{
tx.open_table(table_def).unwrap();
}
tx.commit().unwrap();
let tx = db.begin_write().unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
assert!(t.get(&131072).unwrap().is_none());
let value = vec![0xFF; 1130];
t.insert(&42394, value.as_slice()).unwrap();
t.insert_reserve(&744037, 3645).unwrap().as_mut().fill(0xFF);
assert!(t.get(&0).unwrap().is_none());
}
tx.abort().unwrap();
let tx = db.begin_write().unwrap();
{
let mut t = tx.open_table(table_def).unwrap();
t.insert_reserve(&118749, 734).unwrap().as_mut().fill(0xFF);
}
tx.abort().unwrap();
}
#[test]
fn dynamic_shrink() {
let tmpfile = crate::create_tempfile();
let table_definition: TableDefinition<u64, &[u8]> = TableDefinition::new("x");
let big_value = vec![0u8; 1024];
let db = Database::builder()
.set_region_size(1024 * 1024)
.create(tmpfile.path())
.unwrap();
let txn = db.begin_write().unwrap();
{
let mut table = txn.open_table(table_definition).unwrap();
for i in 0..2048 {
table.insert(&i, big_value.as_slice()).unwrap();
}
}
txn.commit().unwrap();
let file_size = tmpfile.as_file().metadata().unwrap().len();
let txn = db.begin_write().unwrap();
{
let mut table = txn.open_table(table_definition).unwrap();
for i in 0..2048 {
table.remove(&i).unwrap();
}
}
txn.commit().unwrap();
let txn = db.begin_write().unwrap();
{
let mut table = txn.open_table(table_definition).unwrap();
table.insert(0, [].as_slice()).unwrap();
}
txn.commit().unwrap();
let txn = db.begin_write().unwrap();
{
let mut table = txn.open_table(table_definition).unwrap();
table.remove(0).unwrap();
}
txn.commit().unwrap();
let txn = db.begin_write().unwrap();
txn.commit().unwrap();
let final_file_size = tmpfile.as_file().metadata().unwrap().len();
assert!(final_file_size < file_size);
}
#[test]
fn create_new_db_in_empty_file() {
let tmpfile = crate::create_tempfile();
let _db = Database::builder()
.create_file(tmpfile.into_file())
.unwrap();
}
#[test]
fn open_missing_file() {
let tmpfile = crate::create_tempfile();
let err = Database::builder()
.open(tmpfile.path().with_extension("missing"))
.unwrap_err();
match err {
DatabaseError::Storage(StorageError::Io(err)) if err.kind() == ErrorKind::NotFound => {}
err => panic!("Unexpected error for empty file: {err}"),
}
}
#[test]
fn open_empty_file() {
let tmpfile = crate::create_tempfile();
let err = Database::builder().open(tmpfile.path()).unwrap_err();
match err {
DatabaseError::Storage(StorageError::Io(err))
if err.kind() == ErrorKind::InvalidData => {}
err => panic!("Unexpected error for empty file: {err}"),
}
}
}
#[cfg(all(
test,
feature = "experimental-multiprocess",
any(target_os = "linux", target_vendor = "apple", windows)
))]
mod writer_byte_test {
use super::{ConcurrencyMode, Database, WRITER_BYTE, byte_range};
use crate::TableDefinition;
use crate::tree_store::HEADER_LOCK;
use crate::tree_store::file_backend::range_lock::RangeLock;
use std::fs::{File, OpenOptions};
use std::path::Path;
const TABLE: TableDefinition<u64, u64> = TableDefinition::new("x");
fn create(path: &Path, mode: ConcurrencyMode) -> Database {
let mut builder = Database::builder();
builder.set_concurrency_mode(mode);
builder.create(path).unwrap()
}
fn probe(path: &Path) -> File {
OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap()
}
fn commit_one(db: &Database) {
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(0, 0).unwrap();
}
write.commit().unwrap();
}
fn left_unclean(path: &Path) -> bool {
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
match builder.open_read_only(path) {
Err(super::DatabaseError::RepairAborted) => true,
Err(err) => panic!("{err}"),
Ok(_) => false,
}
}
#[test]
fn the_shutdown_header_is_written_under_the_writer_byte_or_not_at_all() {
fn close(path: &Path, under_the_byte: bool) {
let db = create(path, ConcurrencyMode::MultiWriter);
commit_one(&db);
let mem = db.mem.clone();
std::mem::forget(db);
if under_the_byte {
let writer_lock = mem.lock_writer().unwrap();
mem.close(Some(&writer_lock)).unwrap();
} else {
mem.close(None).unwrap();
}
}
let tmpfile = crate::create_tempfile();
close(tmpfile.path(), false);
assert!(
left_unclean(tmpfile.path()),
"a header was written without the byte"
);
let tmpfile = crate::create_tempfile();
close(tmpfile.path(), true);
assert!(
!left_unclean(tmpfile.path()),
"no header was written under the byte"
);
}
#[test]
fn a_close_leaves_a_clean_file() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
commit_one(&db);
drop(db);
assert!(
!left_unclean(tmpfile.path()),
"the close left the file unclean"
);
}
#[test]
fn a_multi_writer_repair_records_the_allocator_state() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
}
#[test]
fn a_sync_rebuilds_a_missing_allocator_state() {
use crate::{ReadableDatabase, ReadableTable};
let tmpfile = crate::create_tempfile();
let mut builder = Database::builder();
builder
.set_concurrency_mode(ConcurrencyMode::MultiWriter)
.set_cache_size(0);
let db = builder.create(tmpfile.path()).unwrap();
commit_one(&db);
let old_read = db.begin_read().unwrap();
let peer = builder.open(tmpfile.path()).unwrap();
let mut write = peer.begin_write().unwrap();
let savepoint_id = write.persistent_savepoint().unwrap();
write.skip_allocator_state_record();
write.open_table(TABLE).unwrap().insert(0, 1).unwrap();
write.commit().unwrap();
let mut write = db.begin_write().unwrap();
{
let table = write.open_table(TABLE).unwrap();
assert_eq!(table.get(0).unwrap().unwrap().value(), 1);
}
let savepoint = write.get_persistent_savepoint(savepoint_id).unwrap();
write.restore_savepoint(&savepoint).unwrap();
write.abort().unwrap();
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
assert_eq!(table.get(0).unwrap().unwrap().value(), 1);
table.insert(0, 2).unwrap();
}
write.commit().unwrap();
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
let old_table = old_read.open_table(TABLE).unwrap();
assert_eq!(old_table.get(0).unwrap().unwrap().value(), 0);
let mut write = peer.begin_write().unwrap();
let savepoint = write.get_persistent_savepoint(savepoint_id).unwrap();
write.restore_savepoint(&savepoint).unwrap();
write.commit().unwrap();
let read = peer.begin_read().unwrap();
let table = read.open_table(TABLE).unwrap();
assert_eq!(table.get(0).unwrap().unwrap().value(), 0);
}
#[test]
fn a_writer_recovers_after_interrupted_compaction() {
use crate::tree_store::ShrinkPolicy;
use crate::{ReadableDatabase, ReadableTable};
use std::process::Command;
const CHILD_PATH: &str = "REDB_TEST_INTERRUPTED_COMPACTION_PATH";
let mut builder = Database::builder();
builder
.set_concurrency_mode(ConcurrencyMode::MultiWriter)
.set_cache_size(0);
if let Some(path) = std::env::var_os(CHILD_PATH) {
let db = builder.open(path).unwrap();
let write = db.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
let writer = db.mem.lock_writer().unwrap();
let header = db.mem.lock_header_exclusive().unwrap();
db.drain_pending_free_pages(ShrinkPolicy::Maximum, Some(&writer), Some(&header))
.unwrap();
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_none()
);
std::process::exit(42);
}
let tmpfile = crate::create_tempfile();
let db = builder.create(tmpfile.path()).unwrap();
commit_one(&db);
let child = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"db::writer_byte_test::a_writer_recovers_after_interrupted_compaction",
"--nocapture",
])
.env(CHILD_PATH, tmpfile.path())
.output()
.unwrap();
assert_eq!(child.status.code(), Some(42), "{child:?}");
let reader = builder.open_read_only(tmpfile.path()).unwrap();
let old_read = reader.begin_read().unwrap();
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
assert_eq!(table.get(1).unwrap().unwrap().value(), 1);
table.insert(2, 2).unwrap();
}
write.commit().unwrap();
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
let old_table = old_read.open_table(TABLE).unwrap();
assert_eq!(old_table.get(1).unwrap().unwrap().value(), 1);
assert!(old_table.get(2).unwrap().is_none());
builder
.set_repair_callback(|_| panic!("the recovered writer did not record its allocator"));
let peer = builder.open(tmpfile.path()).unwrap();
let read = peer.begin_read().unwrap();
let table = read.open_table(TABLE).unwrap();
for key in 0..3u64 {
assert_eq!(table.get(key).unwrap().unwrap().value(), key);
}
}
#[test]
fn a_sync_refuses_corruption_without_an_allocator_snapshot() {
use crate::tree_store::ShrinkPolicy;
use crate::{StorageError, TransactionError};
for corrupt_checksum in [true, false] {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let peer = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
commit_one(&peer);
let writer = peer.mem.lock_writer().unwrap();
let mut root = peer.mem.get_data_root().unwrap();
if corrupt_checksum {
root.checksum ^= 1;
} else {
root.length += 1;
}
peer.mem
.commit(
Some(root),
peer.mem.get_system_root(),
peer.mem.get_last_committed_transaction_id().unwrap().next(),
true,
ShrinkPolicy::Never,
None,
)
.unwrap();
peer.mem.invalidate_allocator_state();
drop(writer);
assert!(matches!(
db.begin_write(),
Err(TransactionError::Storage(StorageError::Corrupted(_)))
));
assert!(!db.mem.allocator_state_loaded());
assert!(matches!(
db.begin_write(),
Err(TransactionError::Storage(StorageError::Corrupted(_)))
));
}
}
#[test]
fn syncing_to_a_peers_commit_releases_the_repair_a_panic_latched() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let peer = builder.open(tmpfile.path()).unwrap();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let write = db.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
panic!("unwinding through the transaction");
}));
assert!(unwound.is_err());
assert!(db.mem.needs_repair());
commit_one(&peer);
commit_one(&db);
assert!(!db.mem.needs_repair());
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
}
#[test]
fn a_transaction_releases_the_repair_a_panic_latched_without_a_peers_commit() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let peer = builder.open(tmpfile.path()).unwrap();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let write = db.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
panic!("unwinding through the transaction");
}));
assert!(unwound.is_err());
assert!(db.mem.needs_repair());
commit_one(&db);
assert!(!db.mem.needs_repair());
peer.begin_write()
.expect("the peer syncs to a commit that recorded the allocator state")
.abort()
.unwrap();
}
#[test]
fn an_open_waits_behind_a_creator_instead_of_calling_the_file_empty() {
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
let initialized = crate::create_tempfile();
drop(create(initialized.path(), ConcurrencyMode::MultiWriter));
let mut bytes = Vec::new();
File::open(initialized.path())
.unwrap()
.read_to_end(&mut bytes)
.unwrap();
let tmpfile = crate::create_tempfile();
let probe = probe(tmpfile.path());
assert!(probe.try_lock_range(HEADER_LOCK).unwrap());
let path = tmpfile.path().to_path_buf();
let (opened, opens) = mpsc::channel();
let opening = thread::spawn(move || {
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let result = builder.open(&path);
opened.send(()).unwrap();
result
});
assert!(
opens.recv_timeout(Duration::from_millis(500)).is_err(),
"the open read the file while the creator held the header lock"
);
(&probe).seek(SeekFrom::Start(0)).unwrap();
(&probe).write_all(&bytes).unwrap();
probe.sync_all().unwrap();
probe.unlock_range(HEADER_LOCK).unwrap();
opens
.recv_timeout(Duration::from_secs(10))
.expect("the open never took the header lock");
opening
.join()
.unwrap()
.expect("the open used a length it read before the creator finished");
}
#[test]
fn a_multi_writer_open_waits_for_the_writer_byte() {
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut write = db.begin_write().unwrap();
write.skip_allocator_state_record();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
let probe = probe(tmpfile.path());
assert!(probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap());
let path = tmpfile.path().to_path_buf();
let (repairing, repairs) = mpsc::channel();
let opening = thread::spawn(move || {
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
builder.set_repair_callback(move |_| {
let _ = repairing.send(());
});
builder.open(&path).unwrap()
});
assert!(
repairs.recv_timeout(Duration::from_millis(500)).is_err(),
"the open repaired while the writer byte was held"
);
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
repairs
.recv_timeout(Duration::from_secs(10))
.expect("the repair runs once the byte is free");
opening.join().unwrap();
}
#[test]
fn a_multi_writer_open_reads_the_file_a_peer_committed_while_it_waited() {
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut write = db.begin_write().unwrap();
write.skip_allocator_state_record();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
let held = db.begin_write().unwrap();
held.open_table(TABLE).unwrap().insert(2, 2).unwrap();
let path = tmpfile.path().to_path_buf();
let (repairing, repairs) = mpsc::channel();
let (opened, opens) = mpsc::channel();
let opening = thread::spawn(move || {
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
builder.set_repair_callback(move |_| {
let _ = repairing.send(());
});
let db = builder.open(&path).unwrap();
opened.send(()).unwrap();
db
});
assert!(
opens.recv_timeout(Duration::from_millis(500)).is_err(),
"the open did not wait for the byte"
);
held.commit().unwrap();
opens
.recv_timeout(Duration::from_secs(10))
.expect("the open takes the byte the commit released");
let peer = opening.join().unwrap();
assert!(
repairs.try_recv().is_err(),
"the open repaired a file the commit had recorded"
);
let read = super::ReadableDatabase::begin_read(&peer).unwrap();
let table = read.open_table(TABLE).unwrap();
assert_eq!(
crate::ReadableTable::get(&table, 2)
.unwrap()
.unwrap()
.value(),
2
);
}
#[test]
fn a_multi_writer_compaction_records_the_allocator_state() {
let tmpfile = crate::create_tempfile();
let mut db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
commit_one(&db);
db.compact().unwrap();
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
}
#[test]
fn a_multi_writer_transaction_holds_the_byte_and_gives_it_back() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let write = db.begin_write().unwrap();
assert!(
!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"the byte was free while a write transaction was open"
);
write.commit().unwrap();
assert!(
probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"the byte was still held after the transaction ended"
);
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
}
#[test]
fn a_single_writer_open_holds_the_byte_for_its_lifetime() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::SingleWriter);
let probe = probe(tmpfile.path());
assert!(
!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"the open did not take the writer byte"
);
commit_one(&db);
assert!(
!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"a write transaction punctured the open's hold on the writer byte"
);
drop(db);
assert!(
probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"the byte was still held after the database closed"
);
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
}
#[test]
fn a_transaction_outliving_the_database_keeps_the_writer_lock() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::SingleWriter);
let probe = probe(tmpfile.path());
let write = db.begin_write().unwrap();
drop(db);
assert!(
!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"dropping the database released the byte from under a live transaction"
);
write.commit().unwrap();
assert!(
probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap(),
"the byte was still held after the deferred close"
);
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
}
}
#[cfg(all(
test,
feature = "experimental-multiprocess",
any(target_os = "linux", target_vendor = "apple", windows)
))]
mod consistent_byte_test {
use super::{CONSISTENT_BYTE, ConcurrencyMode, Database, byte_range};
use crate::backends::FileBackend;
use crate::tree_store::file_backend::range_lock::RangeLock;
use crate::{StorageBackend, TableDefinition};
use std::fs::{File, OpenOptions};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
const TABLE: TableDefinition<u64, u64> = TableDefinition::new("x");
fn builder(mode: ConcurrencyMode) -> crate::Builder {
let mut builder = Database::builder();
builder.set_concurrency_mode(mode);
builder
}
fn probe(path: &Path) -> File {
OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap()
}
fn held(probe: &File) -> bool {
let taken = probe.try_lock_range(byte_range(CONSISTENT_BYTE)).unwrap();
if taken {
probe.unlock_range(byte_range(CONSISTENT_BYTE)).unwrap();
}
!taken
}
#[derive(Debug)]
struct CrashBackend {
inner: FileBackend,
dead: Arc<AtomicBool>,
}
impl StorageBackend for CrashBackend {
fn len(&self) -> Result<u64, std::io::Error> {
self.inner.len()
}
fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), std::io::Error> {
self.inner.read(offset, out)
}
fn set_len(&self, len: u64) -> Result<(), std::io::Error> {
if self.dead.load(Ordering::SeqCst) {
return Ok(());
}
self.inner.set_len(len)
}
fn sync_data(&self) -> Result<(), std::io::Error> {
if self.dead.load(Ordering::SeqCst) {
return Ok(());
}
self.inner.sync_data()
}
fn write(&self, offset: u64, data: &[u8]) -> Result<(), std::io::Error> {
if self.dead.load(Ordering::SeqCst) {
return Ok(());
}
self.inner.write(offset, data)
}
}
fn dirty(path: &Path) {
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
let dead = Arc::new(AtomicBool::new(false));
let db = Database::builder()
.create_with_backend(CrashBackend {
inner: FileBackend::new(file).unwrap(),
dead: Arc::clone(&dead),
})
.unwrap();
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(0, 0).unwrap();
}
write.commit().unwrap();
dead.store(true, Ordering::SeqCst);
drop(db);
}
#[test]
fn a_shared_writable_open_asserts_consistency_until_it_closes() {
for mode in [ConcurrencyMode::SingleWriter, ConcurrencyMode::MultiWriter] {
let tmpfile = crate::create_tempfile();
let db = builder(mode).create(tmpfile.path()).unwrap();
let probe = probe(tmpfile.path());
assert!(held(&probe), "{mode:?} open did not take the byte");
drop(db);
assert!(!held(&probe), "{mode:?} close did not release the byte");
}
}
#[test]
fn a_repairing_open_asserts_consistency_only_once_the_repair_is_done() {
let tmpfile = crate::create_tempfile();
dirty(tmpfile.path());
let probe = Arc::new(Mutex::new(probe(tmpfile.path())));
let during: Arc<Mutex<Option<bool>>> = Arc::new(Mutex::new(None));
let db = {
let probe = probe.clone();
let during = during.clone();
let mut builder = builder(ConcurrencyMode::MultiWriter);
builder.set_repair_callback(move |_| {
let mut during = during.lock().unwrap();
if during.is_none() {
*during = Some(held(&probe.lock().unwrap()));
}
});
builder.open(tmpfile.path()).unwrap()
};
assert_eq!(
during.lock().unwrap().take(),
Some(false),
"the byte was held while the open was still repairing"
);
assert!(held(&probe.lock().unwrap()), "the open never took the byte");
drop(db);
}
}
#[cfg(all(
test,
feature = "experimental-multiprocess",
any(target_os = "linux", target_vendor = "apple", windows)
))]
mod active_transaction_test {
use super::{ConcurrencyMode, Database, ReadableDatabase, TXN_BASE, WRITER_BYTE, byte_range};
use crate::tree_store::HEADER_LOCK;
use crate::tree_store::file_backend::range_lock::RangeLock;
use crate::{Durability, SavepointError, SetDurabilityError, TableDefinition};
use std::fs::{File, OpenOptions};
use std::path::Path;
const TABLE: TableDefinition<u64, u64> = TableDefinition::new("x");
const SEARCHED: std::ops::Range<u64> = 0..16;
fn create(path: &Path, mode: ConcurrencyMode) -> Database {
let mut builder = Database::builder();
builder.set_concurrency_mode(mode);
let db = builder.create(path).unwrap();
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(0, 0).unwrap();
}
write.commit().unwrap();
db
}
fn probe(path: &Path) -> File {
OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap()
}
fn is_held(probe: &File, id: u64) -> bool {
let free = probe.try_lock_range(byte_range(TXN_BASE + id)).unwrap();
if free {
probe.unlock_range(byte_range(TXN_BASE + id)).unwrap();
}
!free
}
fn held_ids(probe: &File) -> Vec<u64> {
SEARCHED.filter(|id| is_held(probe, *id)).collect()
}
#[test]
fn a_read_transaction_locks_the_id_it_reads_until_it_ends() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
assert!(held_ids(&probe).is_empty());
let read = db.begin_read().unwrap();
assert_eq!(
held_ids(&probe).len(),
1,
"a read transaction locked {:?}",
held_ids(&probe)
);
drop(read);
assert!(
held_ids(&probe).is_empty(),
"a lock outlived the read transaction that took it"
);
}
#[test]
fn concurrent_readers_of_one_snapshot_share_the_lock() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let first = db.begin_read().unwrap();
let second = db.begin_read().unwrap();
let held = held_ids(&probe);
assert_eq!(held.len(), 1, "expected one active id: {held:?}");
drop(first);
assert!(is_held(&probe, held[0]), "released early");
drop(second);
assert!(!is_held(&probe, held[0]), "never released");
}
#[test]
fn a_read_only_handle_locks_what_it_reads() {
let tmpfile = crate::create_tempfile();
drop(create(tmpfile.path(), ConcurrencyMode::MultiWriter));
let probe = probe(tmpfile.path());
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let db = builder.open_read_only(tmpfile.path()).unwrap();
let read = db.begin_read().unwrap();
assert_eq!(
held_ids(&probe).len(),
1,
"a read-only handle locked {:?}",
held_ids(&probe)
);
drop(read);
assert!(held_ids(&probe).is_empty(), "the lock outlived the reader");
}
#[test]
fn read_only_snapshot_survives_new_read_before_construction() {
use super::{ReadTransaction, TransactionGuard};
use crate::ReadableTable;
for mode in [ConcurrencyMode::SingleWriter, ConcurrencyMode::MultiWriter] {
let tmpfile = crate::create_tempfile();
let writer = create(tmpfile.path(), mode);
let db = Database::builder()
.set_concurrency_mode(mode)
.set_cache_size(0)
.open_read_only(tmpfile.path())
.unwrap();
let (guard, root) =
TransactionGuard::allocate_read(db.transaction_tracker.clone(), &db.mem).unwrap();
for value in 1..4 {
let write = writer.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(0, value).unwrap();
write.commit().unwrap();
}
let latest = db.begin_read().unwrap();
assert_eq!(
latest
.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
3
);
let read = ReadTransaction::new(db.mem.clone(), guard, root).unwrap();
assert_eq!(
read.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
0
);
}
}
#[cfg(feature = "cache_metrics")]
#[test]
fn read_only_file_snapshots_preserve_memory_state_and_reuse_cache() {
use super::TransactionGuard;
use crate::ReadableTable;
for mode in [ConcurrencyMode::SingleWriter, ConcurrencyMode::MultiWriter] {
let tmpfile = crate::create_tempfile();
let writer = create(tmpfile.path(), mode);
let db = Database::builder()
.set_concurrency_mode(mode)
.open_read_only(tmpfile.path())
.unwrap();
let original_id = db.mem.get_last_committed_transaction_id().unwrap();
let original_root = db.mem.get_data_root();
let read = db.begin_read().unwrap();
assert_eq!(
read.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
0
);
let cached_bytes = db.cache_stats().used_bytes();
assert!(cached_bytes > 0);
let (same, _) =
TransactionGuard::allocate_read(db.transaction_tracker.clone(), &db.mem).unwrap();
assert_eq!(same.id(), original_id);
assert_eq!(db.cache_stats().used_bytes(), cached_bytes);
let write = writer.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(0, 1).unwrap();
write.commit().unwrap();
let (latest, _) =
TransactionGuard::allocate_read(db.transaction_tracker.clone(), &db.mem).unwrap();
assert!(latest.id() > original_id);
assert_eq!(db.cache_stats().used_bytes(), 0);
assert_eq!(
db.mem.get_last_committed_transaction_id().unwrap(),
original_id
);
assert_eq!(db.mem.get_data_root(), original_root);
let current = db.begin_read().unwrap();
assert_eq!(
current
.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
1
);
let cached_bytes = db.cache_stats().used_bytes();
assert!(cached_bytes > 0);
let (repeated, _) =
TransactionGuard::allocate_read(db.transaction_tracker.clone(), &db.mem).unwrap();
assert_eq!(repeated.id(), latest.id());
assert_eq!(db.cache_stats().used_bytes(), cached_bytes);
assert_eq!(
read.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
0
);
}
}
#[cfg(feature = "cache_metrics")]
#[test]
fn a_multi_writer_commit_keeps_its_read_cache() {
use crate::ReadableTable;
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
{
let read = db.begin_read().unwrap();
assert_eq!(
read.open_table(TABLE)
.unwrap()
.get(0)
.unwrap()
.unwrap()
.value(),
0
);
}
let write = db.begin_write().unwrap();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
let cached_bytes = db.cache_stats().used_bytes();
assert!(cached_bytes > 0);
let _read = db.begin_read().unwrap();
assert_eq!(db.cache_stats().used_bytes(), cached_bytes);
}
#[test]
fn an_ephemeral_savepoint_locks_the_snapshot_it_references() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::SingleWriter);
let probe = probe(tmpfile.path());
assert!(held_ids(&probe).is_empty());
let write = db.begin_write().unwrap();
let savepoint = write.ephemeral_savepoint().unwrap();
write.commit().unwrap();
assert_eq!(
held_ids(&probe).len(),
1,
"a savepoint locked {:?}",
held_ids(&probe)
);
drop(savepoint);
assert!(
held_ids(&probe).is_empty(),
"the lock outlived the savepoint"
);
}
#[test]
fn a_persistent_savepoint_takes_no_active_transaction_byte() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let pinned = db.mem.get_last_committed_transaction_id().unwrap();
let write = db.begin_write().unwrap();
let savepoint = write.persistent_savepoint().unwrap();
write.commit().unwrap();
assert!(
held_ids(&probe).is_empty(),
"the savepoint locked {:?}",
held_ids(&probe)
);
assert_eq!(
db.transaction_tracker.oldest_local_referenced_transaction(),
Some(pinned),
"the savepoint stopped bounding this process's reclamation"
);
let read = db.begin_read().unwrap();
assert_eq!(held_ids(&probe).len(), 1);
drop(read);
assert!(held_ids(&probe).is_empty());
let write = db.begin_write().unwrap();
assert!(write.delete_persistent_savepoint(savepoint).unwrap());
write.commit().unwrap();
assert_eq!(
db.transaction_tracker.oldest_local_referenced_transaction(),
None,
"the deleted savepoint kept its reference"
);
assert!(held_ids(&probe).is_empty());
}
#[test]
fn a_synced_persistent_savepoint_takes_no_active_transaction_byte() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let write = db.begin_write().unwrap();
write.persistent_savepoint().unwrap();
write.commit().unwrap();
drop(db);
let probe = probe(tmpfile.path());
assert!(
held_ids(&probe).is_empty(),
"the closed database left a lock"
);
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
assert!(
held_ids(&probe).is_empty(),
"the open locked {:?} for the file's savepoint",
held_ids(&probe)
);
assert!(
db.transaction_tracker
.oldest_local_referenced_transaction()
.is_some(),
"the open did not adopt the file's savepoint"
);
let read = db.begin_read().unwrap();
assert_eq!(held_ids(&probe).len(), 1);
drop(read);
}
#[test]
fn a_shared_mode_refuses_durability_none() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::SingleWriter);
let mut write = db.begin_write().unwrap();
assert!(matches!(
write.set_durability(Durability::None),
Err(SetDurabilityError::NonDurableCommitUnsupported)
));
}
#[test]
fn a_multi_writer_commit_records_the_allocator_state_regardless() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut write = db.begin_write().unwrap();
write.set_quick_repair(false);
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
}
#[test]
fn a_multi_writer_mode_refuses_an_ephemeral_savepoint() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let write = db.begin_write().unwrap();
assert!(matches!(
write.ephemeral_savepoint(),
Err(SavepointError::EphemeralSavepointUnsupported)
));
write.persistent_savepoint().unwrap();
write.commit().unwrap();
}
#[test]
fn an_integrity_check_records_a_peers_unrecorded_commit() {
let tmpfile = crate::create_tempfile();
let mut db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let peer = Database::builder()
.set_concurrency_mode(ConcurrencyMode::MultiWriter)
.open(tmpfile.path())
.unwrap();
let mut write = peer.begin_write().unwrap();
write.skip_allocator_state_record();
write.open_table(TABLE).unwrap().insert(1, 1).unwrap();
write.commit().unwrap();
assert!(!db.check_integrity().unwrap());
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
drop(peer);
}
#[test]
fn a_write_transaction_releases_the_writer_byte_and_slot() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let txn = db.begin_write().unwrap();
assert!(db.transaction_tracker.holds_writer_byte());
assert!(db.transaction_tracker.write_transaction_live());
assert!(!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap());
txn.abort().unwrap();
assert!(!db.transaction_tracker.holds_writer_byte());
assert!(!db.transaction_tracker.write_transaction_live());
assert!(probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap());
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
}
#[test]
fn an_integrity_check_repairs_a_leak_a_peer_recorded() {
use crate::tree_store::{AllocationPolicy, PageAllocator, PageTracker};
let tmpfile = crate::create_tempfile();
let mut db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let peer = Database::builder()
.set_concurrency_mode(ConcurrencyMode::MultiWriter)
.open(tmpfile.path())
.unwrap();
let allocator = PageAllocator::new(peer.mem.clone(), AllocationPolicy::Default);
drop(allocator.allocate(64, &PageTracker::ignore()).unwrap());
drop(allocator);
drop(peer);
assert!(!db.check_integrity().unwrap());
assert!(
Database::get_allocator_state_table(&db.mem)
.unwrap()
.is_some()
);
assert!(db.check_integrity().unwrap());
}
#[test]
fn a_read_only_participant_picks_up_a_peers_commit() {
let tmpfile = crate::create_tempfile();
let writer = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let reader = builder.open_read_only(tmpfile.path()).unwrap();
let probe = probe(tmpfile.path());
let read = reader.begin_read().unwrap();
let before = held_ids(&probe);
assert_eq!(before.len(), 1, "expected one active id: {before:?}");
let before = before[0];
drop(read);
let write = writer.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(1, 1).unwrap();
}
write.commit().unwrap();
drop(writer);
assert!(
held_ids(&probe).is_empty(),
"the closed writer left {:?} locked",
held_ids(&probe)
);
let read = reader.begin_read().unwrap();
let after = held_ids(&probe);
assert_eq!(after.len(), 1, "expected one active id: {after:?}");
assert!(
after[0] > before,
"the reader stayed at {before} after the writer committed"
);
drop(read);
}
#[test]
fn a_read_only_participant_takes_the_primary_as_recorded() {
use std::io::{Read, Write};
let tmpfile = crate::create_tempfile();
drop(create(tmpfile.path(), ConcurrencyMode::ExclusiveWriter));
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
let reader = builder.open_read_only(tmpfile.path()).unwrap();
let copy = crate::create_tempfile();
std::fs::copy(tmpfile.path(), copy.path()).unwrap();
let db = Database::open(copy.path()).unwrap();
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(0, 1).unwrap();
}
write.commit().unwrap();
drop(db);
let header = |path: &Path| -> Vec<u8> {
let mut header = vec![0u8; 320];
File::open(path).unwrap().read_exact(&mut header).unwrap();
header
};
let slot = |index: usize| 64 + index * 128..64 + (index + 1) * 128;
let id = |header: &[u8], index: usize| {
u64::from_le_bytes(header[slot(index)][104..112].try_into().unwrap())
};
let newer = header(copy.path());
let mut spliced = header(tmpfile.path());
let primary = usize::from(spliced[9] & 1);
spliced[slot(1 - primary)].copy_from_slice(&newer[slot(usize::from(newer[9] & 1))]);
spliced[9] &= !6;
OpenOptions::new()
.write(true)
.open(tmpfile.path())
.unwrap()
.write_all(&spliced)
.unwrap();
assert!(id(&spliced, 1 - primary) > id(&spliced, primary));
let probe = probe(tmpfile.path());
let read = reader.begin_read().unwrap();
assert_eq!(
held_ids(&probe),
vec![id(&spliced, primary)],
"the reader adopted a slot whose pages the file never received"
);
drop(read);
let opened = builder.open_read_only(tmpfile.path()).unwrap();
let read = opened.begin_read().unwrap();
assert_eq!(held_ids(&probe), vec![id(&spliced, primary)]);
drop(read);
}
#[test]
fn a_shared_commit_is_two_phase() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let mut write = db.begin_write().unwrap();
write.set_two_phase_commit(false);
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(1, 1).unwrap();
}
write.commit().unwrap();
assert!(
db.mem.used_two_phase_commit(),
"a shared commit published without the flush between the header writes"
);
}
#[test]
fn a_shared_read_refuses_an_unclean_file_with_no_writer() {
use std::io::{Read, Seek, SeekFrom, Write};
let tmpfile = crate::create_tempfile();
drop(create(tmpfile.path(), ConcurrencyMode::MultiWriter));
{
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(tmpfile.path())
.unwrap();
let mut byte = [0u8; 1];
file.seek(SeekFrom::Start(9)).unwrap();
file.read_exact(&mut byte).unwrap();
byte[0] |= 2;
file.seek(SeekFrom::Start(9)).unwrap();
file.write_all(&byte).unwrap();
}
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
assert!(matches!(
builder.open_read_only(tmpfile.path()),
Err(crate::DatabaseError::RepairAborted)
));
}
#[test]
fn an_exclusive_writer_read_does_not_puncture_the_whole_file_lock() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::ExclusiveWriter);
let probe = probe(tmpfile.path());
drop(db.begin_read().unwrap());
assert_eq!(
held_ids(&probe),
SEARCHED.collect::<Vec<_>>(),
"a read transaction punctured the whole-file lock"
);
}
#[test]
fn the_oldest_active_transaction_is_the_lower_of_ours_and_a_peers() {
for mode in [ConcurrencyMode::SingleWriter, ConcurrencyMode::MultiWriter] {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), mode);
let scan = |local| {
let header_lock = db.mem.lock_header_exclusive().unwrap();
db.mem
.oldest_active_transaction(local, &header_lock)
.unwrap()
};
let first = db.mem.get_last_committed_transaction_id().unwrap();
assert_eq!(scan(None), None);
let probe = probe(tmpfile.path());
probe
.lock_shared_range(byte_range(TXN_BASE + first.raw_id()))
.unwrap();
assert_eq!(scan(None), Some(first), "{mode:?}");
let write = db.begin_write().unwrap();
{
let mut table = write.open_table(TABLE).unwrap();
table.insert(1, 1).unwrap();
}
write.commit().unwrap();
let second = db.mem.get_last_committed_transaction_id().unwrap();
assert!(first < second);
assert_eq!(scan(Some(second)), Some(first), "{mode:?}");
probe
.unlock_range(byte_range(TXN_BASE + first.raw_id()))
.unwrap();
assert_eq!(scan(Some(second)), Some(second), "{mode:?}");
}
}
#[test]
fn a_lent_header_lock_covers_syncing_to_a_peers_commit() {
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn open(path: &Path) -> Database {
let mut builder = Database::builder();
builder.set_concurrency_mode(ConcurrencyMode::MultiWriter);
builder.open(path).unwrap()
}
let tmpfile = crate::create_tempfile();
create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let peer = open(tmpfile.path());
let path = tmpfile.path().to_path_buf();
let (opened, wait_for_open) = mpsc::channel();
let (committed, wait_for_commit) = mpsc::channel();
let (synced, wait_for_sync) = mpsc::channel();
let syncing = thread::spawn(move || {
let db = open(&path);
opened.send(()).unwrap();
wait_for_commit.recv().unwrap();
let writer_lock = db.mem.lock_writer().unwrap();
let header_lock = db.mem.lock_header_exclusive().unwrap();
let txn = db
.begin_write_with(Some(&writer_lock), Some(&header_lock))
.unwrap();
let savepoints: Vec<u64> = txn.list_persistent_savepoints().unwrap().collect();
txn.abort().unwrap();
drop(header_lock);
drop(writer_lock);
synced.send(savepoints).unwrap();
});
wait_for_open.recv().unwrap();
let txn = peer.begin_write().unwrap();
let peers_savepoint = txn.persistent_savepoint().unwrap();
txn.commit().unwrap();
drop(peer);
committed.send(()).unwrap();
let savepoints = wait_for_sync
.recv_timeout(Duration::from_secs(10))
.expect("the transaction took the header lock its caller holds");
assert_eq!(savepoints, vec![peers_savepoint]);
syncing.join().unwrap();
}
#[test]
fn a_lent_header_hold_outlives_the_commit() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let header_lock = db.mem.lock_header_exclusive().unwrap();
let txn = db.begin_write_with(None, Some(&header_lock)).unwrap();
txn.open_table(TABLE).unwrap().insert(1, 1).unwrap();
txn.commit_with(Some(&header_lock)).unwrap();
assert!(!probe.try_lock_shared_range(HEADER_LOCK).unwrap());
drop(header_lock);
assert!(probe.try_lock_shared_range(HEADER_LOCK).unwrap());
probe.unlock_range(HEADER_LOCK).unwrap();
}
#[test]
fn a_lent_writer_byte_outlives_the_transaction() {
let tmpfile = crate::create_tempfile();
let db = create(tmpfile.path(), ConcurrencyMode::MultiWriter);
let probe = probe(tmpfile.path());
let writer_lock = db.mem.lock_writer().unwrap();
let txn = db.begin_write_with(Some(&writer_lock), None).unwrap();
txn.abort().unwrap();
assert!(!probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap());
drop(writer_lock);
assert!(probe.try_lock_range(byte_range(WRITER_BYTE)).unwrap());
probe.unlock_range(byte_range(WRITER_BYTE)).unwrap();
}
}