mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-23 03:59:31 +00:00
database: impl trait function bodies for heed
& redb
(#85)
* env: remove `T: Table` for `Env::create_tables()` It creates _all_ tables, not a specific `T: Table` * heed: half impl `Env::open()`, some TODOs * heed: add `HeedTxR{o,w}<'env>` * workspace/cargo: add `parking_lot` * remove `parking_lot` `MappedRwLockGuard` doesn't solve the `returning reference to object owned by function` error when returning heed's lock guard + the tx so we'll be going with `std` * env: add `{EnvInner,TxRoInput,TxRwInput}` and getter `fn()`s * env: fix tx <-> table lifetimes, fix `Env::create_tables()` * heed: impl `DatabaseRo` * heed: impl `DatabaseRw` * database: add `src/backend/${BACKEND}/tests.rs` * heed: impl more of `Env::open()` * redb: fix trait signatures, add `trait ValueGuard` * accommodate `DatabaseRo` bounds for both `{heed,redb}` * fold `get_range()` generic + bounds * add `TxCreator`, add `heed` tests * env: `TxCreator` -> `EnvInner` which doubles as DB/Table opener * database: `DatabaseRw<'tx>` -> `DatabaseRw<'db, 'tx>` * heed: `db_read_write()` test * database: fix `get()` lifetimes, heed: add `db_read_write()` test * database: remove `'env` lifetime from `DatabaseRo<'env, 'tx>` not needed for immutable references * redb: fix new `{Env, EnvInner, DatabaseR{o,w}}` bounds * redb: impl `Database` traits * redb: impl `TxR{o,w}` traits * redb: impl `Env` * redb: open/create tables in `Env::open` * redb: enable tests, add tmp `Storable` printlns * redb: fix alignment issue with `Cow` and `from_bytes_unaligned()` * redb: only allocate bytes when alignment != 1 * heed: remove custom iterator from `range()` * storable: conditionally allocat on misaligned bytes in `from_bytes` * add database guard * database: AccessGuard -> ValueGuard * storable: add `ALIGN` and `from_bytes_unaligned()` * redb: 1 serde type `StorableRedb`, fix impl * cow serde, trait bounds, fix backend's where bounds - Uses Cow for `redb`'s deserialization - Conforms `heed` to use Cow (but not as the actual key/value) - Conforms the `cuprate_database` trait API to use Cow - Adds `ToOwned + Debug` (and permutation) trait bounds - Solves 23098573148968945687345349657398 compiler errors due to aforementioned trait bounds, causing `where` to be everywhere * fix docs, use fully qualified Tx functions for tests * backend: check value guard contains value in test * add `Storable::ALIGN` tests, doc TODOs * add `trait ToOwnedDebug` * traits: `ToOwned + Debug` -> `ToOwnedDebug` * heed: remove `ToOwned` + `Debug` bounds * redb: remove `ToOwned` + `Debug` bounds * add `ValueGuard`, replace signatures, fix `redb` * heed: fix for `ValueGuard` * docs, tests * redb: add `CowRange` for `T::Key` -> `Cow<'_, T::Key>` conversion * separate `config.rs` -> `config/` * ci: combine tests, run both `heed` and `redb` tests * ci: fix workflow * backend: add `resize()` test * ci: remove windows-specific update * ci: remove update + windows default set * backend: add `unreachable` tests, fix docs * trait docs * ci: fix * Update database/src/backend/heed/env.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/heed/env.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/heed/transaction.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/heed/transaction.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/heed/transaction.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/redb/database.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/redb/database.rs Co-authored-by: Boog900 <boog900@tutanota.com> * Update database/src/backend/heed/database.rs Co-authored-by: Boog900 <boog900@tutanota.com> * readme: fix `value_guard.rs` * heed: remove unneeded clippy + fix formatting * heed: create and use `create_table()` in `Env::open()` * redb: create and use `create_table()` in `Env::open()` * redb: remove unneeded clippy * fix clippy, remove `<[u8], [u8]>` docs --------- Co-authored-by: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Co-authored-by: Boog900 <boog900@tutanota.com>
This commit is contained in:
parent
159c8a3b48
commit
8f22d8ab79
31 changed files with 1912 additions and 938 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
@ -96,22 +96,17 @@ jobs:
|
||||||
update: true
|
update: true
|
||||||
install: mingw-w64-x86_64-toolchain mingw-w64-x86_64-boost msys2-runtime-devel git mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
|
install: mingw-w64-x86_64-toolchain mingw-w64-x86_64-boost msys2-runtime-devel git mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
|
||||||
|
|
||||||
- name: Update Rust (UNIX)
|
|
||||||
if: matrix.os != 'windows-latest'
|
|
||||||
run: rustup update
|
|
||||||
|
|
||||||
- name: Switch target (Windows)
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: rustup toolchain install stable-x86_64-pc-windows-gnu -c clippy && rustup set default-host x86_64-pc-windows-gnu && rustup default stable-x86_64-pc-windows-gnu
|
|
||||||
|
|
||||||
- name: Documentation
|
- name: Documentation
|
||||||
run: cargo doc --workspace --all-features
|
run: cargo doc --workspace --all-features
|
||||||
|
|
||||||
- name: Clippy (fail on warnings)
|
- name: Clippy (fail on warnings)
|
||||||
run: cargo clippy --workspace --all-features --all-targets -- -D warnings
|
run: cargo clippy --workspace --all-features --all-targets -- -D warnings
|
||||||
|
|
||||||
|
# HACK: how to test both DB backends that are feature-gated?
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --all-features --workspace
|
run: |
|
||||||
|
cargo test --all-features --workspace
|
||||||
|
cargo test --package cuprate-database --no-default-features --features redb --features service
|
||||||
|
|
||||||
# TODO: upload binaries with `actions/upload-artifact@v3`
|
# TODO: upload binaries with `actions/upload-artifact@v3`
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
|
@ -9,8 +9,8 @@ repository = "https://github.com/Cuprate/cuprate/tree/main/database"
|
||||||
keywords = ["cuprate", "database"]
|
keywords = ["cuprate", "database"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["heed", "service"]
|
default = ["heed", "redb", "service"]
|
||||||
# default = ["redb", "service"] # For testing `redb`.
|
# default = ["redb", "service"]
|
||||||
heed = ["dep:heed"]
|
heed = ["dep:heed"]
|
||||||
redb = ["dep:redb"]
|
redb = ["dep:redb"]
|
||||||
service = ["dep:crossbeam", "dep:tokio", "dep:tower"]
|
service = ["dep:crossbeam", "dep:tokio", "dep:tower"]
|
||||||
|
@ -22,23 +22,14 @@ cfg-if = { workspace = true }
|
||||||
# We only need the `thread` feature if `service` is enabled.
|
# We only need the `thread` feature if `service` is enabled.
|
||||||
# Figure out how to enable features of an already pulled in dependency conditionally.
|
# Figure out how to enable features of an already pulled in dependency conditionally.
|
||||||
cuprate-helper = { path = "../helper", features = ["fs", "thread"] }
|
cuprate-helper = { path = "../helper", features = ["fs", "thread"] }
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
# Needed for database resizes.
|
page_size = { version = "0.6.0" } # Needed for database resizes, they must be a multiple of the OS page size.
|
||||||
# They must be a multiple of the OS page size.
|
thiserror = { workspace = true }
|
||||||
page_size = { version = "0.6.0" }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
|
|
||||||
# `service` feature.
|
# `service` feature.
|
||||||
crossbeam = { workspace = true, features = ["std"], optional = true }
|
crossbeam = { workspace = true, features = ["std"], optional = true }
|
||||||
tokio = { workspace = true, features = ["full"], optional = true }
|
tokio = { workspace = true, features = ["full"], optional = true }
|
||||||
tower = { workspace = true, features = ["full"], optional = true }
|
tower = { workspace = true, features = ["full"], optional = true }
|
||||||
# SOMEDAY: could be used in `service` as
|
|
||||||
# the database mutual exclusive `RwLock`.
|
|
||||||
#
|
|
||||||
# `parking_lot` has a fairness policy unlike `std`,
|
|
||||||
# although for now (and until testing is done),
|
|
||||||
# `std` is fine.
|
|
||||||
# parking_lot = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
# Optional features.
|
# Optional features.
|
||||||
heed = { version = "0.20.0-alpha.9", optional = true }
|
heed = { version = "0.20.0-alpha.9", optional = true }
|
||||||
|
|
|
@ -66,21 +66,23 @@ Note that `lib.rs/mod.rs` files are purely for re-exporting/visibility/lints, an
|
||||||
## `src/`
|
## `src/`
|
||||||
The top-level `src/` files.
|
The top-level `src/` files.
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------------------|---------|
|
|---------------------|---------|
|
||||||
| `config.rs` | Database `Env` configuration
|
| `config.rs` | Database `Env` configuration
|
||||||
| `constants.rs` | General constants used throughout `cuprate-database`
|
| `constants.rs` | General constants used throughout `cuprate-database`
|
||||||
| `database.rs` | Abstracted database; `trait DatabaseR{o,w}`
|
| `database.rs` | Abstracted database; `trait DatabaseR{o,w}`
|
||||||
| `env.rs` | Abstracted database environment; `trait Env`
|
| `env.rs` | Abstracted database environment; `trait Env`
|
||||||
| `error.rs` | Database error types
|
| `error.rs` | Database error types
|
||||||
| `free.rs` | General free functions (related to the database)
|
| `free.rs` | General free functions (related to the database)
|
||||||
| `key.rs` | Abstracted database keys; `trait Key`
|
| `key.rs` | Abstracted database keys; `trait Key`
|
||||||
| `resize.rs` | Database resizing algorithms
|
| `resize.rs` | Database resizing algorithms
|
||||||
| `storable.rs` | Data (de)serialization; `trait Storable`
|
| `storable.rs` | Data (de)serialization; `trait Storable`
|
||||||
| `table.rs` | Database table abstraction; `trait Table`
|
| `table.rs` | Database table abstraction; `trait Table`
|
||||||
| `tables.rs` | All the table definitions used by `cuprate-database`
|
| `tables.rs` | All the table definitions used by `cuprate-database`
|
||||||
| `transaction.rs` | Database transaction abstraction; `trait TxR{o,w}`
|
| `to_owned_debug.rs` | Borrowed/owned data abstraction; `trait ToOwnedDebug`
|
||||||
| `types.rs` | Database table schema types
|
| `transaction.rs` | Database transaction abstraction; `trait TxR{o,w}`
|
||||||
|
| `types.rs` | Database table schema types
|
||||||
|
| `value_guard.rs` | Database value "guard" abstraction; `trait ValueGuard`
|
||||||
|
|
||||||
## `src/ops/`
|
## `src/ops/`
|
||||||
This folder contains the `cupate_database::ops` module.
|
This folder contains the `cupate_database::ops` module.
|
||||||
|
@ -126,9 +128,10 @@ All backends follow the same file structure:
|
||||||
| `database.rs` | Implementation of `trait DatabaseR{o,w}`
|
| `database.rs` | Implementation of `trait DatabaseR{o,w}`
|
||||||
| `env.rs` | Implementation of `trait Env`
|
| `env.rs` | Implementation of `trait Env`
|
||||||
| `error.rs` | Implementation of backend's errors to `cuprate_database`'s error types
|
| `error.rs` | Implementation of backend's errors to `cuprate_database`'s error types
|
||||||
|
| `storable.rs` | Compatibility layer between `cuprate_database::Storable` and backend-specific (de)serialization
|
||||||
|
| `tests.rs` | Tests for the specific backend
|
||||||
| `transaction.rs` | Implementation of `trait TxR{o,w}`
|
| `transaction.rs` | Implementation of `trait TxR{o,w}`
|
||||||
| `types.rs` | Type aliases for long backend-specific types
|
| `types.rs` | Type aliases for long backend-specific types
|
||||||
| `storable.rs` | Compatibility layer between `cuprate_database::Storable` and backend-specific (de)serialization
|
|
||||||
|
|
||||||
# Backends
|
# Backends
|
||||||
`cuprate-database`'s `trait`s abstract over various actual databases.
|
`cuprate-database`'s `trait`s abstract over various actual databases.
|
||||||
|
@ -172,7 +175,7 @@ The default maximum value size is [1012 bytes](https://docs.rs/sanakirja/1.4.1/s
|
||||||
As such, it is not implemented.
|
As such, it is not implemented.
|
||||||
|
|
||||||
## `MDBX`
|
## `MDBX`
|
||||||
[`MDBX`](https://erthink.github.io/libmdbx) was a candidate as a backend, however MDBX deprecated the custom key/value comparison functions, this makes it a bit trickier to implement dup tables. It is also quite similar to the main backend LMDB (of which it was originally a fork of).
|
[`MDBX`](https://erthink.github.io/libmdbx) was a candidate as a backend, however MDBX deprecated the custom key/value comparison functions, this makes it a bit trickier to implement duplicate tables. It is also quite similar to the main backend LMDB (of which it was originally a fork of).
|
||||||
|
|
||||||
As such, it is not implemented (yet).
|
As such, it is not implemented (yet).
|
||||||
|
|
||||||
|
@ -198,4 +201,4 @@ TODO: document disk flushing behavior.
|
||||||
- Backend-specific behavior
|
- Backend-specific behavior
|
||||||
|
|
||||||
# (De)serialization
|
# (De)serialization
|
||||||
TODO: document `Pod` and how databases use (de)serialize objects when storing/fetching, essentially using `<[u8], [u8]>`.
|
TODO: document `Storable` and how databases (de)serialize types when storing/fetching.
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
//! Implementation of `trait Database` for `heed`.
|
//! Implementation of `trait Database` for `heed`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use std::marker::PhantomData;
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
fmt::Debug,
|
||||||
|
ops::RangeBounds,
|
||||||
|
sync::RwLockReadGuard,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::heed::types::HeedDb,
|
backend::heed::{storable::StorableHeed, types::HeedDb},
|
||||||
database::{DatabaseRo, DatabaseRw},
|
database::{DatabaseRo, DatabaseRw},
|
||||||
error::RuntimeError,
|
error::RuntimeError,
|
||||||
table::Table,
|
table::Table,
|
||||||
|
value_guard::ValueGuard,
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Heed Database Wrappers
|
//---------------------------------------------------------------------------------------------------- Heed Database Wrappers
|
||||||
|
@ -26,72 +32,107 @@ use crate::{
|
||||||
/// An opened read-only database associated with a transaction.
|
/// An opened read-only database associated with a transaction.
|
||||||
///
|
///
|
||||||
/// Matches `redb::ReadOnlyTable`.
|
/// Matches `redb::ReadOnlyTable`.
|
||||||
pub(super) struct HeedTableRo<'env, T: Table> {
|
pub(super) struct HeedTableRo<'tx, T: Table> {
|
||||||
/// An already opened database table.
|
/// An already opened database table.
|
||||||
db: HeedDb<T::Key, T::Value>,
|
pub(super) db: HeedDb<T::Key, T::Value>,
|
||||||
/// The associated read-only transaction that opened this table.
|
/// The associated read-only transaction that opened this table.
|
||||||
tx_ro: &'env heed::RoTxn<'env>,
|
pub(super) tx_ro: &'tx heed::RoTxn<'tx>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An opened read/write database associated with a transaction.
|
/// An opened read/write database associated with a transaction.
|
||||||
///
|
///
|
||||||
/// Matches `redb::Table` (read & write).
|
/// Matches `redb::Table` (read & write).
|
||||||
pub(super) struct HeedTableRw<'env, T: Table> {
|
pub(super) struct HeedTableRw<'env, 'tx, T: Table> {
|
||||||
/// TODO
|
/// TODO
|
||||||
db: HeedDb<T::Key, T::Value>,
|
pub(super) db: HeedDb<T::Key, T::Value>,
|
||||||
/// The associated read/write transaction that opened this table.
|
/// The associated read/write transaction that opened this table.
|
||||||
tx_rw: &'env mut heed::RwTxn<'env>,
|
pub(super) tx_rw: &'tx mut heed::RwTxn<'env>,
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Shared functions
|
||||||
|
// FIXME: we cannot just deref `HeedTableRw -> HeedTableRo` and
|
||||||
|
// call the functions since the database is held by value, so
|
||||||
|
// just use these generic functions that both can call instead.
|
||||||
|
|
||||||
|
/// Shared generic `get()` between `HeedTableR{o,w}`.
|
||||||
|
#[inline]
|
||||||
|
fn get<'a, T: Table>(
|
||||||
|
db: &'_ HeedDb<T::Key, T::Value>,
|
||||||
|
tx_ro: &'a heed::RoTxn<'_>,
|
||||||
|
key: &T::Key,
|
||||||
|
) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
db.get(tx_ro, key)?
|
||||||
|
.map(Cow::Borrowed)
|
||||||
|
.ok_or(RuntimeError::KeyNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared generic `get_range()` between `HeedTableR{o,w}`.
|
||||||
|
#[inline]
|
||||||
|
fn get_range<'a, T: Table, Range>(
|
||||||
|
db: &'a HeedDb<T::Key, T::Value>,
|
||||||
|
tx_ro: &'a heed::RoTxn<'_>,
|
||||||
|
range: &'a Range,
|
||||||
|
) -> Result<impl Iterator<Item = Result<impl ValueGuard<T::Value> + 'a, RuntimeError>>, RuntimeError>
|
||||||
|
where
|
||||||
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
|
{
|
||||||
|
Ok(db.range(tx_ro, range)?.map(|res| Ok(Cow::Borrowed(res?.1))))
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRo Impl
|
//---------------------------------------------------------------------------------------------------- DatabaseRo Impl
|
||||||
impl<T: Table> DatabaseRo<T> for HeedTableRo<'_, T> {
|
impl<'tx, T: Table> DatabaseRo<'tx, T> for HeedTableRo<'tx, T> {
|
||||||
fn get(&self, key: &T::Key) -> Result<&T::Value, RuntimeError> {
|
#[inline]
|
||||||
todo!()
|
fn get<'a>(&'a self, key: &'a T::Key) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
get::<T>(&self.db, self.tx_ro, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_range<'a>(
|
#[inline]
|
||||||
|
fn get_range<'a, Range>(
|
||||||
&'a self,
|
&'a self,
|
||||||
key: &'a T::Key,
|
range: &'a Range,
|
||||||
amount: usize,
|
) -> Result<
|
||||||
) -> Result<impl Iterator<Item = &'a T::Value>, RuntimeError>
|
impl Iterator<Item = Result<impl ValueGuard<T::Value> + 'a, RuntimeError>>,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
<T as Table>::Value: 'a,
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
{
|
{
|
||||||
let iter: std::vec::Drain<'_, &T::Value> = todo!();
|
get_range::<T, Range>(&self.db, self.tx_ro, range)
|
||||||
Ok(iter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRw Impl
|
//---------------------------------------------------------------------------------------------------- DatabaseRw Impl
|
||||||
impl<T: Table> DatabaseRo<T> for HeedTableRw<'_, T> {
|
impl<'tx, T: Table> DatabaseRo<'tx, T> for HeedTableRw<'_, 'tx, T> {
|
||||||
fn get(&self, key: &T::Key) -> Result<&T::Value, RuntimeError> {
|
#[inline]
|
||||||
todo!()
|
fn get<'a>(&'a self, key: &'a T::Key) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
get::<T>(&self.db, self.tx_rw, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_range<'a>(
|
#[inline]
|
||||||
|
fn get_range<'a, Range>(
|
||||||
&'a self,
|
&'a self,
|
||||||
key: &'a T::Key,
|
range: &'a Range,
|
||||||
amount: usize,
|
) -> Result<
|
||||||
) -> Result<impl Iterator<Item = &'a T::Value>, RuntimeError>
|
impl Iterator<Item = Result<impl ValueGuard<T::Value> + 'a, RuntimeError>>,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
<T as Table>::Value: 'a,
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
{
|
{
|
||||||
let iter: std::vec::Drain<'_, &T::Value> = todo!();
|
get_range::<T, Range>(&self.db, self.tx_rw, range)
|
||||||
Ok(iter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Table> DatabaseRw<T> for HeedTableRw<'_, T> {
|
impl<'env, 'tx, T: Table> DatabaseRw<'env, 'tx, T> for HeedTableRw<'env, 'tx, T> {
|
||||||
|
#[inline]
|
||||||
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> {
|
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.db.put(self.tx_rw, key, value)?)
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&mut self) -> Result<(), RuntimeError> {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> {
|
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
self.db.delete(self.tx_rw, key)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
//! Implementation of `trait Env` for `heed`.
|
//! Implementation of `trait Env` for `heed`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use std::sync::RwLock;
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
ops::Deref,
|
||||||
|
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||||
|
};
|
||||||
|
|
||||||
|
use heed::{DatabaseOpenOptions, EnvFlags, EnvOpenOptions};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::heed::database::{HeedTableRo, HeedTableRw},
|
backend::heed::{
|
||||||
config::Config,
|
database::{HeedTableRo, HeedTableRw},
|
||||||
|
storable::StorableHeed,
|
||||||
|
types::HeedDb,
|
||||||
|
},
|
||||||
|
config::{Config, SyncMode},
|
||||||
database::{DatabaseRo, DatabaseRw},
|
database::{DatabaseRo, DatabaseRw},
|
||||||
env::Env,
|
env::{Env, EnvInner},
|
||||||
error::{InitError, RuntimeError},
|
error::{InitError, RuntimeError},
|
||||||
resize::ResizeAlgorithm,
|
resize::ResizeAlgorithm,
|
||||||
table::Table,
|
table::Table,
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Env
|
//---------------------------------------------------------------------------------------------------- Consts
|
||||||
|
/// TODO
|
||||||
|
const PANIC_MSG_MISSING_TABLE: &str =
|
||||||
|
"cuprate_database::Env should uphold the invariant that all tables are already created";
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- ConcreteEnv
|
||||||
/// A strongly typed, concrete database environment, backed by `heed`.
|
/// A strongly typed, concrete database environment, backed by `heed`.
|
||||||
pub struct ConcreteEnv {
|
pub struct ConcreteEnv {
|
||||||
/// The actual database environment.
|
/// The actual database environment.
|
||||||
|
@ -44,11 +59,13 @@ pub struct ConcreteEnv {
|
||||||
|
|
||||||
/// The configuration we were opened with
|
/// The configuration we were opened with
|
||||||
/// (and in current use).
|
/// (and in current use).
|
||||||
config: Config,
|
pub(super) config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ConcreteEnv {
|
impl Drop for ConcreteEnv {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
// INVARIANT: drop(ConcreteEnv) must sync.
|
||||||
|
//
|
||||||
// TODO:
|
// TODO:
|
||||||
// "if the environment has the MDB_NOSYNC flag set the flushes will be omitted,
|
// "if the environment has the MDB_NOSYNC flag set the flushes will be omitted,
|
||||||
// and with MDB_MAPASYNC they will be asynchronous."
|
// and with MDB_MAPASYNC they will be asynchronous."
|
||||||
|
@ -57,7 +74,7 @@ impl Drop for ConcreteEnv {
|
||||||
// We need to do `mdb_env_set_flags(&env, MDB_NOSYNC|MDB_ASYNCMAP, 0)`
|
// We need to do `mdb_env_set_flags(&env, MDB_NOSYNC|MDB_ASYNCMAP, 0)`
|
||||||
// to clear the no sync and async flags such that the below `self.sync()`
|
// to clear the no sync and async flags such that the below `self.sync()`
|
||||||
// _actually_ synchronously syncs.
|
// _actually_ synchronously syncs.
|
||||||
if let Err(e) = self.sync() {
|
if let Err(e) = crate::Env::sync(self) {
|
||||||
// TODO: log error?
|
// TODO: log error?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,30 +94,118 @@ impl Drop for ConcreteEnv {
|
||||||
impl Env for ConcreteEnv {
|
impl Env for ConcreteEnv {
|
||||||
const MANUAL_RESIZE: bool = true;
|
const MANUAL_RESIZE: bool = true;
|
||||||
const SYNCS_PER_TX: bool = false;
|
const SYNCS_PER_TX: bool = false;
|
||||||
type TxRo<'env> = heed::RoTxn<'env>;
|
type EnvInner<'env> = RwLockReadGuard<'env, heed::Env>;
|
||||||
type TxRw<'env> = heed::RwTxn<'env>;
|
type TxRo<'tx> = heed::RoTxn<'tx>;
|
||||||
|
type TxRw<'tx> = heed::RwTxn<'tx>;
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
#[inline(never)] // called once.
|
#[inline(never)] // called once.
|
||||||
|
#[allow(clippy::items_after_statements)]
|
||||||
fn open(config: Config) -> Result<Self, InitError> {
|
fn open(config: Config) -> Result<Self, InitError> {
|
||||||
// INVARIANT:
|
|
||||||
// We must open LMDB using `heed::EnvOpenOptions::max_readers`
|
|
||||||
// and input whatever is in `config.reader_threads` or else
|
|
||||||
// LMDB will start throwing errors if there are >126 readers.
|
|
||||||
// <http://www.lmdb.tech/doc/group__mdb.html#gae687966c24b790630be2a41573fe40e2>
|
|
||||||
//
|
|
||||||
// We should also leave reader slots for other processes, e.g. `xmrblocks`.
|
|
||||||
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1372>
|
|
||||||
|
|
||||||
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
||||||
|
|
||||||
todo!()
|
// Map our `Config` sync mode to the LMDB environment flags.
|
||||||
}
|
//
|
||||||
|
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
||||||
|
let flags = match config.sync_mode {
|
||||||
|
SyncMode::Safe => EnvFlags::empty(),
|
||||||
|
SyncMode::Async => EnvFlags::MAP_ASYNC,
|
||||||
|
SyncMode::Fast => EnvFlags::NO_SYNC | EnvFlags::WRITE_MAP | EnvFlags::MAP_ASYNC,
|
||||||
|
// TODO: dynamic syncs are not implemented.
|
||||||
|
SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(),
|
||||||
|
};
|
||||||
|
|
||||||
#[cold]
|
let mut env_open_options = EnvOpenOptions::new();
|
||||||
#[inline(never)] // called once in [`Env::open`]?
|
|
||||||
fn create_tables<T: Table>(&self, tx_rw: &mut Self::TxRw<'_>) -> Result<(), RuntimeError> {
|
// Set the memory map size to
|
||||||
todo!()
|
// (current disk size) + (a bit of leeway)
|
||||||
|
// to account for empty databases where we
|
||||||
|
// need to write same tables.
|
||||||
|
#[allow(clippy::cast_possible_truncation)] // only 64-bit targets
|
||||||
|
let disk_size_bytes = match std::fs::File::open(&config.db_file) {
|
||||||
|
Ok(file) => file.metadata()?.len() as usize,
|
||||||
|
// The database file doesn't exist, 0 bytes.
|
||||||
|
Err(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => 0,
|
||||||
|
Err(io_err) => return Err(io_err.into()),
|
||||||
|
};
|
||||||
|
// Add leeway space.
|
||||||
|
let memory_map_size = crate::resize::fixed_bytes(disk_size_bytes, 1_000_000 /* 1MB */);
|
||||||
|
env_open_options.map_size(memory_map_size.get());
|
||||||
|
|
||||||
|
// Set the max amount of database tables.
|
||||||
|
// We know at compile time how many tables there are.
|
||||||
|
// TODO: ...how many?
|
||||||
|
env_open_options.max_dbs(32);
|
||||||
|
|
||||||
|
// LMDB documentation:
|
||||||
|
// ```
|
||||||
|
// Number of slots in the reader table.
|
||||||
|
// This value was chosen somewhat arbitrarily. 126 readers plus a
|
||||||
|
// couple mutexes fit exactly into 8KB on my development machine.
|
||||||
|
// ```
|
||||||
|
// <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L794-L799>
|
||||||
|
//
|
||||||
|
// So, we're going to be following these rules:
|
||||||
|
// - Use at least 126 reader threads
|
||||||
|
// - Add 16 extra reader threads if <126
|
||||||
|
//
|
||||||
|
// TODO: This behavior is from `monerod`:
|
||||||
|
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
||||||
|
// I believe this could be adjusted percentage-wise so very high
|
||||||
|
// thread PCs can benefit from something like (cuprated + anything that uses the DB in the future).
|
||||||
|
// For now:
|
||||||
|
// - No other program using our DB exists
|
||||||
|
// - Almost no-one has a 126+ thread CPU
|
||||||
|
#[allow(clippy::cast_possible_truncation)] // no-one has `u32::MAX`+ threads
|
||||||
|
let reader_threads = config.reader_threads.as_threads().get() as u32;
|
||||||
|
env_open_options.max_readers(if reader_threads < 110 {
|
||||||
|
126
|
||||||
|
} else {
|
||||||
|
reader_threads + 16
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the environment in the user's PATH.
|
||||||
|
let env = env_open_options.open(config.db_directory())?;
|
||||||
|
|
||||||
|
// TODO: Open/create tables with certain flags
|
||||||
|
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
||||||
|
// `heed` creates the database if it didn't exist.
|
||||||
|
// <https://docs.rs/heed/0.20.0-alpha.9/src/heed/env.rs.html#223-229>
|
||||||
|
use crate::tables::{TestTable, TestTable2};
|
||||||
|
let mut tx_rw = env.write_txn()?;
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
// These wonderful fully qualified trait types are brought
|
||||||
|
// to you by `tower::discover::Discover>::Key` collisions.
|
||||||
|
|
||||||
|
// TODO: Create all tables when schema is done.
|
||||||
|
|
||||||
|
/// Function that creates the tables based off the passed `T: Table`.
|
||||||
|
fn create_table<T: Table>(
|
||||||
|
env: &heed::Env,
|
||||||
|
tx_rw: &mut heed::RwTxn<'_>,
|
||||||
|
) -> Result<(), InitError> {
|
||||||
|
DatabaseOpenOptions::new(env)
|
||||||
|
.name(<T as Table>::NAME)
|
||||||
|
.types::<StorableHeed<<T as Table>::Key>, StorableHeed<<T as Table>::Value>>()
|
||||||
|
.create(tx_rw)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
create_table::<TestTable>(&env, &mut tx_rw)?;
|
||||||
|
create_table::<TestTable2>(&env, &mut tx_rw)?;
|
||||||
|
|
||||||
|
// TODO: Set dupsort and comparison functions for certain tables
|
||||||
|
// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L1324>
|
||||||
|
|
||||||
|
// INVARIANT: this should never return `ResizeNeeded` due to adding
|
||||||
|
// some tables since we added some leeway to the memory map above.
|
||||||
|
tx_rw.commit()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
env: RwLock::new(env),
|
||||||
|
config,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(&self) -> &Config {
|
fn config(&self) -> &Config {
|
||||||
|
@ -108,14 +213,14 @@ impl Env for ConcreteEnv {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync(&self) -> Result<(), RuntimeError> {
|
fn sync(&self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.env.read().unwrap().force_sync()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_map(&self, resize_algorithm: Option<ResizeAlgorithm>) {
|
fn resize_map(&self, resize_algorithm: Option<ResizeAlgorithm>) {
|
||||||
let resize_algorithm = resize_algorithm.unwrap_or_else(|| self.config().resize_algorithm);
|
let resize_algorithm = resize_algorithm.unwrap_or_else(|| self.config().resize_algorithm);
|
||||||
|
|
||||||
let current_size_bytes = self.current_map_size();
|
let current_size_bytes = self.current_map_size();
|
||||||
let new_size_bytes = resize_algorithm.resize(current_size_bytes);
|
let new_size_bytes = resize_algorithm.resize(current_size_bytes).get();
|
||||||
|
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
// Resizing requires that we have
|
// Resizing requires that we have
|
||||||
|
@ -126,44 +231,62 @@ impl Env for ConcreteEnv {
|
||||||
// <http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5>
|
// <http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5>
|
||||||
unsafe {
|
unsafe {
|
||||||
// INVARIANT: `resize()` returns a valid `usize` to resize to.
|
// INVARIANT: `resize()` returns a valid `usize` to resize to.
|
||||||
self.env
|
self.env.write().unwrap().resize(new_size_bytes).unwrap();
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.resize(new_size_bytes.get())
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn current_map_size(&self) -> usize {
|
fn current_map_size(&self) -> usize {
|
||||||
self.env.read().unwrap().info().map_size
|
self.env.read().unwrap().info().map_size
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn tx_ro(&self) -> Result<Self::TxRo<'_>, RuntimeError> {
|
fn env_inner(&self) -> Self::EnvInner<'_> {
|
||||||
todo!()
|
self.env.read().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- EnvInner Impl
|
||||||
|
impl<'env> EnvInner<'env, heed::RoTxn<'env>, heed::RwTxn<'env>> for RwLockReadGuard<'env, heed::Env>
|
||||||
|
where
|
||||||
|
Self: 'env,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn tx_ro(&'env self) -> Result<heed::RoTxn<'env>, RuntimeError> {
|
||||||
|
Ok(self.read_txn()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn tx_rw(&self) -> Result<Self::TxRw<'_>, RuntimeError> {
|
fn tx_rw(&'env self) -> Result<heed::RwTxn<'env>, RuntimeError> {
|
||||||
todo!()
|
Ok(self.write_txn()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn open_db_ro<T: Table>(
|
fn open_db_ro<'tx, T: Table>(
|
||||||
&self,
|
&self,
|
||||||
tx_ro: &Self::TxRo<'_>,
|
tx_ro: &'tx heed::RoTxn<'env>,
|
||||||
) -> Result<impl DatabaseRo<T>, RuntimeError> {
|
) -> Result<impl DatabaseRo<'tx, T>, RuntimeError> {
|
||||||
let tx: HeedTableRo<T> = todo!();
|
// Open up a read-only database using our table's const metadata.
|
||||||
Ok(tx)
|
Ok(HeedTableRo {
|
||||||
|
db: self
|
||||||
|
.open_database(tx_ro, Some(T::NAME))?
|
||||||
|
.expect(PANIC_MSG_MISSING_TABLE),
|
||||||
|
tx_ro,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn open_db_rw<T: Table>(
|
fn open_db_rw<'tx, T: Table>(
|
||||||
&self,
|
&self,
|
||||||
tx_rw: &mut Self::TxRw<'_>,
|
tx_rw: &'tx mut heed::RwTxn<'env>,
|
||||||
) -> Result<impl DatabaseRw<T>, RuntimeError> {
|
) -> Result<impl DatabaseRw<'env, 'tx, T>, RuntimeError> {
|
||||||
let tx: HeedTableRw<T> = todo!();
|
// Open up a read/write database using our table's const metadata.
|
||||||
Ok(tx)
|
Ok(HeedTableRw {
|
||||||
|
db: self
|
||||||
|
.open_database(tx_rw, Some(T::NAME))?
|
||||||
|
.expect(PANIC_MSG_MISSING_TABLE),
|
||||||
|
tx_rw,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! `cuprate_database::Storable` <-> `heed` serde trait compatibility layer.
|
//! `cuprate_database::Storable` <-> `heed` serde trait compatibility layer.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Use
|
//---------------------------------------------------------------------------------------------------- Use
|
||||||
use std::{borrow::Cow, marker::PhantomData};
|
use std::{borrow::Cow, fmt::Debug, marker::PhantomData};
|
||||||
|
|
||||||
use heed::{types::Bytes, BoxedError, BytesDecode, BytesEncode, Database};
|
use heed::{types::Bytes, BoxedError, BytesDecode, BytesEncode, Database};
|
||||||
|
|
||||||
|
@ -12,10 +12,15 @@ use crate::storable::Storable;
|
||||||
/// traits on any type that implements `cuprate_database::Storable`.
|
/// traits on any type that implements `cuprate_database::Storable`.
|
||||||
///
|
///
|
||||||
/// Never actually gets constructed, just used for trait bound translations.
|
/// Never actually gets constructed, just used for trait bound translations.
|
||||||
pub(super) struct StorableHeed<T: Storable + ?Sized>(PhantomData<T>);
|
pub(super) struct StorableHeed<T>(PhantomData<T>)
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized;
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- BytesDecode
|
//---------------------------------------------------------------------------------------------------- BytesDecode
|
||||||
impl<'a, T: Storable + ?Sized + 'a> BytesDecode<'a> for StorableHeed<T> {
|
impl<'a, T> BytesDecode<'a> for StorableHeed<T>
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized + 'a,
|
||||||
|
{
|
||||||
type DItem = &'a T;
|
type DItem = &'a T;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -26,7 +31,10 @@ impl<'a, T: Storable + ?Sized + 'a> BytesDecode<'a> for StorableHeed<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- BytesEncode
|
//---------------------------------------------------------------------------------------------------- BytesEncode
|
||||||
impl<'a, T: Storable + ?Sized + 'a> BytesEncode<'a> for StorableHeed<T> {
|
impl<'a, T> BytesEncode<'a> for StorableHeed<T>
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized + 'a,
|
||||||
|
{
|
||||||
type EItem = T;
|
type EItem = T;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -39,6 +47,8 @@ impl<'a, T: Storable + ?Sized + 'a> BytesEncode<'a> for StorableHeed<T> {
|
||||||
//---------------------------------------------------------------------------------------------------- Tests
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// Each `#[test]` function has a `test()` to:
|
// Each `#[test]` function has a `test()` to:
|
||||||
|
@ -49,7 +59,10 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `BytesEncode::bytes_encode` is accurate.
|
/// Assert `BytesEncode::bytes_encode` is accurate.
|
||||||
fn bytes_encode() {
|
fn bytes_encode() {
|
||||||
fn test<T: Storable + ?Sized>(t: &T, expected: &[u8]) {
|
fn test<T>(t: &T, expected: &[u8])
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized,
|
||||||
|
{
|
||||||
println!("t: {t:?}, expected: {expected:?}");
|
println!("t: {t:?}, expected: {expected:?}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
<StorableHeed::<T> as BytesEncode>::bytes_encode(t).unwrap(),
|
<StorableHeed::<T> as BytesEncode>::bytes_encode(t).unwrap(),
|
||||||
|
@ -76,7 +89,11 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `BytesDecode::bytes_decode` is accurate.
|
/// Assert `BytesDecode::bytes_decode` is accurate.
|
||||||
fn bytes_decode() {
|
fn bytes_decode() {
|
||||||
fn test<T: Storable + ?Sized + PartialEq>(bytes: &[u8], expected: &T) {
|
fn test<T>(bytes: &[u8], expected: &T)
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized + PartialEq + ToOwned + Debug,
|
||||||
|
T::Owned: Debug,
|
||||||
|
{
|
||||||
println!("bytes: {bytes:?}, expected: {expected:?}");
|
println!("bytes: {bytes:?}, expected: {expected:?}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
<StorableHeed::<T> as BytesDecode>::bytes_decode(bytes).unwrap(),
|
<StorableHeed::<T> as BytesDecode>::bytes_decode(bytes).unwrap(),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
//! Implementation of `trait TxRo/TxRw` for `heed`.
|
//! Implementation of `trait TxRo/TxRw` for `heed`.
|
||||||
|
|
||||||
|
use std::{ops::Deref, sync::RwLockReadGuard};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use crate::{
|
use crate::{
|
||||||
error::RuntimeError,
|
error::RuntimeError,
|
||||||
|
@ -9,31 +11,26 @@ use crate::{
|
||||||
//---------------------------------------------------------------------------------------------------- TxRo
|
//---------------------------------------------------------------------------------------------------- TxRo
|
||||||
impl TxRo<'_> for heed::RoTxn<'_> {
|
impl TxRo<'_> for heed::RoTxn<'_> {
|
||||||
fn commit(self) -> Result<(), RuntimeError> {
|
fn commit(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.commit()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TxRw
|
//---------------------------------------------------------------------------------------------------- TxRw
|
||||||
impl TxRo<'_> for heed::RwTxn<'_> {
|
impl TxRo<'_> for heed::RwTxn<'_> {
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn commit(self) -> Result<(), RuntimeError> {
|
fn commit(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.commit()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxRw<'_> for heed::RwTxn<'_> {
|
impl TxRw<'_> for heed::RwTxn<'_> {
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn commit(self) -> Result<(), RuntimeError> {
|
fn commit(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.commit()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO
|
/// This function is infallible.
|
||||||
fn abort(self) {
|
fn abort(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
self.abort();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,6 @@ cfg_if::cfg_if! {
|
||||||
pub use heed::ConcreteEnv;
|
pub use heed::ConcreteEnv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
|
@ -1,62 +1,180 @@
|
||||||
//! Implementation of `trait DatabaseR{o,w}` for `redb`.
|
//! Implementation of `trait DatabaseR{o,w}` for `redb`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use crate::{
|
use std::{
|
||||||
backend::redb::types::{RedbTableRo, RedbTableRw},
|
borrow::{Borrow, Cow},
|
||||||
database::{DatabaseRo, DatabaseRw},
|
fmt::Debug,
|
||||||
error::RuntimeError,
|
marker::PhantomData,
|
||||||
table::Table,
|
ops::{Bound, Deref, RangeBounds},
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRo
|
use crate::{
|
||||||
impl<T: Table> DatabaseRo<T> for RedbTableRo<'_, T::Key, T::Value> {
|
backend::redb::{
|
||||||
fn get(&self, key: &T::Key) -> Result<&T::Value, RuntimeError> {
|
storable::StorableRedb,
|
||||||
todo!()
|
types::{RedbTableRo, RedbTableRw},
|
||||||
|
},
|
||||||
|
database::{DatabaseRo, DatabaseRw},
|
||||||
|
error::RuntimeError,
|
||||||
|
storable::Storable,
|
||||||
|
table::Table,
|
||||||
|
value_guard::ValueGuard,
|
||||||
|
ToOwnedDebug,
|
||||||
|
};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Shared functions
|
||||||
|
// FIXME: we cannot just deref `RedbTableRw -> RedbTableRo` and
|
||||||
|
// call the functions since the database is held by value, so
|
||||||
|
// just use these generic functions that both can call instead.
|
||||||
|
|
||||||
|
/// Shared generic `get()` between `RedbTableR{o,w}`.
|
||||||
|
#[inline]
|
||||||
|
fn get<'a, T: Table + 'static>(
|
||||||
|
db: &'a impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>,
|
||||||
|
key: &'a T::Key,
|
||||||
|
) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
db.get(Cow::Borrowed(key))?.ok_or(RuntimeError::KeyNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared generic `get_range()` between `RedbTableR{o,w}`.
|
||||||
|
#[inline]
|
||||||
|
fn get_range<'a, T: Table, Range>(
|
||||||
|
db: &'a impl redb::ReadableTable<StorableRedb<T::Key>, StorableRedb<T::Value>>,
|
||||||
|
range: &'a Range,
|
||||||
|
) -> Result<
|
||||||
|
impl Iterator<Item = Result<redb::AccessGuard<'a, StorableRedb<T::Value>>, RuntimeError>> + 'a,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
|
{
|
||||||
|
/// HACK: `redb` sees the database's key type as `Cow<'_, T::Key>`,
|
||||||
|
/// not `T::Key` directly like `heed` does. As such, it wants the
|
||||||
|
/// range to be over `Cow<'_, T::Key>`, not `T::Key` directly.
|
||||||
|
///
|
||||||
|
/// If `DatabaseRo` were to want `Cow<'_, T::Key>` as input in `get()`,
|
||||||
|
/// `get_range()`, it would complicate the API:
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// // This would be needed...
|
||||||
|
/// let range = Cow::Owned(0)..Cow::Owned(1);
|
||||||
|
/// // ...instead of the more obvious
|
||||||
|
/// let range = 0..1;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// As such, `DatabaseRo` only wants `RangeBounds<T::Key>` and
|
||||||
|
/// we create a compatibility struct here, essentially converting
|
||||||
|
/// this functions input:
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// RangeBound<T::Key>
|
||||||
|
/// ```
|
||||||
|
/// into `redb`'s desired:
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// RangeBound<Cow<'_, T::Key>>
|
||||||
|
/// ```
|
||||||
|
struct CowRange<'a, K>
|
||||||
|
where
|
||||||
|
K: ToOwnedDebug,
|
||||||
|
{
|
||||||
|
/// The start bound of `Range`.
|
||||||
|
start_bound: Bound<Cow<'a, K>>,
|
||||||
|
/// The end bound of `Range`.
|
||||||
|
end_bound: Bound<Cow<'a, K>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_range<'a>(
|
/// This impl forwards our `T::Key` to be wrapped in a Cow.
|
||||||
&'a self,
|
impl<'a, K> RangeBounds<Cow<'a, K>> for CowRange<'a, K>
|
||||||
key: &'a T::Key,
|
|
||||||
amount: usize,
|
|
||||||
) -> Result<impl Iterator<Item = &'a T::Value>, RuntimeError>
|
|
||||||
where
|
where
|
||||||
<T as Table>::Value: 'a,
|
K: ToOwnedDebug,
|
||||||
{
|
{
|
||||||
let iter: std::vec::Drain<'_, &T::Value> = todo!();
|
fn start_bound(&self) -> Bound<&Cow<'a, K>> {
|
||||||
Ok(iter)
|
self.start_bound.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_bound(&self) -> Bound<&Cow<'a, K>> {
|
||||||
|
self.end_bound.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_bound = match range.start_bound() {
|
||||||
|
Bound::Included(t) => Bound::Included(Cow::Borrowed(t)),
|
||||||
|
Bound::Excluded(t) => Bound::Excluded(Cow::Borrowed(t)),
|
||||||
|
Bound::Unbounded => Bound::Unbounded,
|
||||||
|
};
|
||||||
|
let end_bound = match range.end_bound() {
|
||||||
|
Bound::Included(t) => Bound::Included(Cow::Borrowed(t)),
|
||||||
|
Bound::Excluded(t) => Bound::Excluded(Cow::Borrowed(t)),
|
||||||
|
Bound::Unbounded => Bound::Unbounded,
|
||||||
|
};
|
||||||
|
let range = CowRange {
|
||||||
|
start_bound,
|
||||||
|
end_bound,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(db.range(range)?.map(|result| {
|
||||||
|
let (_key, value_guard) = result?;
|
||||||
|
Ok(value_guard)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- DatabaseRo
|
||||||
|
impl<'tx, T: Table + 'static> DatabaseRo<'tx, T> for RedbTableRo<'tx, T::Key, T::Value> {
|
||||||
|
#[inline]
|
||||||
|
fn get<'a>(&'a self, key: &'a T::Key) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
get::<T>(self, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_range<'a, Range>(
|
||||||
|
&'a self,
|
||||||
|
range: &'a Range,
|
||||||
|
) -> Result<
|
||||||
|
impl Iterator<Item = Result<impl ValueGuard<T::Value>, RuntimeError>> + 'a,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
|
{
|
||||||
|
get_range::<T, Range>(self, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRw
|
//---------------------------------------------------------------------------------------------------- DatabaseRw
|
||||||
impl<T: Table> DatabaseRo<T> for RedbTableRw<'_, '_, T::Key, T::Value> {
|
impl<'tx, T: Table + 'static> DatabaseRo<'tx, T> for RedbTableRw<'_, 'tx, T::Key, T::Value> {
|
||||||
fn get(&self, key: &T::Key) -> Result<&T::Value, RuntimeError> {
|
#[inline]
|
||||||
todo!()
|
fn get<'a>(&'a self, key: &'a T::Key) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError> {
|
||||||
|
get::<T>(self, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_range<'a>(
|
#[inline]
|
||||||
|
fn get_range<'a, Range>(
|
||||||
&'a self,
|
&'a self,
|
||||||
key: &'a T::Key,
|
range: &'a Range,
|
||||||
amount: usize,
|
) -> Result<
|
||||||
) -> Result<impl Iterator<Item = &'a T::Value>, RuntimeError>
|
impl Iterator<Item = Result<impl ValueGuard<T::Value>, RuntimeError>> + 'a,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
<T as Table>::Value: 'a,
|
Range: RangeBounds<T::Key> + 'a,
|
||||||
{
|
{
|
||||||
let iter: std::vec::Drain<'_, &T::Value> = todo!();
|
get_range::<T, Range>(self, range)
|
||||||
Ok(iter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Table> DatabaseRw<T> for RedbTableRw<'_, '_, T::Key, T::Value> {
|
impl<'env, 'tx, T: Table + 'static> DatabaseRw<'env, 'tx, T>
|
||||||
|
for RedbTableRw<'env, 'tx, T::Key, T::Value>
|
||||||
|
{
|
||||||
|
// `redb` returns the value after `insert()/remove()`
|
||||||
|
// we end with Ok(()) instead.
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> {
|
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
self.insert(Cow::Borrowed(key), Cow::Borrowed(value))?;
|
||||||
}
|
Ok(())
|
||||||
|
|
||||||
fn clear(&mut self) -> Result<(), RuntimeError> {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> {
|
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
self.remove(Cow::Borrowed(key))?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
//! Implementation of `trait Env` for `redb`.
|
//! Implementation of `trait Env` for `redb`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{fmt::Debug, ops::Deref, path::Path, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::redb::types::{RedbTableRo, RedbTableRw},
|
backend::redb::{
|
||||||
|
storable::StorableRedb,
|
||||||
|
types::{RedbTableRo, RedbTableRw},
|
||||||
|
},
|
||||||
config::{Config, SyncMode},
|
config::{Config, SyncMode},
|
||||||
database::{DatabaseRo, DatabaseRw},
|
database::{DatabaseRo, DatabaseRw},
|
||||||
env::Env,
|
env::{Env, EnvInner},
|
||||||
error::{InitError, RuntimeError},
|
error::{InitError, RuntimeError},
|
||||||
table::Table,
|
table::Table,
|
||||||
|
TxRw,
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- ConcreteEnv
|
//---------------------------------------------------------------------------------------------------- ConcreteEnv
|
||||||
|
@ -30,6 +34,7 @@ pub struct ConcreteEnv {
|
||||||
|
|
||||||
impl Drop for ConcreteEnv {
|
impl Drop for ConcreteEnv {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
// INVARIANT: drop(ConcreteEnv) must sync.
|
||||||
if let Err(e) = self.sync() {
|
if let Err(e) = self.sync() {
|
||||||
// TODO: log error?
|
// TODO: log error?
|
||||||
}
|
}
|
||||||
|
@ -42,12 +47,13 @@ impl Drop for ConcreteEnv {
|
||||||
impl Env for ConcreteEnv {
|
impl Env for ConcreteEnv {
|
||||||
const MANUAL_RESIZE: bool = false;
|
const MANUAL_RESIZE: bool = false;
|
||||||
const SYNCS_PER_TX: bool = false;
|
const SYNCS_PER_TX: bool = false;
|
||||||
|
type EnvInner<'env> = (&'env redb::Database, redb::Durability);
|
||||||
type TxRo<'env> = redb::ReadTransaction<'env>;
|
type TxRo<'tx> = redb::ReadTransaction<'tx>;
|
||||||
type TxRw<'env> = redb::WriteTransaction<'env>;
|
type TxRw<'tx> = redb::WriteTransaction<'tx>;
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
#[inline(never)] // called once.
|
#[inline(never)] // called once.
|
||||||
|
#[allow(clippy::items_after_statements)]
|
||||||
fn open(config: Config) -> Result<Self, InitError> {
|
fn open(config: Config) -> Result<Self, InitError> {
|
||||||
// TODO: dynamic syncs are not implemented.
|
// TODO: dynamic syncs are not implemented.
|
||||||
let durability = match config.sync_mode {
|
let durability = match config.sync_mode {
|
||||||
|
@ -61,13 +67,57 @@ impl Env for ConcreteEnv {
|
||||||
SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(),
|
SyncMode::FastThenSafe | SyncMode::Threshold(_) => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
todo!()
|
let env_builder = redb::Builder::new();
|
||||||
}
|
|
||||||
|
|
||||||
#[cold]
|
// TODO: we can set cache sizes with:
|
||||||
#[inline(never)] // called once in [`Env::open`]?`
|
// env_builder.set_cache(bytes);
|
||||||
fn create_tables<T: Table>(&self, tx_rw: &mut Self::TxRw<'_>) -> Result<(), RuntimeError> {
|
|
||||||
todo!()
|
// Open the database file, create if needed.
|
||||||
|
let db_file = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(config.db_file())?;
|
||||||
|
let mut env = env_builder.create_file(db_file)?;
|
||||||
|
|
||||||
|
// Create all database tables.
|
||||||
|
// `redb` creates tables if they don't exist.
|
||||||
|
// <https://docs.rs/redb/latest/redb/struct.WriteTransaction.html#method.open_table>
|
||||||
|
use crate::tables::{TestTable, TestTable2};
|
||||||
|
let tx_rw = env.begin_write()?;
|
||||||
|
|
||||||
|
// FIXME:
|
||||||
|
// These wonderful fully qualified trait types are brought
|
||||||
|
// to you by `tower::discover::Discover>::Key` collisions.
|
||||||
|
|
||||||
|
// TODO: Create all tables when schema is done.
|
||||||
|
|
||||||
|
/// Function that creates the tables based off the passed `T: Table`.
|
||||||
|
fn create_table<T: Table>(tx_rw: &redb::WriteTransaction<'_>) -> Result<(), InitError> {
|
||||||
|
let table: redb::TableDefinition<
|
||||||
|
'static,
|
||||||
|
StorableRedb<<T as Table>::Key>,
|
||||||
|
StorableRedb<<T as Table>::Value>,
|
||||||
|
> = redb::TableDefinition::new(<T as Table>::NAME);
|
||||||
|
|
||||||
|
// `redb` creates tables on open if not already created.
|
||||||
|
tx_rw.open_table(table)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
create_table::<TestTable>(&tx_rw)?;
|
||||||
|
create_table::<TestTable2>(&tx_rw)?;
|
||||||
|
tx_rw.commit()?;
|
||||||
|
|
||||||
|
// Check for file integrity.
|
||||||
|
// TODO: should we do this? is it slow?
|
||||||
|
env.check_integrity()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
env,
|
||||||
|
config,
|
||||||
|
durability,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(&self) -> &Config {
|
fn config(&self) -> &Config {
|
||||||
|
@ -75,41 +125,65 @@ impl Env for ConcreteEnv {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync(&self) -> Result<(), RuntimeError> {
|
fn sync(&self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
// `redb`'s syncs are tied with write transactions,
|
||||||
|
// so just create one, don't do anything and commit.
|
||||||
|
let mut tx_rw = self.env.begin_write()?;
|
||||||
|
tx_rw.set_durability(redb::Durability::Paranoid);
|
||||||
|
TxRw::commit(tx_rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_inner(&self) -> Self::EnvInner<'_> {
|
||||||
|
(&self.env, self.durability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- EnvInner Impl
|
||||||
|
impl<'env> EnvInner<'env, redb::ReadTransaction<'env>, redb::WriteTransaction<'env>>
|
||||||
|
for (&'env redb::Database, redb::Durability)
|
||||||
|
where
|
||||||
|
Self: 'env,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn tx_ro(&'env self) -> Result<redb::ReadTransaction<'env>, RuntimeError> {
|
||||||
|
Ok(self.0.begin_read()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn tx_ro(&self) -> Result<Self::TxRo<'_>, RuntimeError> {
|
fn tx_rw(&'env self) -> Result<redb::WriteTransaction<'env>, RuntimeError> {
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn tx_rw(&self) -> Result<Self::TxRw<'_>, RuntimeError> {
|
|
||||||
// `redb` has sync modes on the TX level, unlike heed,
|
// `redb` has sync modes on the TX level, unlike heed,
|
||||||
// which sets it at the Environment level.
|
// which sets it at the Environment level.
|
||||||
//
|
//
|
||||||
// So, set the durability here before returning the TX.
|
// So, set the durability here before returning the TX.
|
||||||
let mut tx_rw = self.env.begin_write()?;
|
let mut tx_rw = self.0.begin_write()?;
|
||||||
tx_rw.set_durability(self.durability);
|
tx_rw.set_durability(self.1);
|
||||||
Ok(tx_rw)
|
Ok(tx_rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn open_db_ro<T: Table>(
|
fn open_db_ro<'tx, T: Table>(
|
||||||
&self,
|
&self,
|
||||||
tx_ro: &Self::TxRo<'_>,
|
tx_ro: &'tx redb::ReadTransaction<'env>,
|
||||||
) -> Result<impl DatabaseRo<T>, RuntimeError> {
|
) -> Result<impl DatabaseRo<'tx, T>, RuntimeError> {
|
||||||
let tx: RedbTableRo<'_, T::Key, T::Value> = todo!();
|
// Open up a read-only database using our `T: Table`'s const metadata.
|
||||||
Ok(tx)
|
let table: redb::TableDefinition<'static, StorableRedb<T::Key>, StorableRedb<T::Value>> =
|
||||||
|
redb::TableDefinition::new(T::NAME);
|
||||||
|
|
||||||
|
// INVARIANT: Our `?` error conversion will panic if the table does not exist.
|
||||||
|
Ok(tx_ro.open_table(table)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn open_db_rw<T: Table>(
|
fn open_db_rw<'tx, T: Table>(
|
||||||
&self,
|
&self,
|
||||||
tx_rw: &mut Self::TxRw<'_>,
|
tx_rw: &'tx mut redb::WriteTransaction<'env>,
|
||||||
) -> Result<impl DatabaseRw<T>, RuntimeError> {
|
) -> Result<impl DatabaseRw<'env, 'tx, T>, RuntimeError> {
|
||||||
let tx: RedbTableRw<'_, '_, T::Key, T::Value> = todo!();
|
// Open up a read/write database using our `T: Table`'s const metadata.
|
||||||
Ok(tx)
|
let table: redb::TableDefinition<'static, StorableRedb<T::Key>, StorableRedb<T::Value>> =
|
||||||
|
redb::TableDefinition::new(T::NAME);
|
||||||
|
|
||||||
|
// `redb` creates tables if they don't exist, so this should never panic.
|
||||||
|
// <https://docs.rs/redb/latest/redb/struct.WriteTransaction.html#method.open_table>
|
||||||
|
Ok(tx_rw.open_table(table)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,13 @@
|
||||||
//! `redb`'s errors are `#[non_exhaustive]`...
|
//! `redb`'s errors are `#[non_exhaustive]`...
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use crate::constants::DATABASE_CORRUPT_MSG;
|
use crate::{
|
||||||
|
constants::DATABASE_CORRUPT_MSG,
|
||||||
|
error::{InitError, RuntimeError},
|
||||||
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseError
|
//---------------------------------------------------------------------------------------------------- InitError
|
||||||
impl From<redb::DatabaseError> for crate::InitError {
|
impl From<redb::DatabaseError> for InitError {
|
||||||
/// Created by `redb` in:
|
/// Created by `redb` in:
|
||||||
/// - [`redb::Database::open`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.open).
|
/// - [`redb::Database::open`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.open).
|
||||||
fn from(error: redb::DatabaseError) -> Self {
|
fn from(error: redb::DatabaseError) -> Self {
|
||||||
|
@ -34,9 +37,67 @@ impl From<redb::DatabaseError> for crate::InitError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TransactionError
|
impl From<redb::StorageError> for InitError {
|
||||||
|
/// Created by `redb` in:
|
||||||
|
/// - [`redb::Database::open`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.check_integrity)
|
||||||
|
fn from(error: redb::StorageError) -> Self {
|
||||||
|
use redb::StorageError as E;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
E::Io(e) => Self::Io(e),
|
||||||
|
E::Corrupted(s) => Self::Corrupt,
|
||||||
|
// HACK: Handle new errors as `redb` adds them.
|
||||||
|
_ => Self::Unknown(Box::new(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<redb::TransactionError> for InitError {
|
||||||
|
/// Created by `redb` in:
|
||||||
|
/// - [`redb::Database::begin_write`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_write)
|
||||||
|
fn from(error: redb::TransactionError) -> Self {
|
||||||
|
use redb::StorageError as E;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
redb::TransactionError::Storage(error) => error.into(),
|
||||||
|
// HACK: Handle new errors as `redb` adds them.
|
||||||
|
_ => Self::Unknown(Box::new(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<redb::TableError> for InitError {
|
||||||
|
/// Created by `redb` in:
|
||||||
|
/// - [`redb::WriteTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.open_table)
|
||||||
|
fn from(error: redb::TableError) -> Self {
|
||||||
|
use redb::StorageError as E2;
|
||||||
|
use redb::TableError as E;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
E::Storage(error) => error.into(),
|
||||||
|
// HACK: Handle new errors as `redb` adds them.
|
||||||
|
_ => Self::Unknown(Box::new(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<redb::CommitError> for InitError {
|
||||||
|
/// Created by `redb` in:
|
||||||
|
/// - [`redb::WriteTransaction::commit`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.commit)
|
||||||
|
fn from(error: redb::CommitError) -> Self {
|
||||||
|
use redb::StorageError as E;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
redb::CommitError::Storage(error) => error.into(),
|
||||||
|
// HACK: Handle new errors as `redb` adds them.
|
||||||
|
_ => Self::Unknown(Box::new(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- RuntimeError
|
||||||
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
||||||
impl From<redb::TransactionError> for crate::RuntimeError {
|
impl From<redb::TransactionError> for RuntimeError {
|
||||||
/// Created by `redb` in:
|
/// Created by `redb` in:
|
||||||
/// - [`redb::Database::begin_write`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_write)
|
/// - [`redb::Database::begin_write`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_write)
|
||||||
/// - [`redb::Database::begin_read`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_read)
|
/// - [`redb::Database::begin_read`](https://docs.rs/redb/1.5.0/redb/struct.Database.html#method.begin_read)
|
||||||
|
@ -52,9 +113,24 @@ impl From<redb::TransactionError> for crate::RuntimeError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TableError
|
|
||||||
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
||||||
impl From<redb::TableError> for crate::RuntimeError {
|
impl From<redb::CommitError> for RuntimeError {
|
||||||
|
/// Created by `redb` in:
|
||||||
|
/// - [`redb::WriteTransaction::commit`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.commit)
|
||||||
|
fn from(error: redb::CommitError) -> Self {
|
||||||
|
use redb::StorageError as E;
|
||||||
|
|
||||||
|
match error {
|
||||||
|
redb::CommitError::Storage(error) => error.into(),
|
||||||
|
|
||||||
|
// HACK: Handle new errors as `redb` adds them.
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
||||||
|
impl From<redb::TableError> for RuntimeError {
|
||||||
/// Created by `redb` in:
|
/// Created by `redb` in:
|
||||||
/// - [`redb::WriteTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.open_table)
|
/// - [`redb::WriteTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.WriteTransaction.html#method.open_table)
|
||||||
/// - [`redb::ReadTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.ReadTransaction.html#method.open_table)
|
/// - [`redb::ReadTransaction::open_table`](https://docs.rs/redb/1.5.0/redb/struct.ReadTransaction.html#method.open_table)
|
||||||
|
@ -79,9 +155,8 @@ impl From<redb::TableError> for crate::RuntimeError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- StorageError
|
|
||||||
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
#[allow(clippy::fallible_impl_from)] // We need to panic sometimes.
|
||||||
impl From<redb::StorageError> for crate::RuntimeError {
|
impl From<redb::StorageError> for RuntimeError {
|
||||||
/// Created by `redb` in:
|
/// Created by `redb` in:
|
||||||
/// - [`redb::Table`](https://docs.rs/redb/1.5.0/redb/struct.Table.html) functions
|
/// - [`redb::Table`](https://docs.rs/redb/1.5.0/redb/struct.Table.html) functions
|
||||||
/// - [`redb::ReadOnlyTable`](https://docs.rs/redb/1.5.0/redb/struct.ReadOnlyTable.html) functions
|
/// - [`redb::ReadOnlyTable`](https://docs.rs/redb/1.5.0/redb/struct.ReadOnlyTable.html) functions
|
||||||
|
|
|
@ -1,23 +1,42 @@
|
||||||
//! `cuprate_database::Storable` <-> `redb` serde trait compatibility layer.
|
//! `cuprate_database::Storable` <-> `redb` serde trait compatibility layer.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Use
|
//---------------------------------------------------------------------------------------------------- Use
|
||||||
use std::{any::Any, borrow::Cow, cmp::Ordering, marker::PhantomData};
|
use std::{any::Any, borrow::Cow, cmp::Ordering, fmt::Debug, marker::PhantomData};
|
||||||
|
|
||||||
use redb::{RedbKey, RedbValue, TypeName};
|
use redb::{RedbKey, RedbValue, TypeName};
|
||||||
|
|
||||||
use crate::{key::Key, storable::Storable};
|
use crate::{key::Key, storable::Storable};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- StorableRedb
|
//---------------------------------------------------------------------------------------------------- StorableRedb
|
||||||
/// The glue struct that implements `redb`'s (de)serialization
|
/// The glue structs that implements `redb`'s (de)serialization
|
||||||
/// traits on any type that implements `cuprate_database::Key`.
|
/// traits on any type that implements `cuprate_database::Key`.
|
||||||
///
|
///
|
||||||
/// Never actually gets constructed, just used for trait bound translations.
|
/// Never actually get constructed, just used for trait bound translations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) struct StorableRedb<T: Storable + ?Sized>(PhantomData<T>);
|
pub(super) struct StorableRedb<T>(PhantomData<T>)
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized;
|
||||||
|
|
||||||
|
impl<T: Storable> crate::value_guard::ValueGuard<T> for redb::AccessGuard<'_, StorableRedb<T>> {
|
||||||
|
#[inline]
|
||||||
|
fn unguard(&self) -> Cow<'_, T> {
|
||||||
|
self.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Storable> crate::value_guard::ValueGuard<T> for &redb::AccessGuard<'_, StorableRedb<T>> {
|
||||||
|
#[inline]
|
||||||
|
fn unguard(&self) -> Cow<'_, T> {
|
||||||
|
self.value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- RedbKey
|
//---------------------------------------------------------------------------------------------------- RedbKey
|
||||||
// If `Key` is also implemented, this can act as a `RedbKey`.
|
// If `Key` is also implemented, this can act as a `RedbKey`.
|
||||||
impl<T: Key + ?Sized> RedbKey for StorableRedb<T> {
|
impl<T> RedbKey for StorableRedb<T>
|
||||||
|
where
|
||||||
|
T: Key,
|
||||||
|
{
|
||||||
#[inline]
|
#[inline]
|
||||||
fn compare(left: &[u8], right: &[u8]) -> Ordering {
|
fn compare(left: &[u8], right: &[u8]) -> Ordering {
|
||||||
<T as Key>::compare(left, right)
|
<T as Key>::compare(left, right)
|
||||||
|
@ -25,8 +44,11 @@ impl<T: Key + ?Sized> RedbKey for StorableRedb<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- RedbValue
|
//---------------------------------------------------------------------------------------------------- RedbValue
|
||||||
impl<T: Storable + ?Sized> RedbValue for StorableRedb<T> {
|
impl<T> RedbValue for StorableRedb<T>
|
||||||
type SelfType<'a> = &'a T where Self: 'a;
|
where
|
||||||
|
T: Storable + ?Sized,
|
||||||
|
{
|
||||||
|
type SelfType<'a> = Cow<'a, T> where Self: 'a;
|
||||||
type AsBytes<'a> = &'a [u8] where Self: 'a;
|
type AsBytes<'a> = &'a [u8] where Self: 'a;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -35,11 +57,18 @@ impl<T: Storable + ?Sized> RedbValue for StorableRedb<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_bytes<'a>(data: &'a [u8]) -> &'a T
|
fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
|
||||||
where
|
where
|
||||||
Self: 'a,
|
Self: 'a,
|
||||||
{
|
{
|
||||||
<T as Storable>::from_bytes(data)
|
// Use the bytes directly if possible...
|
||||||
|
if T::ALIGN == 1 {
|
||||||
|
Cow::Borrowed(<T as Storable>::from_bytes(data))
|
||||||
|
// ...else, make sure the bytes are aligned
|
||||||
|
// when casting by allocating a new buffer.
|
||||||
|
} else {
|
||||||
|
<T as Storable>::from_bytes_unaligned(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -47,7 +76,7 @@ impl<T: Storable + ?Sized> RedbValue for StorableRedb<T> {
|
||||||
where
|
where
|
||||||
Self: 'a + 'b,
|
Self: 'a + 'b,
|
||||||
{
|
{
|
||||||
<T as Storable>::as_bytes(value)
|
<T as Storable>::as_bytes(value.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -70,12 +99,15 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `RedbKey::compare` works for `StorableRedb`.
|
/// Assert `RedbKey::compare` works for `StorableRedb`.
|
||||||
fn compare() {
|
fn compare() {
|
||||||
fn test<T: Key>(left: T, right: T, expected: Ordering) {
|
fn test<T>(left: T, right: T, expected: Ordering)
|
||||||
|
where
|
||||||
|
T: Key,
|
||||||
|
{
|
||||||
println!("left: {left:?}, right: {right:?}, expected: {expected:?}");
|
println!("left: {left:?}, right: {right:?}, expected: {expected:?}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
<StorableRedb::<T> as RedbKey>::compare(
|
<StorableRedb::<T> as RedbKey>::compare(
|
||||||
<StorableRedb::<T> as RedbValue>::as_bytes(&&left),
|
<StorableRedb::<T> as RedbValue>::as_bytes(&Cow::Borrowed(&left)),
|
||||||
<StorableRedb::<T> as RedbValue>::as_bytes(&&right)
|
<StorableRedb::<T> as RedbValue>::as_bytes(&Cow::Borrowed(&right))
|
||||||
),
|
),
|
||||||
expected
|
expected
|
||||||
);
|
);
|
||||||
|
@ -90,7 +122,10 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `RedbKey::fixed_width` is accurate.
|
/// Assert `RedbKey::fixed_width` is accurate.
|
||||||
fn fixed_width() {
|
fn fixed_width() {
|
||||||
fn test<T: Storable + ?Sized>(expected: Option<usize>) {
|
fn test<T>(expected: Option<usize>)
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized,
|
||||||
|
{
|
||||||
assert_eq!(<StorableRedb::<T> as RedbValue>::fixed_width(), expected);
|
assert_eq!(<StorableRedb::<T> as RedbValue>::fixed_width(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +148,15 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `RedbKey::as_bytes` is accurate.
|
/// Assert `RedbKey::as_bytes` is accurate.
|
||||||
fn as_bytes() {
|
fn as_bytes() {
|
||||||
fn test<T: Storable + ?Sized>(t: &T, expected: &[u8]) {
|
fn test<T>(t: &T, expected: &[u8])
|
||||||
|
where
|
||||||
|
T: Storable + ?Sized,
|
||||||
|
{
|
||||||
println!("t: {t:?}, expected: {expected:?}");
|
println!("t: {t:?}, expected: {expected:?}");
|
||||||
assert_eq!(<StorableRedb::<T> as RedbValue>::as_bytes(&t), expected);
|
assert_eq!(
|
||||||
|
<StorableRedb::<T> as RedbValue>::as_bytes(&Cow::Borrowed(t)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test::<()>(&(), &[]);
|
test::<()>(&(), &[]);
|
||||||
|
@ -137,11 +178,14 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
/// Assert `RedbKey::from_bytes` is accurate.
|
/// Assert `RedbKey::from_bytes` is accurate.
|
||||||
fn from_bytes() {
|
fn from_bytes() {
|
||||||
fn test<T: Storable + ?Sized + PartialEq>(bytes: &[u8], expected: &T) {
|
fn test<T>(bytes: &[u8], expected: &T)
|
||||||
|
where
|
||||||
|
T: Storable + PartialEq + ?Sized,
|
||||||
|
{
|
||||||
println!("bytes: {bytes:?}, expected: {expected:?}");
|
println!("bytes: {bytes:?}, expected: {expected:?}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
<StorableRedb::<T> as RedbValue>::from_bytes(bytes),
|
<StorableRedb::<T> as RedbValue>::from_bytes(bytes),
|
||||||
expected
|
Cow::Borrowed(expected)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,22 @@ use crate::{
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TxRo
|
//---------------------------------------------------------------------------------------------------- TxRo
|
||||||
impl TxRo<'_> for redb::ReadTransaction<'_> {
|
impl TxRo<'_> for redb::ReadTransaction<'_> {
|
||||||
|
/// This function is infallible.
|
||||||
fn commit(self) -> Result<(), RuntimeError> {
|
fn commit(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
// `redb`'s read transactions cleanup in their `drop()`, there is no `commit()`.
|
||||||
|
// https://docs.rs/redb/latest/src/redb/transactions.rs.html#1258-1265
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TxRw
|
//---------------------------------------------------------------------------------------------------- TxRw
|
||||||
impl TxRw<'_> for redb::WriteTransaction<'_> {
|
impl TxRw<'_> for redb::WriteTransaction<'_> {
|
||||||
fn commit(self) -> Result<(), RuntimeError> {
|
fn commit(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.commit()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abort(self) {
|
fn abort(self) -> Result<(), RuntimeError> {
|
||||||
todo!()
|
Ok(self.abort()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
192
database/src/backend/tests.rs
Normal file
192
database/src/backend/tests.rs
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
//! Tests for `cuprate_database`'s backends.
|
||||||
|
//!
|
||||||
|
//! These tests are fully trait-based, meaning there
|
||||||
|
//! is no reference to `backend/`-specific types.
|
||||||
|
//!
|
||||||
|
//! As such, which backend is tested is
|
||||||
|
//! dependant on the feature flags used.
|
||||||
|
//!
|
||||||
|
//! | Feature flag | Tested backend |
|
||||||
|
//! |---------------|----------------|
|
||||||
|
//! | Only `redb` | `redb`
|
||||||
|
//! | Anything else | `heed`
|
||||||
|
//!
|
||||||
|
//! `redb`, and it only must be enabled for it to be tested.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::borrow::{Borrow, Cow};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, SyncMode},
|
||||||
|
database::{DatabaseRo, DatabaseRw},
|
||||||
|
env::{Env, EnvInner},
|
||||||
|
error::{InitError, RuntimeError},
|
||||||
|
resize::ResizeAlgorithm,
|
||||||
|
table::Table,
|
||||||
|
tables::{TestTable, TestTable2},
|
||||||
|
transaction::{TxRo, TxRw},
|
||||||
|
types::TestType,
|
||||||
|
value_guard::ValueGuard,
|
||||||
|
ConcreteEnv,
|
||||||
|
};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
|
/// Create an `Env` in a temporarily directory.
|
||||||
|
/// The directory is automatically removed after the `TempDir` is dropped.
|
||||||
|
///
|
||||||
|
/// TODO: changing this to `-> impl Env` causes lifetime errors...
|
||||||
|
fn tmp_concrete_env() -> (ConcreteEnv, tempfile::TempDir) {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let config = Config::low_power(Some(tempdir.path().into()));
|
||||||
|
let env = ConcreteEnv::open(config).unwrap();
|
||||||
|
|
||||||
|
(env, tempdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simply call [`Env::open`]. If this fails, something is really wrong.
|
||||||
|
#[test]
|
||||||
|
fn open() {
|
||||||
|
tmp_concrete_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create database transactions, but don't write any data.
|
||||||
|
#[test]
|
||||||
|
fn tx() {
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
let env_inner = env.env_inner();
|
||||||
|
|
||||||
|
TxRo::commit(env_inner.tx_ro().unwrap()).unwrap();
|
||||||
|
TxRw::commit(env_inner.tx_rw().unwrap()).unwrap();
|
||||||
|
TxRw::abort(env_inner.tx_rw().unwrap()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open (and verify) that all database tables
|
||||||
|
/// exist already after calling [`Env::open`].
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::items_after_statements, clippy::significant_drop_tightening)]
|
||||||
|
fn open_db() {
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
let env_inner = env.env_inner();
|
||||||
|
let tx_ro = env_inner.tx_ro().unwrap();
|
||||||
|
let mut tx_rw = env_inner.tx_rw().unwrap();
|
||||||
|
|
||||||
|
// Open all tables in read-only mode.
|
||||||
|
// This should be updated when tables are modified.
|
||||||
|
env_inner.open_db_ro::<TestTable>(&tx_ro).unwrap();
|
||||||
|
env_inner.open_db_ro::<TestTable2>(&tx_ro).unwrap();
|
||||||
|
TxRo::commit(tx_ro).unwrap();
|
||||||
|
|
||||||
|
// Open all tables in read/write mode.
|
||||||
|
env_inner.open_db_rw::<TestTable>(&mut tx_rw).unwrap();
|
||||||
|
env_inner.open_db_rw::<TestTable2>(&mut tx_rw).unwrap();
|
||||||
|
TxRw::commit(tx_rw).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test `Env` resizes.
|
||||||
|
#[test]
|
||||||
|
fn resize() {
|
||||||
|
// This test is only valid for `Env`'s that need to resize manually.
|
||||||
|
if !ConcreteEnv::MANUAL_RESIZE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
|
||||||
|
// Resize by the OS page size.
|
||||||
|
let page_size = crate::resize::page_size();
|
||||||
|
let old_size = env.current_map_size();
|
||||||
|
env.resize_map(Some(ResizeAlgorithm::FixedBytes(page_size)));
|
||||||
|
|
||||||
|
// Assert it resized exactly by the OS page size.
|
||||||
|
let new_size = env.current_map_size();
|
||||||
|
assert_eq!(new_size, old_size + page_size.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that `Env`'s that don't manually resize.
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "unreachable"]
|
||||||
|
fn non_manual_resize_1() {
|
||||||
|
if ConcreteEnv::MANUAL_RESIZE {
|
||||||
|
unreachable!();
|
||||||
|
} else {
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
env.resize_map(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
#[should_panic = "unreachable"]
|
||||||
|
fn non_manual_resize_2() {
|
||||||
|
if ConcreteEnv::MANUAL_RESIZE {
|
||||||
|
unreachable!();
|
||||||
|
} else {
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
env.current_map_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test all `DatabaseR{o,w}` operations.
|
||||||
|
#[test]
|
||||||
|
#[allow(
|
||||||
|
clippy::items_after_statements,
|
||||||
|
clippy::significant_drop_tightening,
|
||||||
|
clippy::used_underscore_binding
|
||||||
|
)]
|
||||||
|
fn db_read_write() {
|
||||||
|
let (env, _tempdir) = tmp_concrete_env();
|
||||||
|
let env_inner = env.env_inner();
|
||||||
|
let mut tx_rw = env_inner.tx_rw().unwrap();
|
||||||
|
let mut table = env_inner.open_db_rw::<TestTable>(&mut tx_rw).unwrap();
|
||||||
|
|
||||||
|
const KEY: i64 = 0_i64;
|
||||||
|
const VALUE: TestType = TestType {
|
||||||
|
u: 1,
|
||||||
|
b: 255,
|
||||||
|
_pad: [0; 7],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert `0..100` keys.
|
||||||
|
for i in 0..100 {
|
||||||
|
table.put(&(KEY + i), &VALUE).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the 1st key is there.
|
||||||
|
{
|
||||||
|
let guard = table.get(&KEY).unwrap();
|
||||||
|
let cow: Cow<'_, TestType> = guard.unguard();
|
||||||
|
let value: &TestType = cow.as_ref();
|
||||||
|
|
||||||
|
// Make sure all field accesses are aligned.
|
||||||
|
assert_eq!(value, &VALUE);
|
||||||
|
assert_eq!(value.u, VALUE.u);
|
||||||
|
assert_eq!(value.b, VALUE.b);
|
||||||
|
assert_eq!(value._pad, VALUE._pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the whole range is there.
|
||||||
|
{
|
||||||
|
let range = table.get_range(&..).unwrap();
|
||||||
|
let mut i = 0;
|
||||||
|
for result in range {
|
||||||
|
let guard = result.unwrap();
|
||||||
|
let cow: Cow<'_, TestType> = guard.unguard();
|
||||||
|
let value: &TestType = cow.as_ref();
|
||||||
|
|
||||||
|
assert_eq!(value, &VALUE);
|
||||||
|
assert_eq!(value.u, VALUE.u);
|
||||||
|
assert_eq!(value.b, VALUE.b);
|
||||||
|
assert_eq!(value._pad, VALUE._pad);
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(i, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert `get_range()` works.
|
||||||
|
let range = KEY..(KEY + 100);
|
||||||
|
assert_eq!(100, table.get_range(&range).unwrap().count());
|
||||||
|
|
||||||
|
// Assert deleting works.
|
||||||
|
table.delete(&KEY).unwrap();
|
||||||
|
let value = table.get(&KEY);
|
||||||
|
assert!(matches!(value, Err(RuntimeError::KeyNotFound)));
|
||||||
|
}
|
|
@ -1,463 +0,0 @@
|
||||||
//! Database [`Env`](crate::Env) configuration.
|
|
||||||
//!
|
|
||||||
//! This module contains the main [`Config`]uration struct
|
|
||||||
//! for the database [`Env`](crate::Env)ironment, and data
|
|
||||||
//! structures related to any configuration setting.
|
|
||||||
//!
|
|
||||||
//! These configurations are processed at runtime, meaning
|
|
||||||
//! the `Env` can/will dynamically adjust its behavior
|
|
||||||
//! based on these values.
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
|
||||||
use std::{
|
|
||||||
borrow::Cow,
|
|
||||||
num::NonZeroUsize,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use cuprate_helper::fs::cuprate_database_dir;
|
|
||||||
|
|
||||||
use crate::{constants::DATABASE_DATA_FILENAME, resize::ResizeAlgorithm};
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Config
|
|
||||||
/// Database [`Env`](crate::Env) configuration.
|
|
||||||
///
|
|
||||||
/// This is the struct passed to [`Env::open`](crate::Env::open) that
|
|
||||||
/// allows the database to be configured in various ways.
|
|
||||||
///
|
|
||||||
/// TODO: there's probably more options to add.
|
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub struct Config {
|
|
||||||
//------------------------ Database PATHs
|
|
||||||
// These are private since we don't want
|
|
||||||
// users messing with them after construction.
|
|
||||||
/// The directory used to store all database files.
|
|
||||||
///
|
|
||||||
/// By default, if no value is provided in the [`Config`]
|
|
||||||
/// constructor functions, this will be [`cuprate_database_dir`].
|
|
||||||
pub(crate) db_directory: Cow<'static, Path>,
|
|
||||||
/// The actual database data file.
|
|
||||||
///
|
|
||||||
/// This is private, and created from the above `db_directory`.
|
|
||||||
pub(crate) db_file: Cow<'static, Path>,
|
|
||||||
|
|
||||||
/// Disk synchronization mode.
|
|
||||||
pub sync_mode: SyncMode,
|
|
||||||
|
|
||||||
/// Database reader thread count.
|
|
||||||
pub reader_threads: ReaderThreads,
|
|
||||||
|
|
||||||
/// Database memory map resizing algorithm.
|
|
||||||
///
|
|
||||||
/// This is used as the default fallback, but
|
|
||||||
/// custom algorithms can be used as well with
|
|
||||||
/// [`Env::resize_map`](crate::Env::resize_map).
|
|
||||||
pub resize_algorithm: ResizeAlgorithm,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
/// Private function to acquire [`Config::db_file`]
|
|
||||||
/// from the user provided (or default) [`Config::db_directory`].
|
|
||||||
///
|
|
||||||
/// As the database data file PATH is just the directory + the filename,
|
|
||||||
/// we only need the directory from the user/Config, and can add it here.
|
|
||||||
fn return_db_dir_and_file(
|
|
||||||
db_directory: Option<PathBuf>,
|
|
||||||
) -> (Cow<'static, Path>, Cow<'static, Path>) {
|
|
||||||
// INVARIANT: all PATH safety checks are done
|
|
||||||
// in `helper::fs`. No need to do them here.
|
|
||||||
let db_directory =
|
|
||||||
db_directory.map_or_else(|| Cow::Borrowed(cuprate_database_dir()), Cow::Owned);
|
|
||||||
|
|
||||||
// Add the database filename to the directory.
|
|
||||||
let mut db_file = db_directory.to_path_buf();
|
|
||||||
db_file.push(DATABASE_DATA_FILENAME);
|
|
||||||
|
|
||||||
(db_directory, Cow::Owned(db_file))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`Config`] with sane default settings.
|
|
||||||
///
|
|
||||||
/// # `db_directory`
|
|
||||||
/// If this is `Some`, it will be used as the
|
|
||||||
/// directory that contains all database files.
|
|
||||||
///
|
|
||||||
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
|
||||||
pub fn new(db_directory: Option<PathBuf>) -> Self {
|
|
||||||
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
|
||||||
Self {
|
|
||||||
db_directory,
|
|
||||||
db_file,
|
|
||||||
sync_mode: SyncMode::FastThenSafe,
|
|
||||||
reader_threads: ReaderThreads::OnePerThread,
|
|
||||||
resize_algorithm: ResizeAlgorithm::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`Config`] with the highest performing,
|
|
||||||
/// but also most resource-intensive & maybe risky settings.
|
|
||||||
///
|
|
||||||
/// Good default for testing, and resource-available machines.
|
|
||||||
///
|
|
||||||
/// # `db_directory`
|
|
||||||
/// If this is `Some`, it will be used as the
|
|
||||||
/// directory that contains all database files.
|
|
||||||
///
|
|
||||||
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
|
||||||
pub fn fast(db_directory: Option<PathBuf>) -> Self {
|
|
||||||
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
|
||||||
Self {
|
|
||||||
db_directory,
|
|
||||||
db_file,
|
|
||||||
sync_mode: SyncMode::Fast,
|
|
||||||
reader_threads: ReaderThreads::OnePerThread,
|
|
||||||
resize_algorithm: ResizeAlgorithm::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`Config`] with the lowest performing,
|
|
||||||
/// but also least resource-intensive settings.
|
|
||||||
///
|
|
||||||
/// Good default for resource-limited machines, e.g. a cheap VPS.
|
|
||||||
///
|
|
||||||
/// # `db_directory`
|
|
||||||
/// If this is `Some`, it will be used as the
|
|
||||||
/// directory that contains all database files.
|
|
||||||
///
|
|
||||||
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
|
||||||
pub fn low_power(db_directory: Option<PathBuf>) -> Self {
|
|
||||||
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
|
||||||
Self {
|
|
||||||
db_directory,
|
|
||||||
db_file,
|
|
||||||
sync_mode: SyncMode::FastThenSafe,
|
|
||||||
reader_threads: ReaderThreads::One,
|
|
||||||
resize_algorithm: ResizeAlgorithm::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the absolute [`Path`] to the database directory.
|
|
||||||
///
|
|
||||||
/// This will be the `db_directory` given
|
|
||||||
/// (or default) during [`Config`] construction.
|
|
||||||
pub const fn db_directory(&self) -> &Cow<'_, Path> {
|
|
||||||
&self.db_directory
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the absolute [`Path`] to the database data file.
|
|
||||||
///
|
|
||||||
/// This will be based off the `db_directory` given
|
|
||||||
/// (or default) during [`Config`] construction.
|
|
||||||
pub const fn db_file(&self) -> &Cow<'_, Path> {
|
|
||||||
&self.db_file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
/// Same as `Self::new(None)`.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use cuprate_database::config::*;
|
|
||||||
/// assert_eq!(Config::default(), Config::new(None));
|
|
||||||
/// ```
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- SyncMode
|
|
||||||
/// Disk synchronization mode.
|
|
||||||
///
|
|
||||||
/// This controls how/when the database syncs its data to disk.
|
|
||||||
///
|
|
||||||
/// Regardless of the variant chosen, dropping [`Env`](crate::Env)
|
|
||||||
/// will always cause it to fully sync to disk.
|
|
||||||
///
|
|
||||||
/// # Sync vs Async
|
|
||||||
/// All invariants except [`SyncMode::Async`] & [`SyncMode::Fast`]
|
|
||||||
/// are `synchronous`, as in the database will wait until the OS has
|
|
||||||
/// finished syncing all the data to disk before continuing.
|
|
||||||
///
|
|
||||||
/// `SyncMode::Async` & `SyncMode::Fast` are `asynchronous`, meaning
|
|
||||||
/// the database will _NOT_ wait until the data is fully synced to disk
|
|
||||||
/// before continuing. Note that this doesn't mean the database itself
|
|
||||||
/// won't be synchronized between readers/writers, but rather that the
|
|
||||||
/// data _on disk_ may not be immediately synchronized after a write.
|
|
||||||
///
|
|
||||||
/// Something like:
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// db.put("key", value);
|
|
||||||
/// db.get("key");
|
|
||||||
/// ```
|
|
||||||
/// will be fine, most likely pulling from memory instead of disk.
|
|
||||||
///
|
|
||||||
/// # TODO
|
|
||||||
/// Dynamic sync's are not yet supported.
|
|
||||||
///
|
|
||||||
/// Only:
|
|
||||||
///
|
|
||||||
/// - [`SyncMode::Safe`]
|
|
||||||
/// - [`SyncMode::Async`]
|
|
||||||
/// - [`SyncMode::Fast`]
|
|
||||||
///
|
|
||||||
/// are supported, all other variants will panic on [`crate::Env::open`].
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub enum SyncMode {
|
|
||||||
/// Use [`SyncMode::Fast`] until fully synced,
|
|
||||||
/// then use [`SyncMode::Safe`].
|
|
||||||
///
|
|
||||||
/// # TODO: how to implement this?
|
|
||||||
/// ref: <https://github.com/monero-project/monero/issues/1463>
|
|
||||||
/// monerod-solution: <https://github.com/monero-project/monero/pull/1506>
|
|
||||||
/// cuprate-issue: <https://github.com/Cuprate/cuprate/issues/78>
|
|
||||||
///
|
|
||||||
/// We could:
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// if current_db_block <= top_block.saturating_sub(N) {
|
|
||||||
/// // don't sync()
|
|
||||||
/// } else {
|
|
||||||
/// // sync()
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// where N is some threshold we pick that is _close_ enough
|
|
||||||
/// to being synced where we want to start being safer.
|
|
||||||
///
|
|
||||||
/// Essentially, when we are in a certain % range of being finished,
|
|
||||||
/// switch to safe mode, until then, go fast.
|
|
||||||
#[default]
|
|
||||||
FastThenSafe,
|
|
||||||
|
|
||||||
/// Fully sync to disk per transaction.
|
|
||||||
///
|
|
||||||
/// Every database transaction commit will
|
|
||||||
/// fully sync all data to disk, _synchronously_,
|
|
||||||
/// so the database (writer) halts until synced.
|
|
||||||
///
|
|
||||||
/// This is expected to be very slow.
|
|
||||||
///
|
|
||||||
/// This matches:
|
|
||||||
/// - LMDB without any special sync flags
|
|
||||||
/// - [`redb::Durability::Immediate`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Immediate)
|
|
||||||
Safe,
|
|
||||||
|
|
||||||
/// Asynchrously sync to disk per transaction.
|
|
||||||
///
|
|
||||||
/// This is the same as [`SyncMode::Safe`],
|
|
||||||
/// but the syncs will be asynchronous, i.e.
|
|
||||||
/// each transaction commit will sync to disk,
|
|
||||||
/// but only eventually, not necessarily immediately.
|
|
||||||
///
|
|
||||||
/// This matches:
|
|
||||||
/// - [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94)
|
|
||||||
/// - [`redb::Durability::Eventual`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Eventual)
|
|
||||||
Async,
|
|
||||||
|
|
||||||
/// Fully sync to disk after we cross this transaction threshold.
|
|
||||||
///
|
|
||||||
/// After committing [`usize`] amount of database
|
|
||||||
/// transactions, it will be sync to disk.
|
|
||||||
///
|
|
||||||
/// `0` behaves the same as [`SyncMode::Safe`], and a ridiculously large
|
|
||||||
/// number like `usize::MAX` is practically the same as [`SyncMode::Fast`].
|
|
||||||
Threshold(usize),
|
|
||||||
|
|
||||||
/// Only flush at database shutdown.
|
|
||||||
///
|
|
||||||
/// This is the fastest, yet unsafest option.
|
|
||||||
///
|
|
||||||
/// It will cause the database to never _actively_ sync,
|
|
||||||
/// letting the OS decide when to flush data to disk.
|
|
||||||
///
|
|
||||||
/// This matches:
|
|
||||||
/// - [`MDB_NOSYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#ga5791dd1adb09123f82dd1f331209e12e) + [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94)
|
|
||||||
/// - [`redb::Durability::None`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.None)
|
|
||||||
///
|
|
||||||
/// `monerod` reference: <https://github.com/monero-project/monero/blob/7b7958bbd9d76375c47dc418b4adabba0f0b1785/src/blockchain_db/lmdb/db_lmdb.cpp#L1380-L1381>
|
|
||||||
///
|
|
||||||
/// # Corruption
|
|
||||||
/// In the case of a system crash, the database
|
|
||||||
/// may become corrupted when using this option.
|
|
||||||
//
|
|
||||||
// TODO: we could call this `unsafe`
|
|
||||||
// and use that terminology in the config file
|
|
||||||
// so users know exactly what they are getting
|
|
||||||
// themselves into.
|
|
||||||
Fast,
|
|
||||||
}
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- ReaderThreads
|
|
||||||
/// Amount of database reader threads to spawn.
|
|
||||||
///
|
|
||||||
/// This controls how many reader thread [`crate::service`]'s
|
|
||||||
/// thread-pool will spawn to receive and send requests/responses.
|
|
||||||
///
|
|
||||||
/// It will always be at least 1, up until the amount of threads on the machine.
|
|
||||||
///
|
|
||||||
/// The main function used to extract an actual
|
|
||||||
/// usable thread count out of this is [`ReaderThreads::as_threads`].
|
|
||||||
#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub enum ReaderThreads {
|
|
||||||
#[default]
|
|
||||||
/// Spawn 1 reader thread per available thread on the machine.
|
|
||||||
///
|
|
||||||
/// For example, a `16-core, 32-thread` Ryzen 5950x will
|
|
||||||
/// spawn `32` reader threads using this setting.
|
|
||||||
OnePerThread,
|
|
||||||
|
|
||||||
/// Only spawn 1 reader thread.
|
|
||||||
One,
|
|
||||||
|
|
||||||
/// Spawn a specified amount of reader threads.
|
|
||||||
///
|
|
||||||
/// Note that no matter how large this value, it will be
|
|
||||||
/// ultimately capped at the amount of system threads.
|
|
||||||
///
|
|
||||||
/// # `0`
|
|
||||||
/// `ReaderThreads::Number(0)` represents "use maximum value",
|
|
||||||
/// as such, it is equal to [`ReaderThreads::OnePerThread`].
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use cuprate_database::config::*;
|
|
||||||
/// let reader_threads = ReaderThreads::from(0_usize);
|
|
||||||
/// assert!(matches!(reader_threads, ReaderThreads::OnePerThread));
|
|
||||||
/// ```
|
|
||||||
Number(usize),
|
|
||||||
|
|
||||||
/// Spawn a specified % of reader threads.
|
|
||||||
///
|
|
||||||
/// This must be a value in-between `0.0..1.0`
|
|
||||||
/// where `1.0` represents [`ReaderThreads::OnePerThread`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// For example, using a `16-core, 32-thread` Ryzen 5950x CPU:
|
|
||||||
///
|
|
||||||
/// | Input | Total thread used |
|
|
||||||
/// |------------------------------------|-------------------|
|
|
||||||
/// | `ReaderThreads::Percent(0.0)` | 32 (maximum value)
|
|
||||||
/// | `ReaderThreads::Percent(0.5)` | 16
|
|
||||||
/// | `ReaderThreads::Percent(0.75)` | 24
|
|
||||||
/// | `ReaderThreads::Percent(1.0)` | 32
|
|
||||||
/// | `ReaderThreads::Percent(2.0)` | 32 (saturating)
|
|
||||||
/// | `ReaderThreads::Percent(f32::NAN)` | 32 (non-normal default)
|
|
||||||
///
|
|
||||||
/// # `0.0`
|
|
||||||
/// `ReaderThreads::Percent(0.0)` represents "use maximum value",
|
|
||||||
/// as such, it is equal to [`ReaderThreads::OnePerThread`].
|
|
||||||
///
|
|
||||||
/// # Not quite `0.0`
|
|
||||||
/// If the thread count multiplied by the percentage ends up being
|
|
||||||
/// non-zero, but not 1 thread, the minimum value 1 will be returned.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use cuprate_database::config::*;
|
|
||||||
/// assert_eq!(ReaderThreads::Percent(0.000000001).as_threads().get(), 1);
|
|
||||||
/// ```
|
|
||||||
Percent(f32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReaderThreads {
|
|
||||||
/// This converts [`ReaderThreads`] into a safe, usable
|
|
||||||
/// number representing how many threads to spawn.
|
|
||||||
///
|
|
||||||
/// This function will always return a number in-between `1..=total_thread_count`.
|
|
||||||
///
|
|
||||||
/// It uses [`cuprate_helper::thread::threads()`] internally to determine the total thread count.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use cuprate_database::config::ReaderThreads as Rt;
|
|
||||||
///
|
|
||||||
/// let total_threads: std::num::NonZeroUsize =
|
|
||||||
/// cuprate_helper::thread::threads();
|
|
||||||
///
|
|
||||||
/// assert_eq!(Rt::OnePerThread.as_threads(), total_threads);
|
|
||||||
///
|
|
||||||
/// assert_eq!(Rt::One.as_threads().get(), 1);
|
|
||||||
///
|
|
||||||
/// assert_eq!(Rt::Number(0).as_threads(), total_threads);
|
|
||||||
/// assert_eq!(Rt::Number(1).as_threads().get(), 1);
|
|
||||||
/// assert_eq!(Rt::Number(usize::MAX).as_threads(), total_threads);
|
|
||||||
///
|
|
||||||
/// assert_eq!(Rt::Percent(0.01).as_threads().get(), 1);
|
|
||||||
/// assert_eq!(Rt::Percent(0.0).as_threads(), total_threads);
|
|
||||||
/// assert_eq!(Rt::Percent(1.0).as_threads(), total_threads);
|
|
||||||
/// assert_eq!(Rt::Percent(f32::NAN).as_threads(), total_threads);
|
|
||||||
/// assert_eq!(Rt::Percent(f32::INFINITY).as_threads(), total_threads);
|
|
||||||
/// assert_eq!(Rt::Percent(f32::NEG_INFINITY).as_threads(), total_threads);
|
|
||||||
///
|
|
||||||
/// // Percentage only works on more than 1 thread.
|
|
||||||
/// if total_threads.get() > 1 {
|
|
||||||
/// assert_eq!(
|
|
||||||
/// Rt::Percent(0.5).as_threads().get(),
|
|
||||||
/// (total_threads.get() as f32 / 2.0) as usize,
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
//
|
|
||||||
// INVARIANT:
|
|
||||||
// LMDB will error if we input zero, so don't allow that.
|
|
||||||
// <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L4687>
|
|
||||||
pub fn as_threads(&self) -> NonZeroUsize {
|
|
||||||
let total_threads = cuprate_helper::thread::threads();
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::OnePerThread => total_threads, // use all threads
|
|
||||||
Self::One => NonZeroUsize::MIN, // one
|
|
||||||
Self::Number(n) => match NonZeroUsize::new(*n) {
|
|
||||||
Some(n) => std::cmp::min(n, total_threads), // saturate at total threads
|
|
||||||
None => total_threads, // 0 == maximum value
|
|
||||||
},
|
|
||||||
|
|
||||||
// We handle the casting loss.
|
|
||||||
#[allow(
|
|
||||||
clippy::cast_precision_loss,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss
|
|
||||||
)]
|
|
||||||
Self::Percent(f) => {
|
|
||||||
// If non-normal float, use the default (all threads).
|
|
||||||
if !f.is_normal() || !(0.0..=1.0).contains(f) {
|
|
||||||
return total_threads;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0.0 == maximum value.
|
|
||||||
if *f == 0.0 {
|
|
||||||
return total_threads;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate percentage of total threads.
|
|
||||||
let thread_percent = (total_threads.get() as f32) * f;
|
|
||||||
match NonZeroUsize::new(thread_percent as usize) {
|
|
||||||
Some(n) => std::cmp::min(n, total_threads), // saturate at total threads.
|
|
||||||
None => {
|
|
||||||
// We checked for `0.0` above, so what this
|
|
||||||
// being 0 means that the percentage was _so_
|
|
||||||
// low it made our thread count something like
|
|
||||||
// 0.99. In this case, just use 1 thread.
|
|
||||||
NonZeroUsize::MIN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<usize>> From<T> for ReaderThreads {
|
|
||||||
/// Create a [`ReaderThreads::Number`].
|
|
||||||
///
|
|
||||||
/// If `value` is `0`, this will return [`ReaderThreads::OnePerThread`].
|
|
||||||
fn from(value: T) -> Self {
|
|
||||||
let u: usize = value.into();
|
|
||||||
if u == 0 {
|
|
||||||
Self::OnePerThread
|
|
||||||
} else {
|
|
||||||
Self::Number(u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
31
database/src/config/backend.rs
Normal file
31
database/src/config/backend.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//! TODO
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use cuprate_helper::fs::cuprate_database_dir;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{ReaderThreads, SyncMode},
|
||||||
|
constants::DATABASE_DATA_FILENAME,
|
||||||
|
resize::ResizeAlgorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Backend
|
||||||
|
/// TODO
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum Backend {
|
||||||
|
#[default]
|
||||||
|
/// TODO
|
||||||
|
Heed,
|
||||||
|
/// TODO
|
||||||
|
Redb,
|
||||||
|
}
|
177
database/src/config/config.rs
Normal file
177
database/src/config/config.rs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
//! Database [`Env`](crate::Env) configuration.
|
||||||
|
//!
|
||||||
|
//! This module contains the main [`Config`]uration struct
|
||||||
|
//! for the database [`Env`](crate::Env)ironment, and data
|
||||||
|
//! structures related to any configuration setting.
|
||||||
|
//!
|
||||||
|
//! These configurations are processed at runtime, meaning
|
||||||
|
//! the `Env` can/will dynamically adjust its behavior
|
||||||
|
//! based on these values.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use cuprate_helper::fs::cuprate_database_dir;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{ReaderThreads, SyncMode},
|
||||||
|
constants::DATABASE_DATA_FILENAME,
|
||||||
|
resize::ResizeAlgorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Config
|
||||||
|
/// Database [`Env`](crate::Env) configuration.
|
||||||
|
///
|
||||||
|
/// This is the struct passed to [`Env::open`](crate::Env::open) that
|
||||||
|
/// allows the database to be configured in various ways.
|
||||||
|
///
|
||||||
|
/// TODO: there's probably more options to add.
|
||||||
|
#[derive(Debug, Clone, PartialEq, PartialOrd)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub struct Config {
|
||||||
|
//------------------------ Database PATHs
|
||||||
|
// These are private since we don't want
|
||||||
|
// users messing with them after construction.
|
||||||
|
/// The directory used to store all database files.
|
||||||
|
///
|
||||||
|
/// By default, if no value is provided in the [`Config`]
|
||||||
|
/// constructor functions, this will be [`cuprate_database_dir`].
|
||||||
|
///
|
||||||
|
/// TODO: we should also support `/etc/cuprated.conf`.
|
||||||
|
/// This could be represented with an `enum DbPath { Default, Custom, Etc, }`
|
||||||
|
pub(crate) db_directory: Cow<'static, Path>,
|
||||||
|
/// The actual database data file.
|
||||||
|
///
|
||||||
|
/// This is private, and created from the above `db_directory`.
|
||||||
|
pub(crate) db_file: Cow<'static, Path>,
|
||||||
|
|
||||||
|
/// Disk synchronization mode.
|
||||||
|
pub sync_mode: SyncMode,
|
||||||
|
|
||||||
|
/// Database reader thread count.
|
||||||
|
pub reader_threads: ReaderThreads,
|
||||||
|
|
||||||
|
/// Database memory map resizing algorithm.
|
||||||
|
///
|
||||||
|
/// This is used as the default fallback, but
|
||||||
|
/// custom algorithms can be used as well with
|
||||||
|
/// [`Env::resize_map`](crate::Env::resize_map).
|
||||||
|
pub resize_algorithm: ResizeAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Private function to acquire [`Config::db_file`]
|
||||||
|
/// from the user provided (or default) [`Config::db_directory`].
|
||||||
|
///
|
||||||
|
/// As the database data file PATH is just the directory + the filename,
|
||||||
|
/// we only need the directory from the user/Config, and can add it here.
|
||||||
|
fn return_db_dir_and_file(
|
||||||
|
db_directory: Option<PathBuf>,
|
||||||
|
) -> (Cow<'static, Path>, Cow<'static, Path>) {
|
||||||
|
// INVARIANT: all PATH safety checks are done
|
||||||
|
// in `helper::fs`. No need to do them here.
|
||||||
|
let db_directory =
|
||||||
|
db_directory.map_or_else(|| Cow::Borrowed(cuprate_database_dir()), Cow::Owned);
|
||||||
|
|
||||||
|
// Add the database filename to the directory.
|
||||||
|
let mut db_file = db_directory.to_path_buf();
|
||||||
|
db_file.push(DATABASE_DATA_FILENAME);
|
||||||
|
|
||||||
|
(db_directory, Cow::Owned(db_file))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Config`] with sane default settings.
|
||||||
|
///
|
||||||
|
/// # `db_directory`
|
||||||
|
/// If this is `Some`, it will be used as the
|
||||||
|
/// directory that contains all database files.
|
||||||
|
///
|
||||||
|
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
||||||
|
pub fn new(db_directory: Option<PathBuf>) -> Self {
|
||||||
|
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
||||||
|
Self {
|
||||||
|
db_directory,
|
||||||
|
db_file,
|
||||||
|
sync_mode: SyncMode::default(),
|
||||||
|
reader_threads: ReaderThreads::OnePerThread,
|
||||||
|
resize_algorithm: ResizeAlgorithm::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`Config`] with the highest performing,
|
||||||
|
/// but also most resource-intensive & maybe risky settings.
|
||||||
|
///
|
||||||
|
/// Good default for testing, and resource-available machines.
|
||||||
|
///
|
||||||
|
/// # `db_directory`
|
||||||
|
/// If this is `Some`, it will be used as the
|
||||||
|
/// directory that contains all database files.
|
||||||
|
///
|
||||||
|
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
||||||
|
pub fn fast(db_directory: Option<PathBuf>) -> Self {
|
||||||
|
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
||||||
|
Self {
|
||||||
|
db_directory,
|
||||||
|
db_file,
|
||||||
|
sync_mode: SyncMode::Fast,
|
||||||
|
reader_threads: ReaderThreads::OnePerThread,
|
||||||
|
resize_algorithm: ResizeAlgorithm::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`Config`] with the lowest performing,
|
||||||
|
/// but also least resource-intensive settings.
|
||||||
|
///
|
||||||
|
/// Good default for resource-limited machines, e.g. a cheap VPS.
|
||||||
|
///
|
||||||
|
/// # `db_directory`
|
||||||
|
/// If this is `Some`, it will be used as the
|
||||||
|
/// directory that contains all database files.
|
||||||
|
///
|
||||||
|
/// If `None`, it will use the default directory [`cuprate_database_dir`].
|
||||||
|
pub fn low_power(db_directory: Option<PathBuf>) -> Self {
|
||||||
|
let (db_directory, db_file) = Self::return_db_dir_and_file(db_directory);
|
||||||
|
Self {
|
||||||
|
db_directory,
|
||||||
|
db_file,
|
||||||
|
sync_mode: SyncMode::default(),
|
||||||
|
reader_threads: ReaderThreads::One,
|
||||||
|
resize_algorithm: ResizeAlgorithm::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the absolute [`Path`] to the database directory.
|
||||||
|
///
|
||||||
|
/// This will be the `db_directory` given
|
||||||
|
/// (or default) during [`Config`] construction.
|
||||||
|
pub const fn db_directory(&self) -> &Cow<'_, Path> {
|
||||||
|
&self.db_directory
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the absolute [`Path`] to the database data file.
|
||||||
|
///
|
||||||
|
/// This will be based off the `db_directory` given
|
||||||
|
/// (or default) during [`Config`] construction.
|
||||||
|
pub const fn db_file(&self) -> &Cow<'_, Path> {
|
||||||
|
&self.db_file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
/// Same as `Self::new(None)`.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use cuprate_database::config::*;
|
||||||
|
/// assert_eq!(Config::default(), Config::new(None));
|
||||||
|
/// ```
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(None)
|
||||||
|
}
|
||||||
|
}
|
10
database/src/config/mod.rs
Normal file
10
database/src/config/mod.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
//! TODO
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
pub use config::Config;
|
||||||
|
|
||||||
|
mod reader_threads;
|
||||||
|
pub use reader_threads::ReaderThreads;
|
||||||
|
|
||||||
|
mod sync_mode;
|
||||||
|
pub use sync_mode::SyncMode;
|
195
database/src/config/reader_threads.rs
Normal file
195
database/src/config/reader_threads.rs
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
//! Database [`Env`](crate::Env) configuration.
|
||||||
|
//!
|
||||||
|
//! This module contains the main [`Config`]uration struct
|
||||||
|
//! for the database [`Env`](crate::Env)ironment, and data
|
||||||
|
//! structures related to any configuration setting.
|
||||||
|
//!
|
||||||
|
//! These configurations are processed at runtime, meaning
|
||||||
|
//! the `Env` can/will dynamically adjust its behavior
|
||||||
|
//! based on these values.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use cuprate_helper::fs::cuprate_database_dir;
|
||||||
|
|
||||||
|
use crate::{constants::DATABASE_DATA_FILENAME, resize::ResizeAlgorithm};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- ReaderThreads
|
||||||
|
/// Amount of database reader threads to spawn.
|
||||||
|
///
|
||||||
|
/// This controls how many reader thread [`crate::service`]'s
|
||||||
|
/// thread-pool will spawn to receive and send requests/responses.
|
||||||
|
///
|
||||||
|
/// It will always be at least 1, up until the amount of threads on the machine.
|
||||||
|
///
|
||||||
|
/// The main function used to extract an actual
|
||||||
|
/// usable thread count out of this is [`ReaderThreads::as_threads`].
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum ReaderThreads {
|
||||||
|
#[default]
|
||||||
|
/// Spawn 1 reader thread per available thread on the machine.
|
||||||
|
///
|
||||||
|
/// For example, a `16-core, 32-thread` Ryzen 5950x will
|
||||||
|
/// spawn `32` reader threads using this setting.
|
||||||
|
OnePerThread,
|
||||||
|
|
||||||
|
/// Only spawn 1 reader thread.
|
||||||
|
One,
|
||||||
|
|
||||||
|
/// Spawn a specified amount of reader threads.
|
||||||
|
///
|
||||||
|
/// Note that no matter how large this value, it will be
|
||||||
|
/// ultimately capped at the amount of system threads.
|
||||||
|
///
|
||||||
|
/// # `0`
|
||||||
|
/// `ReaderThreads::Number(0)` represents "use maximum value",
|
||||||
|
/// as such, it is equal to [`ReaderThreads::OnePerThread`].
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use cuprate_database::config::*;
|
||||||
|
/// let reader_threads = ReaderThreads::from(0_usize);
|
||||||
|
/// assert!(matches!(reader_threads, ReaderThreads::OnePerThread));
|
||||||
|
/// ```
|
||||||
|
Number(usize),
|
||||||
|
|
||||||
|
/// Spawn a specified % of reader threads.
|
||||||
|
///
|
||||||
|
/// This must be a value in-between `0.0..1.0`
|
||||||
|
/// where `1.0` represents [`ReaderThreads::OnePerThread`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// For example, using a `16-core, 32-thread` Ryzen 5950x CPU:
|
||||||
|
///
|
||||||
|
/// | Input | Total thread used |
|
||||||
|
/// |------------------------------------|-------------------|
|
||||||
|
/// | `ReaderThreads::Percent(0.0)` | 32 (maximum value)
|
||||||
|
/// | `ReaderThreads::Percent(0.5)` | 16
|
||||||
|
/// | `ReaderThreads::Percent(0.75)` | 24
|
||||||
|
/// | `ReaderThreads::Percent(1.0)` | 32
|
||||||
|
/// | `ReaderThreads::Percent(2.0)` | 32 (saturating)
|
||||||
|
/// | `ReaderThreads::Percent(f32::NAN)` | 32 (non-normal default)
|
||||||
|
///
|
||||||
|
/// # `0.0`
|
||||||
|
/// `ReaderThreads::Percent(0.0)` represents "use maximum value",
|
||||||
|
/// as such, it is equal to [`ReaderThreads::OnePerThread`].
|
||||||
|
///
|
||||||
|
/// # Not quite `0.0`
|
||||||
|
/// If the thread count multiplied by the percentage ends up being
|
||||||
|
/// non-zero, but not 1 thread, the minimum value 1 will be returned.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use cuprate_database::config::*;
|
||||||
|
/// assert_eq!(ReaderThreads::Percent(0.000000001).as_threads().get(), 1);
|
||||||
|
/// ```
|
||||||
|
Percent(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReaderThreads {
|
||||||
|
/// This converts [`ReaderThreads`] into a safe, usable
|
||||||
|
/// number representing how many threads to spawn.
|
||||||
|
///
|
||||||
|
/// This function will always return a number in-between `1..=total_thread_count`.
|
||||||
|
///
|
||||||
|
/// It uses [`cuprate_helper::thread::threads()`] internally to determine the total thread count.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use cuprate_database::config::ReaderThreads as Rt;
|
||||||
|
///
|
||||||
|
/// let total_threads: std::num::NonZeroUsize =
|
||||||
|
/// cuprate_helper::thread::threads();
|
||||||
|
///
|
||||||
|
/// assert_eq!(Rt::OnePerThread.as_threads(), total_threads);
|
||||||
|
///
|
||||||
|
/// assert_eq!(Rt::One.as_threads().get(), 1);
|
||||||
|
///
|
||||||
|
/// assert_eq!(Rt::Number(0).as_threads(), total_threads);
|
||||||
|
/// assert_eq!(Rt::Number(1).as_threads().get(), 1);
|
||||||
|
/// assert_eq!(Rt::Number(usize::MAX).as_threads(), total_threads);
|
||||||
|
///
|
||||||
|
/// assert_eq!(Rt::Percent(0.01).as_threads().get(), 1);
|
||||||
|
/// assert_eq!(Rt::Percent(0.0).as_threads(), total_threads);
|
||||||
|
/// assert_eq!(Rt::Percent(1.0).as_threads(), total_threads);
|
||||||
|
/// assert_eq!(Rt::Percent(f32::NAN).as_threads(), total_threads);
|
||||||
|
/// assert_eq!(Rt::Percent(f32::INFINITY).as_threads(), total_threads);
|
||||||
|
/// assert_eq!(Rt::Percent(f32::NEG_INFINITY).as_threads(), total_threads);
|
||||||
|
///
|
||||||
|
/// // Percentage only works on more than 1 thread.
|
||||||
|
/// if total_threads.get() > 1 {
|
||||||
|
/// assert_eq!(
|
||||||
|
/// Rt::Percent(0.5).as_threads().get(),
|
||||||
|
/// (total_threads.get() as f32 / 2.0) as usize,
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
//
|
||||||
|
// INVARIANT:
|
||||||
|
// LMDB will error if we input zero, so don't allow that.
|
||||||
|
// <https://github.com/LMDB/lmdb/blob/b8e54b4c31378932b69f1298972de54a565185b1/libraries/liblmdb/mdb.c#L4687>
|
||||||
|
pub fn as_threads(&self) -> NonZeroUsize {
|
||||||
|
let total_threads = cuprate_helper::thread::threads();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::OnePerThread => total_threads, // use all threads
|
||||||
|
Self::One => NonZeroUsize::MIN, // one
|
||||||
|
Self::Number(n) => match NonZeroUsize::new(*n) {
|
||||||
|
Some(n) => std::cmp::min(n, total_threads), // saturate at total threads
|
||||||
|
None => total_threads, // 0 == maximum value
|
||||||
|
},
|
||||||
|
|
||||||
|
// We handle the casting loss.
|
||||||
|
#[allow(
|
||||||
|
clippy::cast_precision_loss,
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_sign_loss
|
||||||
|
)]
|
||||||
|
Self::Percent(f) => {
|
||||||
|
// If non-normal float, use the default (all threads).
|
||||||
|
if !f.is_normal() || !(0.0..=1.0).contains(f) {
|
||||||
|
return total_threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0.0 == maximum value.
|
||||||
|
if *f == 0.0 {
|
||||||
|
return total_threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentage of total threads.
|
||||||
|
let thread_percent = (total_threads.get() as f32) * f;
|
||||||
|
match NonZeroUsize::new(thread_percent as usize) {
|
||||||
|
Some(n) => std::cmp::min(n, total_threads), // saturate at total threads.
|
||||||
|
None => {
|
||||||
|
// We checked for `0.0` above, so what this
|
||||||
|
// being 0 means that the percentage was _so_
|
||||||
|
// low it made our thread count something like
|
||||||
|
// 0.99. In this case, just use 1 thread.
|
||||||
|
NonZeroUsize::MIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<usize>> From<T> for ReaderThreads {
|
||||||
|
/// Create a [`ReaderThreads::Number`].
|
||||||
|
///
|
||||||
|
/// If `value` is `0`, this will return [`ReaderThreads::OnePerThread`].
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
let u: usize = value.into();
|
||||||
|
if u == 0 {
|
||||||
|
Self::OnePerThread
|
||||||
|
} else {
|
||||||
|
Self::Number(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
database/src/config/sync_mode.rs
Normal file
144
database/src/config/sync_mode.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
//! Database [`Env`](crate::Env) configuration.
|
||||||
|
//!
|
||||||
|
//! This module contains the main [`Config`]uration struct
|
||||||
|
//! for the database [`Env`](crate::Env)ironment, and data
|
||||||
|
//! structures related to any configuration setting.
|
||||||
|
//!
|
||||||
|
//! These configurations are processed at runtime, meaning
|
||||||
|
//! the `Env` can/will dynamically adjust its behavior
|
||||||
|
//! based on these values.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use cuprate_helper::fs::cuprate_database_dir;
|
||||||
|
|
||||||
|
use crate::{constants::DATABASE_DATA_FILENAME, resize::ResizeAlgorithm};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- SyncMode
|
||||||
|
/// Disk synchronization mode.
|
||||||
|
///
|
||||||
|
/// This controls how/when the database syncs its data to disk.
|
||||||
|
///
|
||||||
|
/// Regardless of the variant chosen, dropping [`Env`](crate::Env)
|
||||||
|
/// will always cause it to fully sync to disk.
|
||||||
|
///
|
||||||
|
/// # Sync vs Async
|
||||||
|
/// All invariants except [`SyncMode::Async`] & [`SyncMode::Fast`]
|
||||||
|
/// are `synchronous`, as in the database will wait until the OS has
|
||||||
|
/// finished syncing all the data to disk before continuing.
|
||||||
|
///
|
||||||
|
/// `SyncMode::Async` & `SyncMode::Fast` are `asynchronous`, meaning
|
||||||
|
/// the database will _NOT_ wait until the data is fully synced to disk
|
||||||
|
/// before continuing. Note that this doesn't mean the database itself
|
||||||
|
/// won't be synchronized between readers/writers, but rather that the
|
||||||
|
/// data _on disk_ may not be immediately synchronized after a write.
|
||||||
|
///
|
||||||
|
/// Something like:
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// db.put("key", value);
|
||||||
|
/// db.get("key");
|
||||||
|
/// ```
|
||||||
|
/// will be fine, most likely pulling from memory instead of disk.
|
||||||
|
///
|
||||||
|
/// # TODO
|
||||||
|
/// Dynamic sync's are not yet supported.
|
||||||
|
///
|
||||||
|
/// Only:
|
||||||
|
///
|
||||||
|
/// - [`SyncMode::Safe`]
|
||||||
|
/// - [`SyncMode::Async`]
|
||||||
|
/// - [`SyncMode::Fast`]
|
||||||
|
///
|
||||||
|
/// are supported, all other variants will panic on [`crate::Env::open`].
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum SyncMode {
|
||||||
|
/// Use [`SyncMode::Fast`] until fully synced,
|
||||||
|
/// then use [`SyncMode::Safe`].
|
||||||
|
///
|
||||||
|
/// # TODO: how to implement this?
|
||||||
|
/// ref: <https://github.com/monero-project/monero/issues/1463>
|
||||||
|
/// monerod-solution: <https://github.com/monero-project/monero/pull/1506>
|
||||||
|
/// cuprate-issue: <https://github.com/Cuprate/cuprate/issues/78>
|
||||||
|
///
|
||||||
|
/// We could:
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// if current_db_block <= top_block.saturating_sub(N) {
|
||||||
|
/// // don't sync()
|
||||||
|
/// } else {
|
||||||
|
/// // sync()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// where N is some threshold we pick that is _close_ enough
|
||||||
|
/// to being synced where we want to start being safer.
|
||||||
|
///
|
||||||
|
/// Essentially, when we are in a certain % range of being finished,
|
||||||
|
/// switch to safe mode, until then, go fast.
|
||||||
|
FastThenSafe,
|
||||||
|
|
||||||
|
#[default]
|
||||||
|
/// Fully sync to disk per transaction.
|
||||||
|
///
|
||||||
|
/// Every database transaction commit will
|
||||||
|
/// fully sync all data to disk, _synchronously_,
|
||||||
|
/// so the database (writer) halts until synced.
|
||||||
|
///
|
||||||
|
/// This is expected to be very slow.
|
||||||
|
///
|
||||||
|
/// This matches:
|
||||||
|
/// - LMDB without any special sync flags
|
||||||
|
/// - [`redb::Durability::Immediate`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Immediate)
|
||||||
|
Safe,
|
||||||
|
|
||||||
|
/// Asynchrously sync to disk per transaction.
|
||||||
|
///
|
||||||
|
/// This is the same as [`SyncMode::Safe`],
|
||||||
|
/// but the syncs will be asynchronous, i.e.
|
||||||
|
/// each transaction commit will sync to disk,
|
||||||
|
/// but only eventually, not necessarily immediately.
|
||||||
|
///
|
||||||
|
/// This matches:
|
||||||
|
/// - [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94)
|
||||||
|
/// - [`redb::Durability::Eventual`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.Eventual)
|
||||||
|
Async,
|
||||||
|
|
||||||
|
/// Fully sync to disk after we cross this transaction threshold.
|
||||||
|
///
|
||||||
|
/// After committing [`usize`] amount of database
|
||||||
|
/// transactions, it will be sync to disk.
|
||||||
|
///
|
||||||
|
/// `0` behaves the same as [`SyncMode::Safe`], and a ridiculously large
|
||||||
|
/// number like `usize::MAX` is practically the same as [`SyncMode::Fast`].
|
||||||
|
Threshold(usize),
|
||||||
|
|
||||||
|
/// Only flush at database shutdown.
|
||||||
|
///
|
||||||
|
/// This is the fastest, yet unsafest option.
|
||||||
|
///
|
||||||
|
/// It will cause the database to never _actively_ sync,
|
||||||
|
/// letting the OS decide when to flush data to disk.
|
||||||
|
///
|
||||||
|
/// This matches:
|
||||||
|
/// - [`MDB_NOSYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#ga5791dd1adb09123f82dd1f331209e12e) + [`MDB_MAPASYNC`](http://www.lmdb.tech/doc/group__mdb__env.html#gab034ed0d8e5938090aef5ee0997f7e94)
|
||||||
|
/// - [`redb::Durability::None`](https://docs.rs/redb/1.5.0/redb/enum.Durability.html#variant.None)
|
||||||
|
///
|
||||||
|
/// `monerod` reference: <https://github.com/monero-project/monero/blob/7b7958bbd9d76375c47dc418b4adabba0f0b1785/src/blockchain_db/lmdb/db_lmdb.cpp#L1380-L1381>
|
||||||
|
///
|
||||||
|
/// # Corruption
|
||||||
|
/// In the case of a system crash, the database
|
||||||
|
/// may become corrupted when using this option.
|
||||||
|
//
|
||||||
|
// TODO: we could call this `unsafe`
|
||||||
|
// and use that terminology in the config file
|
||||||
|
// so users know exactly what they are getting
|
||||||
|
// themselves into.
|
||||||
|
Fast,
|
||||||
|
}
|
|
@ -1,54 +1,73 @@
|
||||||
//! Abstracted database; `trait DatabaseRo` & `trait DatabaseRw`.
|
//! Abstracted database; `trait DatabaseRo` & `trait DatabaseRw`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use crate::{error::RuntimeError, table::Table};
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
fmt::Debug,
|
||||||
|
ops::{Deref, RangeBounds},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::RuntimeError,
|
||||||
|
table::Table,
|
||||||
|
transaction::{TxRo, TxRw},
|
||||||
|
value_guard::ValueGuard,
|
||||||
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRo
|
//---------------------------------------------------------------------------------------------------- DatabaseRo
|
||||||
/// Database (key-value store) read abstraction.
|
/// Database (key-value store) read abstraction.
|
||||||
///
|
///
|
||||||
/// TODO: document relation between `DatabaseRo` <-> `DatabaseRw`.
|
/// This is a read-only database table,
|
||||||
pub trait DatabaseRo<T: Table> {
|
/// write operations are defined in [`DatabaseRw`].
|
||||||
/// TODO
|
pub trait DatabaseRo<'tx, T: Table> {
|
||||||
/// # Errors
|
/// Get the value corresponding to a key.
|
||||||
/// TODO
|
///
|
||||||
|
/// This returns a guard to the value, not the value itself.
|
||||||
|
/// See [`ValueGuard`] for more info.
|
||||||
///
|
///
|
||||||
/// This will return [`RuntimeError::KeyNotFound`] wrapped in [`Err`] if `key` does not exist.
|
|
||||||
fn get(&self, key: &T::Key) -> Result<&T::Value, RuntimeError>;
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// TODO
|
/// This will return [`RuntimeError::KeyNotFound`] wrapped in [`Err`] if `key` does not exist.
|
||||||
//
|
///
|
||||||
// TODO: (Iterators + ?Sized + lifetimes) == bad time
|
/// It will return other [`RuntimeError`]'s on things like IO errors as well.
|
||||||
// fix this later.
|
fn get<'a>(&'a self, key: &'a T::Key) -> Result<impl ValueGuard<T::Value> + 'a, RuntimeError>;
|
||||||
fn get_range<'a>(
|
|
||||||
|
/// Get an iterator of values corresponding to a range of keys.
|
||||||
|
///
|
||||||
|
/// This returns guards to the values, not the values themselves.
|
||||||
|
/// See [`ValueGuard`] for more info.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Each key in the `range` has the potential to error, for example,
|
||||||
|
/// if a particular key in the `range` does not exist,
|
||||||
|
/// [`RuntimeError::KeyNotFound`] wrapped in [`Err`] will be returned
|
||||||
|
/// from the iterator.
|
||||||
|
fn get_range<'a, Range>(
|
||||||
&'a self,
|
&'a self,
|
||||||
key: &'a T::Key,
|
range: &'a Range,
|
||||||
amount: usize,
|
) -> Result<
|
||||||
) -> Result<impl Iterator<Item = &'a T::Value>, RuntimeError>
|
impl Iterator<Item = Result<impl ValueGuard<T::Value>, RuntimeError>> + 'a,
|
||||||
|
RuntimeError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
<T as Table>::Value: 'a;
|
Range: RangeBounds<T::Key> + 'a;
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- DatabaseRw
|
//---------------------------------------------------------------------------------------------------- DatabaseRw
|
||||||
/// Database (key-value store) read/write abstraction.
|
/// Database (key-value store) read/write abstraction.
|
||||||
///
|
///
|
||||||
/// TODO: document relation between `DatabaseRo` <-> `DatabaseRw`.
|
/// All [`DatabaseRo`] functions are also callable by [`DatabaseRw`].
|
||||||
pub trait DatabaseRw<T: Table>: DatabaseRo<T> {
|
pub trait DatabaseRw<'env, 'tx, T: Table>: DatabaseRo<'tx, T> {
|
||||||
/// TODO
|
/// Insert a key-value pair into the database.
|
||||||
|
///
|
||||||
|
/// This will overwrite any existing key-value pairs.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// TODO
|
/// This will not return [`RuntimeError::KeyExists`].
|
||||||
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError>;
|
fn put(&mut self, key: &T::Key, value: &T::Value) -> Result<(), RuntimeError>;
|
||||||
|
|
||||||
/// TODO
|
/// Delete a key-value pair in the database.
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn clear(&mut self) -> Result<(), RuntimeError>;
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
///
|
///
|
||||||
|
/// # Errors
|
||||||
/// This will return [`RuntimeError::KeyNotFound`] wrapped in [`Err`] if `key` does not exist.
|
/// This will return [`RuntimeError::KeyNotFound`] wrapped in [`Err`] if `key` does not exist.
|
||||||
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError>;
|
fn delete(&mut self, key: &T::Key) -> Result<(), RuntimeError>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
//! Abstracted database environment; `trait Env`.
|
//! Abstracted database environment; `trait Env`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::{fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
database::{DatabaseRo, DatabaseRw},
|
database::{DatabaseRo, DatabaseRw},
|
||||||
|
@ -19,6 +21,10 @@ use crate::{
|
||||||
/// Objects that implement [`Env`] _should_ probably
|
/// Objects that implement [`Env`] _should_ probably
|
||||||
/// [`Env::sync`] in their drop implementations,
|
/// [`Env::sync`] in their drop implementations,
|
||||||
/// although, no invariant relies on this (yet).
|
/// although, no invariant relies on this (yet).
|
||||||
|
///
|
||||||
|
/// # Lifetimes
|
||||||
|
/// TODO: Explain the very sequential lifetime pipeline:
|
||||||
|
/// - `ConcreteEnv` -> `'env` -> `'tx` -> `impl DatabaseR{o,w}`
|
||||||
pub trait Env: Sized {
|
pub trait Env: Sized {
|
||||||
//------------------------------------------------ Constants
|
//------------------------------------------------ Constants
|
||||||
/// Does the database backend need to be manually
|
/// Does the database backend need to be manually
|
||||||
|
@ -39,51 +45,49 @@ pub trait Env: Sized {
|
||||||
const SYNCS_PER_TX: bool;
|
const SYNCS_PER_TX: bool;
|
||||||
|
|
||||||
//------------------------------------------------ Types
|
//------------------------------------------------ Types
|
||||||
/// TODO
|
/// The struct representing the actual backend's database environment.
|
||||||
type TxRo<'env>: TxRo<'env>;
|
///
|
||||||
|
/// This is used as the `self` in [`EnvInner`] functions, so whatever
|
||||||
|
/// this type is, is what will be accessible from those functions.
|
||||||
|
///
|
||||||
|
/// # Explanation (not needed for practical use)
|
||||||
|
/// For `heed`, this is just `heed::Env`, for `redb` this is
|
||||||
|
/// `(redb::Database, redb::Durability)` as each transaction
|
||||||
|
/// needs the sync mode set during creation.
|
||||||
|
type EnvInner<'env>: EnvInner<'env, Self::TxRo<'env>, Self::TxRw<'env>>
|
||||||
|
where
|
||||||
|
Self: 'env;
|
||||||
|
|
||||||
/// TODO
|
/// The read-only transaction type of the backend.
|
||||||
type TxRw<'env>: TxRw<'env>;
|
type TxRo<'env>: TxRo<'env> + 'env
|
||||||
|
where
|
||||||
|
Self: 'env;
|
||||||
|
|
||||||
|
/// The read/write transaction type of the backend.
|
||||||
|
type TxRw<'env>: TxRw<'env> + 'env
|
||||||
|
where
|
||||||
|
Self: 'env;
|
||||||
|
|
||||||
//------------------------------------------------ Required
|
//------------------------------------------------ Required
|
||||||
/// TODO
|
/// Open the database environment, using the passed [`Config`].
|
||||||
|
///
|
||||||
|
/// # Invariants
|
||||||
|
/// This function **must** create all tables listed in [`crate::tables`].
|
||||||
|
///
|
||||||
|
/// The rest of the functions depend on the fact
|
||||||
|
/// they already exist, or else they will panic.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// TODO
|
/// This will error if the database could not be opened.
|
||||||
|
///
|
||||||
|
/// This is the only [`Env`] function that will return
|
||||||
|
/// an [`InitError`] instead of a [`RuntimeError`].
|
||||||
fn open(config: Config) -> Result<Self, InitError>;
|
fn open(config: Config) -> Result<Self, InitError>;
|
||||||
|
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn create_tables<T: Table>(&self, tx_rw: &mut Self::TxRw<'_>) -> Result<(), RuntimeError>;
|
|
||||||
|
|
||||||
/// Return the [`Config`] that this database was [`Env::open`]ed with.
|
/// Return the [`Config`] that this database was [`Env::open`]ed with.
|
||||||
fn config(&self) -> &Config;
|
fn config(&self) -> &Config;
|
||||||
|
|
||||||
/// Return the amount of actual of bytes the database is taking up on disk.
|
/// Fully sync the database caches to disk.
|
||||||
///
|
|
||||||
/// This is the current _disk_ value in bytes, not the memory map.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// This will error if either:
|
|
||||||
///
|
|
||||||
/// - [`std::fs::File::open`]
|
|
||||||
/// - [`std::fs::File::metadata`]
|
|
||||||
///
|
|
||||||
/// failed on the database file on disk.
|
|
||||||
fn disk_size_bytes(&self) -> std::io::Result<u64> {
|
|
||||||
// We have the direct PATH to the file,
|
|
||||||
// no need to use backend-specific functions.
|
|
||||||
//
|
|
||||||
// SAFETY: as we are only accessing the metadata of
|
|
||||||
// the file and not reading the bytes, it should be
|
|
||||||
// fine even with a memory mapped file being actively
|
|
||||||
// written to.
|
|
||||||
Ok(std::fs::File::open(&self.config().db_file)?
|
|
||||||
.metadata()?
|
|
||||||
.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
///
|
///
|
||||||
/// # Invariant
|
/// # Invariant
|
||||||
/// This must **fully** and **synchronously** flush the database data to disk.
|
/// This must **fully** and **synchronously** flush the database data to disk.
|
||||||
|
@ -122,47 +126,98 @@ pub trait Env: Sized {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO
|
/// Return the [`Env::EnvInner`].
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn tx_ro(&self) -> Result<Self::TxRo<'_>, RuntimeError>;
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn tx_rw(&self) -> Result<Self::TxRw<'_>, RuntimeError>;
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
///
|
///
|
||||||
/// # TODO: Invariant
|
/// # Locking behavior
|
||||||
/// This should never panic the database because the table doesn't exist.
|
/// When using the `heed` backend, [`Env::EnvInner`] is a
|
||||||
|
/// `RwLockReadGuard`, i.e., calling this function takes a
|
||||||
|
/// read lock on the `heed::Env`.
|
||||||
///
|
///
|
||||||
/// Opening/using the database [`Env`] should have an invariant
|
/// Be aware of this, as other functions (currently only
|
||||||
/// that it creates all the tables we need, such that this
|
/// [`Env::resize_map`]) will take a _write_ lock.
|
||||||
/// never returns `None`.
|
fn env_inner(&self) -> Self::EnvInner<'_>;
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn open_db_ro<T: Table>(
|
|
||||||
&self,
|
|
||||||
tx_ro: &Self::TxRo<'_>,
|
|
||||||
) -> Result<impl DatabaseRo<T>, RuntimeError>;
|
|
||||||
|
|
||||||
/// TODO
|
|
||||||
///
|
|
||||||
/// # TODO: Invariant
|
|
||||||
/// This should never panic the database because the table doesn't exist.
|
|
||||||
///
|
|
||||||
/// Opening/using the database [`Env`] should have an invariant
|
|
||||||
/// that it creates all the tables we need, such that this
|
|
||||||
/// never returns `None`.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// TODO
|
|
||||||
fn open_db_rw<T: Table>(
|
|
||||||
&self,
|
|
||||||
tx_rw: &mut Self::TxRw<'_>,
|
|
||||||
) -> Result<impl DatabaseRw<T>, RuntimeError>;
|
|
||||||
|
|
||||||
//------------------------------------------------ Provided
|
//------------------------------------------------ Provided
|
||||||
|
/// Return the amount of actual of bytes the database is taking up on disk.
|
||||||
|
///
|
||||||
|
/// This is the current _disk_ value in bytes, not the memory map.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This will error if either:
|
||||||
|
///
|
||||||
|
/// - [`std::fs::File::open`]
|
||||||
|
/// - [`std::fs::File::metadata`]
|
||||||
|
///
|
||||||
|
/// failed on the database file on disk.
|
||||||
|
fn disk_size_bytes(&self) -> std::io::Result<u64> {
|
||||||
|
// We have the direct PATH to the file,
|
||||||
|
// no need to use backend-specific functions.
|
||||||
|
//
|
||||||
|
// SAFETY: as we are only accessing the metadata of
|
||||||
|
// the file and not reading the bytes, it should be
|
||||||
|
// fine even with a memory mapped file being actively
|
||||||
|
// written to.
|
||||||
|
Ok(std::fs::File::open(&self.config().db_file)?
|
||||||
|
.metadata()?
|
||||||
|
.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- DatabaseRo
|
||||||
|
/// TODO
|
||||||
|
pub trait EnvInner<'env, Ro, Rw>
|
||||||
|
where
|
||||||
|
Self: 'env,
|
||||||
|
Ro: TxRo<'env>,
|
||||||
|
Rw: TxRw<'env>,
|
||||||
|
{
|
||||||
|
/// Create a read-only transaction.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This will only return [`RuntimeError::Io`] if it errors.
|
||||||
|
fn tx_ro(&'env self) -> Result<Ro, RuntimeError>;
|
||||||
|
|
||||||
|
/// Create a read/write transaction.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This will only return [`RuntimeError::Io`] if it errors.
|
||||||
|
fn tx_rw(&'env self) -> Result<Rw, RuntimeError>;
|
||||||
|
|
||||||
|
/// Open a database in read-only mode.
|
||||||
|
///
|
||||||
|
/// This will open the database [`Table`]
|
||||||
|
/// passed as a generic to this function.
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let db = env.open_db_ro::<Table>(&tx_ro);
|
||||||
|
/// // ^ ^
|
||||||
|
/// // database table table metadata
|
||||||
|
/// // (name, key/value type)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// As [`Table`] is `Sealed`, and all tables are created
|
||||||
|
/// upon [`Env::open`], this function will never error because
|
||||||
|
/// a table doesn't exist.
|
||||||
|
fn open_db_ro<'tx, T: Table>(
|
||||||
|
&self,
|
||||||
|
tx_ro: &'tx Ro,
|
||||||
|
) -> Result<impl DatabaseRo<'tx, T>, RuntimeError>;
|
||||||
|
|
||||||
|
/// Open a database in read/write mode.
|
||||||
|
///
|
||||||
|
/// All [`DatabaseRo`] functions are also callable
|
||||||
|
/// with the returned [`DatabaseRw`] structure.
|
||||||
|
///
|
||||||
|
/// This will open the database [`Table`]
|
||||||
|
/// passed as a generic to this function.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// As [`Table`] is `Sealed`, and all tables are created
|
||||||
|
/// upon [`Env::open`], this function will never error because
|
||||||
|
/// a table doesn't exist.
|
||||||
|
fn open_db_rw<'tx, T: Table>(
|
||||||
|
&self,
|
||||||
|
tx_rw: &'tx mut Rw,
|
||||||
|
) -> Result<impl DatabaseRw<'env, 'tx, T>, RuntimeError>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
//! Database key abstraction; `trait Key`.
|
//! Database key abstraction; `trait Key`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use std::cmp::Ordering;
|
use std::{cmp::Ordering, fmt::Debug};
|
||||||
|
|
||||||
use bytemuck::Pod;
|
use bytemuck::Pod;
|
||||||
|
|
||||||
use crate::storable::{self, Storable};
|
use crate::{
|
||||||
|
storable::{self, Storable},
|
||||||
|
ToOwnedDebug,
|
||||||
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Table
|
//---------------------------------------------------------------------------------------------------- Table
|
||||||
/// Database [`Table`](crate::table::Table) key metadata.
|
/// Database [`Table`](crate::table::Table) key metadata.
|
||||||
|
@ -106,11 +109,10 @@ impl_key! {
|
||||||
i64,
|
i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize, T: Key + Pod> Key for [T; N] {
|
impl<T: Key + Pod, const N: usize> Key for [T; N] {
|
||||||
const DUPLICATE: bool = false;
|
const DUPLICATE: bool = false;
|
||||||
const CUSTOM_COMPARE: bool = false;
|
const CUSTOM_COMPARE: bool = false;
|
||||||
|
type Primary = Self;
|
||||||
type Primary = [T; N];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Tests
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
|
|
|
@ -187,89 +187,6 @@
|
||||||
// TODO: should be removed after all `todo!()`'s are gone.
|
// TODO: should be removed after all `todo!()`'s are gone.
|
||||||
clippy::diverging_sub_expression,
|
clippy::diverging_sub_expression,
|
||||||
|
|
||||||
// FIXME:
|
|
||||||
// If #[deny(clippy::restriction)] is used, it
|
|
||||||
// enables a whole bunch of very subjective lints.
|
|
||||||
// The below disables most of the ones that are
|
|
||||||
// a bit too unwieldy.
|
|
||||||
//
|
|
||||||
// Figure out if if `clippy::restriction` should be
|
|
||||||
// used (it enables a bunch of good lints but has
|
|
||||||
// many false positives).
|
|
||||||
|
|
||||||
// clippy::single_char_lifetime_names,
|
|
||||||
// clippy::implicit_return,
|
|
||||||
// clippy::std_instead_of_alloc,
|
|
||||||
// clippy::std_instead_of_core,
|
|
||||||
// clippy::unwrap_used,
|
|
||||||
// clippy::min_ident_chars,
|
|
||||||
// clippy::absolute_paths,
|
|
||||||
// clippy::missing_inline_in_public_items,
|
|
||||||
// clippy::shadow_reuse,
|
|
||||||
// clippy::shadow_unrelated,
|
|
||||||
// clippy::missing_trait_methods,
|
|
||||||
// clippy::pub_use,
|
|
||||||
// clippy::pub_with_shorthand,
|
|
||||||
// clippy::blanket_clippy_restriction_lints,
|
|
||||||
// clippy::exhaustive_structs,
|
|
||||||
// clippy::exhaustive_enums,
|
|
||||||
// clippy::unsafe_derive_deserialize,
|
|
||||||
// clippy::multiple_inherent_impl,
|
|
||||||
// clippy::unreadable_literal,
|
|
||||||
// clippy::indexing_slicing,
|
|
||||||
// clippy::float_arithmetic,
|
|
||||||
// clippy::cast_possible_truncation,
|
|
||||||
// clippy::as_conversions,
|
|
||||||
// clippy::cast_precision_loss,
|
|
||||||
// clippy::cast_sign_loss,
|
|
||||||
// clippy::missing_asserts_for_indexing,
|
|
||||||
// clippy::default_numeric_fallback,
|
|
||||||
// clippy::module_inception,
|
|
||||||
// clippy::mod_module_files,
|
|
||||||
// clippy::multiple_unsafe_ops_per_block,
|
|
||||||
// clippy::too_many_lines,
|
|
||||||
// clippy::missing_assert_message,
|
|
||||||
// clippy::len_zero,
|
|
||||||
// clippy::separated_literal_suffix,
|
|
||||||
// clippy::single_call_fn,
|
|
||||||
// clippy::unreachable,
|
|
||||||
// clippy::many_single_char_names,
|
|
||||||
// clippy::redundant_pub_crate,
|
|
||||||
// clippy::decimal_literal_representation,
|
|
||||||
// clippy::option_if_let_else,
|
|
||||||
// clippy::lossy_float_literal,
|
|
||||||
// clippy::modulo_arithmetic,
|
|
||||||
// clippy::print_stdout,
|
|
||||||
// clippy::module_name_repetitions,
|
|
||||||
// clippy::no_effect,
|
|
||||||
// clippy::semicolon_outside_block,
|
|
||||||
// clippy::panic,
|
|
||||||
// clippy::question_mark_used,
|
|
||||||
// clippy::expect_used,
|
|
||||||
// clippy::integer_division,
|
|
||||||
// clippy::type_complexity,
|
|
||||||
// clippy::pattern_type_mismatch,
|
|
||||||
// clippy::arithmetic_side_effects,
|
|
||||||
// clippy::default_trait_access,
|
|
||||||
// clippy::similar_names,
|
|
||||||
// clippy::needless_pass_by_value,
|
|
||||||
// clippy::inline_always,
|
|
||||||
// clippy::if_then_some_else_none,
|
|
||||||
// clippy::arithmetic_side_effects,
|
|
||||||
// clippy::float_cmp,
|
|
||||||
// clippy::items_after_statements,
|
|
||||||
// clippy::use_debug,
|
|
||||||
// clippy::mem_forget,
|
|
||||||
// clippy::else_if_without_else,
|
|
||||||
// clippy::str_to_string,
|
|
||||||
// clippy::branches_sharing_code,
|
|
||||||
// clippy::impl_trait_in_params,
|
|
||||||
// clippy::struct_excessive_bools,
|
|
||||||
// clippy::exit,
|
|
||||||
// // This lint is actually good but
|
|
||||||
// // it sometimes hits false positive.
|
|
||||||
// clippy::self_named_module_files
|
|
||||||
|
|
||||||
clippy::module_name_repetitions,
|
clippy::module_name_repetitions,
|
||||||
clippy::module_inception,
|
clippy::module_inception,
|
||||||
clippy::redundant_pub_crate,
|
clippy::redundant_pub_crate,
|
||||||
|
@ -282,6 +199,10 @@
|
||||||
//
|
//
|
||||||
// This allows us to assume 64-bit
|
// This allows us to assume 64-bit
|
||||||
// invariants in code, e.g. `usize as u64`.
|
// invariants in code, e.g. `usize as u64`.
|
||||||
|
//
|
||||||
|
// # Safety
|
||||||
|
// As of 0d67bfb1bcc431e90c82d577bf36dd1182c807e2 (2024-04-12)
|
||||||
|
// there are invariants relying on 64-bit pointer sizes.
|
||||||
#[cfg(not(target_pointer_width = "64"))]
|
#[cfg(not(target_pointer_width = "64"))]
|
||||||
compile_error!("Cuprate is only compatible with 64-bit CPUs");
|
compile_error!("Cuprate is only compatible with 64-bit CPUs");
|
||||||
|
|
||||||
|
@ -304,7 +225,7 @@ mod database;
|
||||||
pub use database::{DatabaseRo, DatabaseRw};
|
pub use database::{DatabaseRo, DatabaseRw};
|
||||||
|
|
||||||
mod env;
|
mod env;
|
||||||
pub use env::Env;
|
pub use env::{Env, EnvInner};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::{InitError, RuntimeError};
|
pub use error::{InitError, RuntimeError};
|
||||||
|
@ -333,6 +254,12 @@ pub mod types;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
pub use transaction::{TxRo, TxRw};
|
pub use transaction::{TxRo, TxRw};
|
||||||
|
|
||||||
|
mod to_owned_debug;
|
||||||
|
pub use to_owned_debug::ToOwnedDebug;
|
||||||
|
|
||||||
|
mod value_guard;
|
||||||
|
pub use value_guard::ValueGuard;
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Feature-gated
|
//---------------------------------------------------------------------------------------------------- Feature-gated
|
||||||
#[cfg(feature = "service")]
|
#[cfg(feature = "service")]
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
|
@ -63,8 +63,8 @@ impl ResizeAlgorithm {
|
||||||
pub fn resize(&self, current_size_bytes: usize) -> NonZeroUsize {
|
pub fn resize(&self, current_size_bytes: usize) -> NonZeroUsize {
|
||||||
match self {
|
match self {
|
||||||
Self::Monero => monero(current_size_bytes),
|
Self::Monero => monero(current_size_bytes),
|
||||||
Self::FixedBytes(u) => todo!(),
|
Self::FixedBytes(add_bytes) => fixed_bytes(current_size_bytes, add_bytes.get()),
|
||||||
Self::Percent(f) => todo!(),
|
Self::Percent(f) => percent(current_size_bytes, *f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,15 @@
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
char::ToLowercase,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytemuck::{AnyBitPattern, NoUninit};
|
use bytemuck::Pod;
|
||||||
|
|
||||||
|
use crate::ToOwnedDebug;
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Storable
|
//---------------------------------------------------------------------------------------------------- Storable
|
||||||
/// A type that can be stored in the database.
|
/// A type that can be stored in the database.
|
||||||
|
@ -20,8 +23,12 @@ use bytemuck::{AnyBitPattern, NoUninit};
|
||||||
/// casted/represented as raw bytes.
|
/// casted/represented as raw bytes.
|
||||||
///
|
///
|
||||||
/// ## `bytemuck`
|
/// ## `bytemuck`
|
||||||
/// Any type that implements `bytemuck`'s [`NoUninit`] + [`AnyBitPattern`]
|
/// Any type that implements:
|
||||||
/// (and [Debug]) will automatically implement [`Storable`].
|
/// - [`bytemuck::Pod`]
|
||||||
|
/// - [`Debug`]
|
||||||
|
/// - [`ToOwned`]
|
||||||
|
///
|
||||||
|
/// will automatically implement [`Storable`].
|
||||||
///
|
///
|
||||||
/// This includes:
|
/// This includes:
|
||||||
/// - Most primitive types
|
/// - Most primitive types
|
||||||
|
@ -30,6 +37,7 @@ use bytemuck::{AnyBitPattern, NoUninit};
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use cuprate_database::*;
|
/// # use cuprate_database::*;
|
||||||
|
/// # use std::borrow::*;
|
||||||
/// let number: u64 = 0;
|
/// let number: u64 = 0;
|
||||||
///
|
///
|
||||||
/// // Into bytes.
|
/// // Into bytes.
|
||||||
|
@ -37,8 +45,8 @@ use bytemuck::{AnyBitPattern, NoUninit};
|
||||||
/// assert_eq!(into, &[0; 8]);
|
/// assert_eq!(into, &[0; 8]);
|
||||||
///
|
///
|
||||||
/// // From bytes.
|
/// // From bytes.
|
||||||
/// let from: &u64 = Storable::from_bytes(&into);
|
/// let from: u64 = *Storable::from_bytes(&into);
|
||||||
/// assert_eq!(from, &number);
|
/// assert_eq!(from, number);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Invariants
|
/// ## Invariants
|
||||||
|
@ -54,7 +62,36 @@ use bytemuck::{AnyBitPattern, NoUninit};
|
||||||
///
|
///
|
||||||
/// Most likely, the bytes are little-endian, however
|
/// Most likely, the bytes are little-endian, however
|
||||||
/// that cannot be relied upon when using this trait.
|
/// that cannot be relied upon when using this trait.
|
||||||
pub trait Storable: Debug {
|
pub trait Storable: ToOwnedDebug {
|
||||||
|
/// What is the alignment of `Self`?
|
||||||
|
///
|
||||||
|
/// For `[T]` types, this is set to the alignment of `T`.
|
||||||
|
///
|
||||||
|
/// This is used to prevent copying when unneeded, e.g.
|
||||||
|
/// `[u8] -> [u8]` does not need to account for unaligned bytes,
|
||||||
|
/// since no cast needs to occur.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// # use cuprate_database::Storable;
|
||||||
|
/// assert_eq!(<()>::ALIGN, 1);
|
||||||
|
/// assert_eq!(u8::ALIGN, 1);
|
||||||
|
/// assert_eq!(u16::ALIGN, 2);
|
||||||
|
/// assert_eq!(u32::ALIGN, 4);
|
||||||
|
/// assert_eq!(u64::ALIGN, 8);
|
||||||
|
/// assert_eq!(i8::ALIGN, 1);
|
||||||
|
/// assert_eq!(i16::ALIGN, 2);
|
||||||
|
/// assert_eq!(i32::ALIGN, 4);
|
||||||
|
/// assert_eq!(i64::ALIGN, 8);
|
||||||
|
/// assert_eq!(<[u8]>::ALIGN, 1);
|
||||||
|
/// assert_eq!(<[u64]>::ALIGN, 8);
|
||||||
|
/// assert_eq!(<[u8; 0]>::ALIGN, 1);
|
||||||
|
/// assert_eq!(<[u8; 1]>::ALIGN, 1);
|
||||||
|
/// assert_eq!(<[u8; 2]>::ALIGN, 1);
|
||||||
|
/// assert_eq!(<[u64; 2]>::ALIGN, 8);
|
||||||
|
/// ```
|
||||||
|
const ALIGN: usize;
|
||||||
|
|
||||||
/// Is this type fixed width in byte length?
|
/// Is this type fixed width in byte length?
|
||||||
///
|
///
|
||||||
/// I.e., when converting `Self` to bytes, is it
|
/// I.e., when converting `Self` to bytes, is it
|
||||||
|
@ -97,12 +134,35 @@ pub trait Storable: Debug {
|
||||||
/// Return `self` in byte form.
|
/// Return `self` in byte form.
|
||||||
fn as_bytes(&self) -> &[u8];
|
fn as_bytes(&self) -> &[u8];
|
||||||
|
|
||||||
/// Create [`Self`] from bytes.
|
/// Create a borrowed [`Self`] from bytes.
|
||||||
|
///
|
||||||
|
/// # Invariant
|
||||||
|
/// `bytes` must be perfectly aligned for `Self`
|
||||||
|
/// or else this function may cause UB.
|
||||||
|
///
|
||||||
|
/// This function _may_ panic if `bytes` isn't aligned.
|
||||||
|
///
|
||||||
|
/// # Blanket implementation
|
||||||
|
/// The blanket implementation that covers all types used
|
||||||
|
/// by `cuprate_database` will simply cast `bytes` into `Self`,
|
||||||
|
/// with no copying.
|
||||||
fn from_bytes(bytes: &[u8]) -> &Self;
|
fn from_bytes(bytes: &[u8]) -> &Self;
|
||||||
|
|
||||||
|
/// Create a [`Self`] from potentially unaligned bytes.
|
||||||
|
///
|
||||||
|
/// # Blanket implementation
|
||||||
|
/// The blanket implementation that covers all types used
|
||||||
|
/// by `cuprate_database` will **always** allocate a new buffer
|
||||||
|
/// or create a new `Self`.
|
||||||
|
fn from_bytes_unaligned(bytes: &[u8]) -> Cow<'_, Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Impl
|
//---------------------------------------------------------------------------------------------------- Impl
|
||||||
impl<T: NoUninit + AnyBitPattern + Debug> Storable for T {
|
impl<T> Storable for T
|
||||||
|
where
|
||||||
|
Self: Pod + ToOwnedDebug<OwnedDebug = T>,
|
||||||
|
{
|
||||||
|
const ALIGN: usize = std::mem::align_of::<T>();
|
||||||
const BYTE_LENGTH: Option<usize> = Some(std::mem::size_of::<T>());
|
const BYTE_LENGTH: Option<usize> = Some(std::mem::size_of::<T>());
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -111,12 +171,22 @@ impl<T: NoUninit + AnyBitPattern + Debug> Storable for T {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_bytes(bytes: &[u8]) -> &Self {
|
fn from_bytes(bytes: &[u8]) -> &T {
|
||||||
bytemuck::from_bytes(bytes)
|
bytemuck::from_bytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_bytes_unaligned(bytes: &[u8]) -> Cow<'static, Self> {
|
||||||
|
Cow::Owned(bytemuck::pod_read_unaligned(bytes))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: NoUninit + AnyBitPattern + Debug> Storable for [T] {
|
impl<T> Storable for [T]
|
||||||
|
where
|
||||||
|
T: Pod + ToOwnedDebug<OwnedDebug = T>,
|
||||||
|
Self: ToOwnedDebug<OwnedDebug = Vec<T>>,
|
||||||
|
{
|
||||||
|
const ALIGN: usize = std::mem::align_of::<T>();
|
||||||
const BYTE_LENGTH: Option<usize> = None;
|
const BYTE_LENGTH: Option<usize> = None;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -125,8 +195,13 @@ impl<T: NoUninit + AnyBitPattern + Debug> Storable for [T] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_bytes(bytes: &[u8]) -> &Self {
|
fn from_bytes(bytes: &[u8]) -> &[T] {
|
||||||
bytemuck::must_cast_slice(bytes)
|
bytemuck::cast_slice(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_bytes_unaligned(bytes: &[u8]) -> Cow<'static, Self> {
|
||||||
|
Cow::Owned(bytemuck::pod_collect_to_vec(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,14 +212,16 @@ mod test {
|
||||||
|
|
||||||
/// Serialize, deserialize, and compare that
|
/// Serialize, deserialize, and compare that
|
||||||
/// the intermediate/end results are correct.
|
/// the intermediate/end results are correct.
|
||||||
fn test_storable<const LEN: usize, T: Storable + Copy + PartialEq>(
|
fn test_storable<const LEN: usize, T>(
|
||||||
// The primitive number function that
|
// The primitive number function that
|
||||||
// converts the number into little endian bytes,
|
// converts the number into little endian bytes,
|
||||||
// e.g `u8::to_le_bytes`.
|
// e.g `u8::to_le_bytes`.
|
||||||
to_le_bytes: fn(T) -> [u8; LEN],
|
to_le_bytes: fn(T) -> [u8; LEN],
|
||||||
// A `Vec` of the numbers to test.
|
// A `Vec` of the numbers to test.
|
||||||
t: Vec<T>,
|
t: Vec<T>,
|
||||||
) {
|
) where
|
||||||
|
T: Storable + Copy + PartialEq,
|
||||||
|
{
|
||||||
for t in t {
|
for t in t {
|
||||||
let expected_bytes = to_le_bytes(t);
|
let expected_bytes = to_le_bytes(t);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
//! Database table abstraction; `trait Table`.
|
//! Database table abstraction; `trait Table`.
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Import
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
use crate::{key::Key, storable::Storable};
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::{key::Key, storable::Storable, to_owned_debug::ToOwnedDebug};
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Table
|
//---------------------------------------------------------------------------------------------------- Table
|
||||||
/// Database table metadata.
|
/// Database table metadata.
|
||||||
|
@ -12,29 +14,15 @@ use crate::{key::Key, storable::Storable};
|
||||||
/// This trait is [`Sealed`](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed).
|
/// This trait is [`Sealed`](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed).
|
||||||
///
|
///
|
||||||
/// It is, and can only be implemented on the types inside [`tables`][crate::tables].
|
/// It is, and can only be implemented on the types inside [`tables`][crate::tables].
|
||||||
pub trait Table: crate::tables::private::Sealed {
|
pub trait Table: crate::tables::private::Sealed + 'static {
|
||||||
/// Name of the database table.
|
/// Name of the database table.
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
|
|
||||||
// TODO:
|
|
||||||
//
|
|
||||||
// `redb` requires `K/V` is `'static`:
|
|
||||||
// - <https://docs.rs/redb/1.5.0/redb/struct.ReadOnlyTable.html>
|
|
||||||
// - <https://docs.rs/redb/1.5.0/redb/struct.Table.html>
|
|
||||||
//
|
|
||||||
// ...but kinda not really?
|
|
||||||
// "Note that the lifetime of the K and V type parameters does not impact
|
|
||||||
// the lifetimes of the data that is stored or retrieved from the table"
|
|
||||||
// <https://docs.rs/redb/1.5.0/redb/struct.TableDefinition.html>
|
|
||||||
//
|
|
||||||
// This might be incompatible with `heed`. We'll see
|
|
||||||
// after function bodies are actually implemented...
|
|
||||||
|
|
||||||
/// Primary key type.
|
/// Primary key type.
|
||||||
type Key: Key + 'static;
|
type Key: Key + 'static;
|
||||||
|
|
||||||
/// Value type.
|
/// Value type.
|
||||||
type Value: Storable + ?Sized + 'static;
|
type Value: Storable + 'static;
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- Tests
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
|
|
51
database/src/to_owned_debug.rs
Normal file
51
database/src/to_owned_debug.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
//! Borrowed/owned data abstraction; `trait ToOwnedDebug`.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::{key::Key, storable::Storable};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Table
|
||||||
|
/// `T: Debug` and `T::Owned: Debug`.
|
||||||
|
///
|
||||||
|
/// This trait simply combines [`Debug`] and [`ToOwned`]
|
||||||
|
/// such that the `Owned` version must also be [`Debug`].
|
||||||
|
///
|
||||||
|
/// An example is `[u8]` which is [`Debug`], and
|
||||||
|
/// its owned version `Vec<u8>` is also [`Debug`].
|
||||||
|
///
|
||||||
|
/// # Explanation (not needed for practical use)
|
||||||
|
/// This trait solely exists due to the `redb` backend
|
||||||
|
/// requiring [`Debug`] bounds on keys and values.
|
||||||
|
///
|
||||||
|
/// As we have `?Sized` types like `[u8]`, and due to `redb` requiring
|
||||||
|
/// allocation upon deserialization, we must make our values `ToOwned`.
|
||||||
|
///
|
||||||
|
/// However, this requires that the `Owned` version is also `Debug`.
|
||||||
|
/// Combined with:
|
||||||
|
/// - [`Table::Key`](crate::Table::Key)
|
||||||
|
/// - [`Table::Value`](crate::Table::Value)
|
||||||
|
/// - [`Key::Primary`]
|
||||||
|
///
|
||||||
|
/// this quickly permutates into many many many `where` bounds on
|
||||||
|
/// each function that touchs any data that must be deserialized.
|
||||||
|
///
|
||||||
|
/// This trait and the blanket impl it provides get applied all these types
|
||||||
|
/// automatically, which means we don't have to write these bounds everywhere.
|
||||||
|
pub trait ToOwnedDebug: Debug + ToOwned<Owned = Self::OwnedDebug> {
|
||||||
|
/// The owned version of [`Self`].
|
||||||
|
///
|
||||||
|
/// Should be equal to `<T as ToOwned>::Owned`.
|
||||||
|
type OwnedDebug: Debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The blanket impl that covers all our types.
|
||||||
|
impl<O: Debug, T: ToOwned<Owned = O> + Debug + ?Sized> ToOwnedDebug for T {
|
||||||
|
type OwnedDebug = O;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
// use super::*;
|
||||||
|
}
|
|
@ -6,24 +6,43 @@ use crate::{config::SyncMode, env::Env, error::RuntimeError};
|
||||||
//---------------------------------------------------------------------------------------------------- TxRo
|
//---------------------------------------------------------------------------------------------------- TxRo
|
||||||
/// Read-only database transaction.
|
/// Read-only database transaction.
|
||||||
///
|
///
|
||||||
/// TODO
|
/// Returned from [`EnvInner::tx_ro`](crate::EnvInner::tx_ro).
|
||||||
|
///
|
||||||
|
/// # TODO
|
||||||
|
/// I don't think we need this, we can just drop the `tx_ro`?
|
||||||
|
/// <https://docs.rs/heed/0.20.0-alpha.9/heed/struct.RoTxn.html#method.commit>
|
||||||
pub trait TxRo<'env> {
|
pub trait TxRo<'env> {
|
||||||
/// TODO
|
/// Commit the read-only transaction.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// TODO
|
/// This operation is infallible (will always return `Ok(())`) with the `redb` backend.
|
||||||
fn commit(self) -> Result<(), RuntimeError>;
|
fn commit(self) -> Result<(), RuntimeError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------------------------------------------- TxRw
|
//---------------------------------------------------------------------------------------------------- TxRw
|
||||||
/// Read/write database transaction.
|
/// Read/write database transaction.
|
||||||
///
|
///
|
||||||
/// TODO
|
/// Returned from [`EnvInner::tx_rw`](crate::EnvInner::tx_rw).
|
||||||
pub trait TxRw<'env> {
|
pub trait TxRw<'env> {
|
||||||
/// TODO
|
/// Commit the read/write transaction.
|
||||||
|
///
|
||||||
|
/// Note that this doesn't necessarily sync the database caches to disk.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// TODO
|
/// This operation is infallible (will always return `Ok(())`) with the `redb` backend.
|
||||||
|
///
|
||||||
|
/// Else, this will only return:
|
||||||
|
/// - [`RuntimeError::ResizeNeeded`] (if `Env::MANUAL_RESIZE == true`)
|
||||||
|
/// - [`RuntimeError::Io`]
|
||||||
fn commit(self) -> Result<(), RuntimeError>;
|
fn commit(self) -> Result<(), RuntimeError>;
|
||||||
|
|
||||||
/// TODO
|
/// Abort the transaction, erasing any writes that have occurred.
|
||||||
fn abort(self);
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This operation is infallible (will always return `Ok(())`) with the `heed` backend.
|
||||||
|
///
|
||||||
|
/// Else, this will only return:
|
||||||
|
/// - [`RuntimeError::ResizeNeeded`] (if `Env::MANUAL_RESIZE == true`)
|
||||||
|
/// - [`RuntimeError::Io`]
|
||||||
|
fn abort(self) -> Result<(), RuntimeError>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,15 +53,25 @@ use serde::{Deserialize, Serialize};
|
||||||
/// TEST
|
/// TEST
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use cuprate_database::types::*;
|
/// # use cuprate_database::{*, types::*};
|
||||||
|
/// // Assert bytemuck is correct.
|
||||||
/// let a = TestType { u: 1, b: 255, _pad: [0; 7] }; // original struct
|
/// let a = TestType { u: 1, b: 255, _pad: [0; 7] }; // original struct
|
||||||
/// let b = bytemuck::must_cast::<TestType, [u8; 16]>(a); // cast into bytes
|
/// let b = bytemuck::must_cast::<TestType, [u8; 16]>(a); // cast into bytes
|
||||||
/// let c = bytemuck::checked::cast::<[u8; 16], TestType>(b); // cast back into struct
|
/// let c = bytemuck::checked::cast::<[u8; 16], TestType>(b); // cast back into struct
|
||||||
///
|
|
||||||
/// assert_eq!(a, c);
|
/// assert_eq!(a, c);
|
||||||
/// assert_eq!(c.u, 1);
|
/// assert_eq!(c.u, 1);
|
||||||
/// assert_eq!(c.b, 255);
|
/// assert_eq!(c.b, 255);
|
||||||
/// assert_eq!(c._pad, [0; 7]);
|
/// assert_eq!(c._pad, [0; 7]);
|
||||||
|
///
|
||||||
|
/// // Assert Storable is correct.
|
||||||
|
/// let b2 = Storable::as_bytes(&a);
|
||||||
|
/// let c2: &TestType = Storable::from_bytes(b2);
|
||||||
|
/// assert_eq!(a, *c2);
|
||||||
|
/// assert_eq!(b, b2);
|
||||||
|
/// assert_eq!(c, *c2);
|
||||||
|
/// assert_eq!(c2.u, 1);
|
||||||
|
/// assert_eq!(c2.b, 255);
|
||||||
|
/// assert_eq!(c2._pad, [0; 7]);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Size & Alignment
|
/// # Size & Alignment
|
||||||
|
@ -94,14 +104,23 @@ pub struct TestType {
|
||||||
/// TEST2
|
/// TEST2
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use cuprate_database::types::*;
|
/// # use cuprate_database::{*, types::*};
|
||||||
|
/// // Assert bytemuck is correct.
|
||||||
/// let a = TestType2 { u: 1, b: [1; 32] }; // original struct
|
/// let a = TestType2 { u: 1, b: [1; 32] }; // original struct
|
||||||
/// let b = bytemuck::must_cast::<TestType2, [u8; 40]>(a); // cast into bytes
|
/// let b = bytemuck::must_cast::<TestType2, [u8; 40]>(a); // cast into bytes
|
||||||
/// let c = bytemuck::must_cast::<[u8; 40], TestType2>(b); // cast back into struct
|
/// let c = bytemuck::must_cast::<[u8; 40], TestType2>(b); // cast back into struct
|
||||||
///
|
|
||||||
/// assert_eq!(a, c);
|
/// assert_eq!(a, c);
|
||||||
/// assert_eq!(c.u, 1);
|
/// assert_eq!(c.u, 1);
|
||||||
/// assert_eq!(c.b, [1; 32]);
|
/// assert_eq!(c.b, [1; 32]);
|
||||||
|
///
|
||||||
|
/// // Assert Storable is correct.
|
||||||
|
/// let b2 = Storable::as_bytes(&a);
|
||||||
|
/// let c2: &TestType2 = Storable::from_bytes(b2);
|
||||||
|
/// assert_eq!(a, *c2);
|
||||||
|
/// assert_eq!(b, b2);
|
||||||
|
/// assert_eq!(c, *c2);
|
||||||
|
/// assert_eq!(c.u, 1);
|
||||||
|
/// assert_eq!(c.b, [1; 32]);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Size & Alignment
|
/// # Size & Alignment
|
||||||
|
|
47
database/src/value_guard.rs
Normal file
47
database/src/value_guard.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
//! Database table value "guard" abstraction; `trait ValueGuard`.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Import
|
||||||
|
use std::borrow::{Borrow, Cow};
|
||||||
|
|
||||||
|
use crate::{table::Table, Storable, ToOwnedDebug};
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Table
|
||||||
|
/// A guard that allows you to access a value.
|
||||||
|
///
|
||||||
|
/// This trait acts as an object that must be kept alive,
|
||||||
|
/// and will give you access to a [`Table`]'s value.
|
||||||
|
///
|
||||||
|
/// # Explanation (not needed for practical use)
|
||||||
|
/// This trait solely exists due to the `redb` backend
|
||||||
|
/// not _directly_ returning the value, but a
|
||||||
|
/// [guard object](https://docs.rs/redb/1.5.0/redb/struct.AccessGuard.html)
|
||||||
|
/// that has a lifetime attached to the key.
|
||||||
|
/// It does not implement `Deref` or `Borrow` and such.
|
||||||
|
///
|
||||||
|
/// Also, due to `redb` requiring `Cow`, this object builds on that.
|
||||||
|
///
|
||||||
|
/// - `heed` will always be `Cow::Borrowed`
|
||||||
|
/// - `redb` will always be `Cow::Borrowed` for `[u8]`
|
||||||
|
/// or any type where `Storable::ALIGN == 1`
|
||||||
|
/// - `redb` will always be `Cow::Owned` for everything else
|
||||||
|
pub trait ValueGuard<T: ToOwnedDebug> {
|
||||||
|
/// Retrieve the data from the guard.
|
||||||
|
fn unguard(&self) -> Cow<'_, T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToOwnedDebug> ValueGuard<T> for Cow<'_, T> {
|
||||||
|
#[inline]
|
||||||
|
fn unguard(&self) -> Cow<'_, T> {
|
||||||
|
Cow::Borrowed(self.borrow())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK:
|
||||||
|
// This is implemented for `redb::AccessGuard<'_>` in
|
||||||
|
// `src/backend/redb/storable.rs` due to struct privacy.
|
||||||
|
|
||||||
|
//---------------------------------------------------------------------------------------------------- Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
// use super::*;
|
||||||
|
}
|
Loading…
Reference in a new issue