mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-13 18:44:52 -07:00
global: mono repo
This commit is contained in:
Generated
+733
-53
File diff suppressed because it is too large
Load Diff
+13
-6
@@ -59,33 +59,40 @@ brk_traversable = { version = "0.3.6", path = "crates/brk_traversable", features
|
||||
brk_traversable_derive = { version = "0.3.6", path = "crates/brk_traversable_derive" }
|
||||
brk_types = { version = "0.3.6", path = "crates/brk_types" }
|
||||
brk_website = { version = "0.3.6", path = "crates/brk_website" }
|
||||
byteview = "0.10.1"
|
||||
byteview = "0.10.2"
|
||||
color-eyre = "0.6.5"
|
||||
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
|
||||
corepc-types = { version = "0.15.0", features = ["std"], default-features = false }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
fjall = { path = "../fjall", version = "3.1.8" }
|
||||
fjall = { path = "crates/fjall", version = "0.3.6" }
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.34", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
jiff = { version = "0.2.35", features = ["perf-inline", "tz-system"], default-features = false }
|
||||
libc = "0.2"
|
||||
log = "0.4.33"
|
||||
lsm-tree = { path = "crates/lsm-tree", version = "0.3.6", default-features = false }
|
||||
lz4_flex = { version = "0.13.1", default-features = false }
|
||||
owo-colors = "4.3.0"
|
||||
parking_lot = "0.12.5"
|
||||
pco = "1.0.2"
|
||||
quickmatch = { path = "crates/quickmatch", version = "0.3.6" }
|
||||
rawdb = { path = "crates/rawdb", version = "0.3.6" }
|
||||
rayon = "1.12.0"
|
||||
rapidhash = "4.5.1"
|
||||
rustc-hash = "2.1.3"
|
||||
schemars = { version = "1.2.1", features = ["indexmap2"] }
|
||||
schemars = { version = "1.2.2", features = ["indexmap2"] }
|
||||
serde = "1.0.229"
|
||||
serde_bytes = "0.11.19"
|
||||
serde_derive = "1.0.229"
|
||||
serde_json = { version = "1.0.151", features = ["float_roundtrip", "preserve_order"] }
|
||||
smallvec = "1.15.2"
|
||||
tempfile = "3.27.0"
|
||||
tokio = { version = "1.53.1", features = ["rt-multi-thread"] }
|
||||
tower-http = { version = "0.7.0", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
ureq = { version = "3.3.0", features = ["json"] }
|
||||
# vecdb = { version = "0.10.4", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { path = "crates/vecdb", version = "0.3.6", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb_derive = { path = "crates/vecdb_derive", version = "0.3.6" }
|
||||
|
||||
|
||||
[workspace.metadata.release]
|
||||
|
||||
@@ -25,7 +25,7 @@ owo-colors = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = "1.1.3"
|
||||
toml = "1.1.4"
|
||||
vecdb = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
|
||||
+5064
-1202
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@ use brk_types::{
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BytesVec, Database, ImportOptions, ImportableVec, ReadableVec, Reader,
|
||||
Rw, Stamp, StorageMode, WritableVec,
|
||||
AnyStoredVec, AnyVec, BytesVec, Database, ImportOptions, ImportableVec, ReadableVec, Rw, Stamp,
|
||||
StorageMode, WritableVec,
|
||||
};
|
||||
|
||||
use super::super::AddrTypeToTypeIndexMap;
|
||||
@@ -57,15 +57,6 @@ macro_rules! define_any_addr_indexes_vecs {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get address index for a given type and type_index.
|
||||
/// Uses get_any_or_read_at to check updated layer (needed after rollback).
|
||||
pub(crate) fn get(&self, addr_type: OutputType, type_index: TypeIndex, reader: &Reader) -> Result<AnyAddrIndex> {
|
||||
match addr_type {
|
||||
$(OutputType::$variant => Ok(self.$field.get_any_or_read_at(type_index.into(), reader)?.unwrap()),)*
|
||||
_ => unreachable!("Invalid addr type: {:?}", addr_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a parallel iterator over all vecs for parallel writing.
|
||||
pub(crate) fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
vec![$(&mut self.$field as &mut dyn AnyStoredVec),*].into_par_iter()
|
||||
|
||||
+8
-18
@@ -1,5 +1,4 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_types::{
|
||||
AnyAddrDataIndexEnum, EmptyAddrData, FundedAddrData, OutputType, TxIndex, TypeIndex,
|
||||
};
|
||||
@@ -99,38 +98,29 @@ pub(crate) fn load_uncached_addr_data(
|
||||
vr: &VecsReaders,
|
||||
any_addr_indexes: &AnyAddrIndexesVecs,
|
||||
addrs_data: &AddrsDataVecs,
|
||||
) -> Result<Option<WithAddrDataSource<FundedAddrData>>> {
|
||||
) -> Option<WithAddrDataSource<FundedAddrData>> {
|
||||
// Check if this is a new address (type_index >= first for this height)
|
||||
let first = *first_addr_indexes.get(addr_type).unwrap();
|
||||
if first <= type_index {
|
||||
return Ok(Some(WithAddrDataSource::New(FundedAddrData::default())));
|
||||
return Some(WithAddrDataSource::New(FundedAddrData::default()));
|
||||
}
|
||||
|
||||
// Skip if already in cache
|
||||
if cache.contains(addr_type, type_index) {
|
||||
return Ok(None);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read from storage
|
||||
let reader = vr.addr_reader(addr_type);
|
||||
let any_addr_index = any_addr_indexes.get(addr_type, type_index, reader)?;
|
||||
let any_addr_index = vr.any_addr_index(any_addr_indexes, addr_type, type_index);
|
||||
|
||||
Ok(Some(match any_addr_index.to_enum() {
|
||||
Some(match any_addr_index.to_enum() {
|
||||
AnyAddrDataIndexEnum::Funded(funded_index) => {
|
||||
let reader = &vr.any_addr_index_to_any_addr_data.funded;
|
||||
let funded_data = addrs_data
|
||||
.funded
|
||||
.get_any_or_read_at(funded_index.into(), reader)?
|
||||
.unwrap();
|
||||
let funded_data = vr.funded_data(addrs_data, funded_index);
|
||||
WithAddrDataSource::FromFunded(funded_index, funded_data)
|
||||
}
|
||||
AnyAddrDataIndexEnum::Empty(empty_index) => {
|
||||
let reader = &vr.any_addr_index_to_any_addr_data.empty;
|
||||
let empty_data = addrs_data
|
||||
.empty
|
||||
.get_any_or_read_at(empty_index.into(), reader)?
|
||||
.unwrap();
|
||||
let empty_data = vr.empty_data(addrs_data, empty_index);
|
||||
WithAddrDataSource::FromEmpty(empty_index, empty_data.into())
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_types::{FundedAddrData, Height, OutputType, Sats, TxIndex, TypeIndex};
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -56,8 +55,8 @@ pub(crate) fn process_inputs(
|
||||
vr: &VecsReaders,
|
||||
any_addr_indexes: &AnyAddrIndexesVecs,
|
||||
addrs_data: &AddrsDataVecs,
|
||||
) -> Result<InputsResult> {
|
||||
let map_fn = |local_idx: usize| -> Result<_> {
|
||||
) -> InputsResult {
|
||||
let map_fn = |local_idx: usize| {
|
||||
let tx_index = txin_index_to_tx_index[local_idx];
|
||||
|
||||
let prev_height = txin_index_to_prev_height[local_idx];
|
||||
@@ -65,7 +64,7 @@ pub(crate) fn process_inputs(
|
||||
let input_type = txin_index_to_output_type[local_idx];
|
||||
|
||||
if input_type.is_not_addr() {
|
||||
return Ok((prev_height, value, input_type, None));
|
||||
return (prev_height, value, input_type, None);
|
||||
}
|
||||
|
||||
let type_index = txin_index_to_type_index[local_idx];
|
||||
@@ -79,23 +78,20 @@ pub(crate) fn process_inputs(
|
||||
vr,
|
||||
any_addr_indexes,
|
||||
addrs_data,
|
||||
)?;
|
||||
);
|
||||
|
||||
Ok((
|
||||
(
|
||||
prev_height,
|
||||
value,
|
||||
input_type,
|
||||
Some((type_index, tx_index, value, addr_data_opt)),
|
||||
))
|
||||
)
|
||||
};
|
||||
|
||||
let items: Vec<_> = if input_count < 128 {
|
||||
(0..input_count).map(map_fn).collect::<Result<Vec<_>>>()?
|
||||
(0..input_count).map(map_fn).collect()
|
||||
} else {
|
||||
(0..input_count)
|
||||
.into_par_iter()
|
||||
.map(map_fn)
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
(0..input_count).into_par_iter().map(map_fn).collect()
|
||||
};
|
||||
|
||||
// Phase 2: Sequential accumulation - no merge overhead
|
||||
@@ -140,10 +136,10 @@ pub(crate) fn process_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InputsResult {
|
||||
InputsResult {
|
||||
height_to_sent,
|
||||
sent_data,
|
||||
addr_data,
|
||||
tx_index_vecs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_types::{FundedAddrData, Sats, TxIndex, TypeIndex};
|
||||
use rayon::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
@@ -43,17 +42,17 @@ pub(crate) fn process_outputs(
|
||||
vr: &VecsReaders,
|
||||
any_addr_indexes: &AnyAddrIndexesVecs,
|
||||
addrs_data: &AddrsDataVecs,
|
||||
) -> Result<OutputsResult> {
|
||||
) -> OutputsResult {
|
||||
let output_count = txout_data_vec.len();
|
||||
|
||||
// Phase 1: Addr lookups (mmap reads) — parallel for large blocks, sequential for small
|
||||
let map_fn = |local_idx: usize| -> Result<_> {
|
||||
let map_fn = |local_idx: usize| {
|
||||
let txout_data = &txout_data_vec[local_idx];
|
||||
let value = txout_data.value;
|
||||
let output_type = txout_data.output_type;
|
||||
|
||||
if output_type.is_not_addr() {
|
||||
return Ok((value, output_type, None));
|
||||
return (value, output_type, None);
|
||||
}
|
||||
|
||||
let type_index = txout_data.type_index;
|
||||
@@ -67,22 +66,19 @@ pub(crate) fn process_outputs(
|
||||
vr,
|
||||
any_addr_indexes,
|
||||
addrs_data,
|
||||
)?;
|
||||
);
|
||||
|
||||
Ok((
|
||||
(
|
||||
value,
|
||||
output_type,
|
||||
Some((type_index, tx_index, value, addr_data_opt)),
|
||||
))
|
||||
)
|
||||
};
|
||||
|
||||
let items: Vec<_> = if output_count < 128 {
|
||||
(0..output_count).map(map_fn).collect::<Result<Vec<_>>>()?
|
||||
(0..output_count).map(map_fn).collect()
|
||||
} else {
|
||||
(0..output_count)
|
||||
.into_par_iter()
|
||||
.map(map_fn)
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
(0..output_count).into_par_iter().map(map_fn).collect()
|
||||
};
|
||||
|
||||
// Phase 2: Sequential accumulation
|
||||
@@ -117,10 +113,10 @@ pub(crate) fn process_outputs(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OutputsResult {
|
||||
OutputsResult {
|
||||
transacted,
|
||||
received_data,
|
||||
addr_data,
|
||||
tx_index_vecs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,13 +266,13 @@ pub(crate) fn process_blocks(
|
||||
// Collection (build tx_index mappings + bulk mmap reads) is merged into the
|
||||
// processing closures so outputs and inputs collection overlap each other
|
||||
// and tick-tock, instead of running sequentially before the join.
|
||||
let (matured, oi_result) = rayon::join(
|
||||
let (matured, (outputs_result, inputs_result)) = rayon::join(
|
||||
|| {
|
||||
vecs.utxo_cohorts
|
||||
.tick_tock_next_block(chain_state, timestamp)
|
||||
},
|
||||
|| -> Result<_> {
|
||||
let (outputs_result, inputs_result) = rayon::join(
|
||||
|| {
|
||||
rayon::join(
|
||||
|| {
|
||||
let txout_index_to_tx_index = txout_to_tx_index_buf.build(
|
||||
first_tx_index,
|
||||
@@ -291,7 +291,7 @@ pub(crate) fn process_blocks(
|
||||
&vecs.addrs_data,
|
||||
)
|
||||
},
|
||||
|| -> Result<_> {
|
||||
|| {
|
||||
if input_count > 1 {
|
||||
let txin_index_to_tx_index = txin_to_tx_index_buf.build(
|
||||
first_tx_index,
|
||||
@@ -322,19 +322,17 @@ pub(crate) fn process_blocks(
|
||||
&vecs.addrs_data,
|
||||
)
|
||||
} else {
|
||||
Ok(InputsResult {
|
||||
InputsResult {
|
||||
height_to_sent: Default::default(),
|
||||
sent_data: Default::default(),
|
||||
addr_data: Default::default(),
|
||||
tx_index_vecs: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
Ok((outputs_result?, inputs_result?))
|
||||
)
|
||||
},
|
||||
);
|
||||
let (outputs_result, inputs_result) = oi_result?;
|
||||
|
||||
// Merge new address data into current cache
|
||||
cache.merge_funded(outputs_result.addr_data);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use brk_cohort::{ByAddrType, ByAnyAddr};
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Height, OutPoint, OutputType, Sats, StoredU64, TxIndex, TypeIndex};
|
||||
use vecdb::{ReadableVec, Reader, VecIndex};
|
||||
use brk_types::{
|
||||
AnyAddrIndex, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, OutPoint,
|
||||
OutputType, P2AAddrIndex, P2PK33AddrIndex, P2PK65AddrIndex, P2PKHAddrIndex, P2SHAddrIndex,
|
||||
P2TRAddrIndex, P2WPKHAddrIndex, P2WSHAddrIndex, Sats, StoredU64, TxIndex, TypeIndex,
|
||||
};
|
||||
use vecdb::{BytesVecReader, ReadableVec, VecIndex};
|
||||
|
||||
use crate::{
|
||||
distribution::{
|
||||
@@ -159,35 +162,67 @@ impl<'a> TxInReaders<'a> {
|
||||
|
||||
/// Cached readers for stateful vectors.
|
||||
pub struct VecsReaders {
|
||||
pub addr_type_index_to_any_addr_index: ByAddrType<Reader>,
|
||||
pub any_addr_index_to_any_addr_data: ByAnyAddr<Reader>,
|
||||
p2a: BytesVecReader<P2AAddrIndex, AnyAddrIndex>,
|
||||
p2pk33: BytesVecReader<P2PK33AddrIndex, AnyAddrIndex>,
|
||||
p2pk65: BytesVecReader<P2PK65AddrIndex, AnyAddrIndex>,
|
||||
p2pkh: BytesVecReader<P2PKHAddrIndex, AnyAddrIndex>,
|
||||
p2sh: BytesVecReader<P2SHAddrIndex, AnyAddrIndex>,
|
||||
p2tr: BytesVecReader<P2TRAddrIndex, AnyAddrIndex>,
|
||||
p2wpkh: BytesVecReader<P2WPKHAddrIndex, AnyAddrIndex>,
|
||||
p2wsh: BytesVecReader<P2WSHAddrIndex, AnyAddrIndex>,
|
||||
funded: BytesVecReader<FundedAddrIndex, FundedAddrData>,
|
||||
empty: BytesVecReader<EmptyAddrIndex, EmptyAddrData>,
|
||||
}
|
||||
|
||||
impl VecsReaders {
|
||||
pub(crate) fn new(any_addr_indexes: &AnyAddrIndexesVecs, addrs_data: &AddrsDataVecs) -> Self {
|
||||
Self {
|
||||
addr_type_index_to_any_addr_index: ByAddrType {
|
||||
p2a: any_addr_indexes.p2a.create_reader(),
|
||||
p2pk33: any_addr_indexes.p2pk33.create_reader(),
|
||||
p2pk65: any_addr_indexes.p2pk65.create_reader(),
|
||||
p2pkh: any_addr_indexes.p2pkh.create_reader(),
|
||||
p2sh: any_addr_indexes.p2sh.create_reader(),
|
||||
p2tr: any_addr_indexes.p2tr.create_reader(),
|
||||
p2wpkh: any_addr_indexes.p2wpkh.create_reader(),
|
||||
p2wsh: any_addr_indexes.p2wsh.create_reader(),
|
||||
},
|
||||
any_addr_index_to_any_addr_data: ByAnyAddr {
|
||||
funded: addrs_data.funded.create_reader(),
|
||||
empty: addrs_data.empty.create_reader(),
|
||||
},
|
||||
p2a: any_addr_indexes.p2a.reader(),
|
||||
p2pk33: any_addr_indexes.p2pk33.reader(),
|
||||
p2pk65: any_addr_indexes.p2pk65.reader(),
|
||||
p2pkh: any_addr_indexes.p2pkh.reader(),
|
||||
p2sh: any_addr_indexes.p2sh.reader(),
|
||||
p2tr: any_addr_indexes.p2tr.reader(),
|
||||
p2wpkh: any_addr_indexes.p2wpkh.reader(),
|
||||
p2wsh: any_addr_indexes.p2wsh.reader(),
|
||||
funded: addrs_data.funded.reader(),
|
||||
empty: addrs_data.empty.reader(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reader for specific address type.
|
||||
pub(crate) fn addr_reader(&self, addr_type: OutputType) -> &Reader {
|
||||
self.addr_type_index_to_any_addr_index
|
||||
.get(addr_type)
|
||||
.unwrap()
|
||||
/// Read the unified address index, including uncommitted updates after rollback.
|
||||
pub(crate) fn any_addr_index(
|
||||
&self,
|
||||
vecs: &AnyAddrIndexesVecs,
|
||||
addr_type: OutputType,
|
||||
type_index: TypeIndex,
|
||||
) -> AnyAddrIndex {
|
||||
let index = match addr_type {
|
||||
OutputType::P2A => vecs.p2a.get_with_reader(type_index.into(), &self.p2a),
|
||||
OutputType::P2PK33 => vecs.p2pk33.get_with_reader(type_index.into(), &self.p2pk33),
|
||||
OutputType::P2PK65 => vecs.p2pk65.get_with_reader(type_index.into(), &self.p2pk65),
|
||||
OutputType::P2PKH => vecs.p2pkh.get_with_reader(type_index.into(), &self.p2pkh),
|
||||
OutputType::P2SH => vecs.p2sh.get_with_reader(type_index.into(), &self.p2sh),
|
||||
OutputType::P2TR => vecs.p2tr.get_with_reader(type_index.into(), &self.p2tr),
|
||||
OutputType::P2WPKH => vecs.p2wpkh.get_with_reader(type_index.into(), &self.p2wpkh),
|
||||
OutputType::P2WSH => vecs.p2wsh.get_with_reader(type_index.into(), &self.p2wsh),
|
||||
_ => unreachable!("invalid address type: {addr_type:?}"),
|
||||
};
|
||||
index.unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn funded_data(
|
||||
&self,
|
||||
vecs: &AddrsDataVecs,
|
||||
index: FundedAddrIndex,
|
||||
) -> FundedAddrData {
|
||||
vecs.funded.get_with_reader(index, &self.funded).unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn empty_data(&self, vecs: &AddrsDataVecs, index: EmptyAddrIndex) -> EmptyAddrData {
|
||||
vecs.empty.get_with_reader(index, &self.empty).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -350,6 +350,44 @@ impl Vecs {
|
||||
) -> Result<()> {
|
||||
self.db.sync_bg_tasks()?;
|
||||
|
||||
let base_version = VERSION
|
||||
+ [
|
||||
prices.spot.cents.height.version(),
|
||||
indexes.timestamp.monotonic.version(),
|
||||
indexer.vecs.transactions.first_tx_index.version(),
|
||||
indexer.vecs.outputs.first_txout_index.version(),
|
||||
indexer.vecs.inputs.first_txin_index.version(),
|
||||
transactions.count.total.block.version(),
|
||||
outputs.count.total.sum.version(),
|
||||
inputs.count.sum.version(),
|
||||
indexes.tx_index.output_count.version(),
|
||||
indexes.tx_index.input_count.version(),
|
||||
indexer.vecs.outputs.value.version(),
|
||||
indexer.vecs.outputs.output_type.version(),
|
||||
indexer.vecs.outputs.type_index.version(),
|
||||
inputs.spent.value.version(),
|
||||
indexer.vecs.inputs.outpoint.version(),
|
||||
indexer.vecs.inputs.output_type.version(),
|
||||
indexer.vecs.inputs.type_index.version(),
|
||||
indexer.vecs.addrs.p2pk65.first_index.version(),
|
||||
indexer.vecs.addrs.p2pk33.first_index.version(),
|
||||
indexer.vecs.addrs.p2pkh.first_index.version(),
|
||||
indexer.vecs.addrs.p2sh.first_index.version(),
|
||||
indexer.vecs.addrs.p2wpkh.first_index.version(),
|
||||
indexer.vecs.addrs.p2wsh.first_index.version(),
|
||||
indexer.vecs.addrs.p2tr.first_index.version(),
|
||||
indexer.vecs.addrs.p2a.first_index.version(),
|
||||
]
|
||||
.into_iter()
|
||||
.sum::<Version>();
|
||||
|
||||
debug!("validating computed versions");
|
||||
self.supply_state
|
||||
.validate_computed_version_or_reset(base_version)?;
|
||||
self.utxo_cohorts.validate_computed_versions(base_version)?;
|
||||
self.addr_cohorts.validate_computed_versions(base_version)?;
|
||||
debug!("computed versions validated");
|
||||
|
||||
let starting_lengths = indexer.safe_lengths();
|
||||
|
||||
// 1. Find minimum height we have data for across stateful vecs
|
||||
@@ -503,13 +541,6 @@ impl Vecs {
|
||||
recovered_height
|
||||
};
|
||||
|
||||
// 2c. Validate computed versions
|
||||
debug!("validating computed versions");
|
||||
let base_version = VERSION;
|
||||
self.utxo_cohorts.validate_computed_versions(base_version)?;
|
||||
self.addr_cohorts.validate_computed_versions(base_version)?;
|
||||
debug!("computed versions validated");
|
||||
|
||||
// 3. Get last height from indexer
|
||||
let last_height = Height::from(indexer.vecs.blocks.blockhash.len().saturating_sub(1));
|
||||
debug!(
|
||||
|
||||
@@ -70,8 +70,7 @@ impl Vecs {
|
||||
if entry.tx_index.is_coinbase() {
|
||||
break;
|
||||
}
|
||||
entry.txout_index =
|
||||
first_txout_index_reader.get(entry.tx_index.to_usize()) + entry.vout;
|
||||
entry.txout_index = first_txout_index_reader.get(entry.tx_index) + entry.vout;
|
||||
}
|
||||
|
||||
// Sort 2: by txout_index (sequential value reads)
|
||||
@@ -80,7 +79,7 @@ impl Vecs {
|
||||
if entry.txout_index.is_coinbase() {
|
||||
break;
|
||||
}
|
||||
entry.value = value_reader.get(entry.txout_index.to_usize());
|
||||
entry.value = value_reader.get(entry.txout_index);
|
||||
}
|
||||
|
||||
// Scatter-write to output buffers using original_idx (avoids Sort 3)
|
||||
|
||||
@@ -5,7 +5,7 @@ use brk_types::{
|
||||
Bitcoin, Cents, Date, Day1, Dollars, Height, PartsPerMillionSigned64, Sats, Version,
|
||||
};
|
||||
use vecdb::{
|
||||
BinaryTransform, CachedBoxedVec, CheckedSub, EagerVec, ImportableVec, PcoVec,
|
||||
AnyVec, BinaryTransform, CachedBoxedVec, CheckedSub, EagerVec, ImportableVec, PcoVec,
|
||||
ReadableCloneableVec, ReadableVec, TypedVec, VecIndex,
|
||||
};
|
||||
|
||||
@@ -33,8 +33,9 @@ impl Vecs {
|
||||
let db = open_db(parent_path, super::DB_NAME, 50_000)?;
|
||||
let version = parent_version;
|
||||
|
||||
let sats_cumulative_version = version + prices.split.close.usd.day1.version();
|
||||
let sats_cumulative: EagerVec<PcoVec<Height, Sats>> =
|
||||
ImportableVec::forced_import(&db, "dca_sats_cumulative", version)?;
|
||||
ImportableVec::forced_import(&db, "dca_sats_cumulative", sats_cumulative_version)?;
|
||||
let sats_per_day = LazyPreviousDeltaVec::new(
|
||||
"dca_sats_per_day",
|
||||
version,
|
||||
|
||||
@@ -121,6 +121,7 @@ impl Computer {
|
||||
let mining = Box::new(mining::Vecs::forced_import(
|
||||
&computed_path,
|
||||
VERSION,
|
||||
indexer,
|
||||
&indexes,
|
||||
&cached_starts,
|
||||
)?);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::Version;
|
||||
|
||||
use crate::{
|
||||
@@ -17,13 +18,14 @@ impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
parent_path: &Path,
|
||||
parent_version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let db = open_db(parent_path, super::DB_NAME, 1_000_000)?;
|
||||
let version = parent_version;
|
||||
|
||||
let rewards = RewardsVecs::forced_import(&db, version, indexes, cached_starts)?;
|
||||
let rewards = RewardsVecs::forced_import(&db, version, indexer, indexes, cached_starts)?;
|
||||
let hashrate = HashrateVecs::forced_import(&db, version, indexes)?;
|
||||
|
||||
let this = Self {
|
||||
|
||||
@@ -28,7 +28,12 @@ impl Vecs {
|
||||
prices,
|
||||
&indexer.vecs.transactions.first_tx_index,
|
||||
|_, tx_index| {
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut count_cursor = indexes.tx_index.output_count.cursor();
|
||||
|
||||
let ti = tx_index.to_usize();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, EagerVec, ImportableVec};
|
||||
use vecdb::{AnyVec, Database, EagerVec, ImportableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
@@ -16,9 +17,15 @@ impl Vecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let coinbase_version = version
|
||||
+ indexer.vecs.transactions.first_txout_index.version()
|
||||
+ indexes.tx_index.output_count.version()
|
||||
+ indexer.vecs.outputs.value.version();
|
||||
|
||||
let fee_dominance =
|
||||
PercentCumulativeRolling::forced_import(db, "fee_dominance", version, indexes)?;
|
||||
|
||||
@@ -32,7 +39,7 @@ impl Vecs {
|
||||
coinbase: ValuePerBlockCumulativeRolling::forced_import(
|
||||
db,
|
||||
"coinbase",
|
||||
version,
|
||||
coinbase_version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
|
||||
@@ -41,8 +41,13 @@ impl Vecs {
|
||||
let txid_len = indexer.vecs.transactions.txid.len();
|
||||
let total_txout_len = indexer.vecs.outputs.output_type.len();
|
||||
|
||||
let mut otype_cursor = indexer.vecs.outputs.output_type.cursor();
|
||||
let mut fo_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut otype_cursor = indexer.vecs.outputs.output_type.reader().cursor();
|
||||
let fo_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut height = skip;
|
||||
|
||||
walk_blocks(
|
||||
|
||||
@@ -18,6 +18,13 @@ impl Vecs {
|
||||
) -> Result<ExitGuard> {
|
||||
let starting_lengths = indexer.safe_lengths();
|
||||
|
||||
let dep_version = inputs.spent.txout_index.version()
|
||||
+ indexer.vecs.outputs.first_txout_index.version()
|
||||
+ indexer.vecs.inputs.first_txin_index.version()
|
||||
+ indexer.vecs.outputs.value.version();
|
||||
self.txin_index
|
||||
.validate_computed_version_or_reset(dep_version)?;
|
||||
|
||||
let target_height = indexer.vecs.blocks.blockhash.len();
|
||||
if target_height == 0 {
|
||||
return Ok(exit.lock());
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::{collections::BTreeMap, path::Path};
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Addr, AddrBytes, Height, OutputType, PoolSlug, Pools, TxOutIndex, pools};
|
||||
use brk_types::{
|
||||
Addr, AddrBytes, Height, OutputType, POOL_ATTRIBUTION_VERSION, PoolSlug, Pools, TxOutIndex,
|
||||
pools,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{
|
||||
AnyStoredVec, AnyVec, BytesVec, Database, Exit, ImportableVec, ReadableVec, Rw, StorageMode,
|
||||
@@ -49,7 +52,10 @@ impl Vecs {
|
||||
let db = open_db(parent_path, DB_NAME, 100_000)?;
|
||||
let pools = pools();
|
||||
|
||||
let version = parent_version + Version::new(4) + Version::new(pools.len() as u32);
|
||||
let version = parent_version
|
||||
+ Version::new(4)
|
||||
+ POOL_ATTRIBUTION_VERSION
|
||||
+ Version::new(pools.len() as u32);
|
||||
|
||||
let mut major_map = BTreeMap::new();
|
||||
let mut minor_map = BTreeMap::new();
|
||||
@@ -121,7 +127,24 @@ impl Vecs {
|
||||
) -> Result<()> {
|
||||
let starting_height = indexer.safe_lengths().height;
|
||||
|
||||
let dep_version = indexer.vecs.blocks.coinbase_tag.version();
|
||||
let dep_version: Version = [
|
||||
indexer.vecs.blocks.coinbase_tag.version(),
|
||||
indexer.vecs.transactions.first_tx_index.version(),
|
||||
indexer.vecs.transactions.first_txout_index.version(),
|
||||
indexes.tx_index.output_count.version(),
|
||||
indexer.vecs.outputs.output_type.version(),
|
||||
indexer.vecs.outputs.type_index.version(),
|
||||
indexer.vecs.addrs.p2pk65.bytes.version(),
|
||||
indexer.vecs.addrs.p2pk33.bytes.version(),
|
||||
indexer.vecs.addrs.p2pkh.bytes.version(),
|
||||
indexer.vecs.addrs.p2sh.bytes.version(),
|
||||
indexer.vecs.addrs.p2wpkh.bytes.version(),
|
||||
indexer.vecs.addrs.p2wsh.bytes.version(),
|
||||
indexer.vecs.addrs.p2tr.bytes.version(),
|
||||
indexer.vecs.addrs.p2a.bytes.version(),
|
||||
]
|
||||
.into_iter()
|
||||
.sum();
|
||||
let pool_vec_version = self.pool.header().vec_version();
|
||||
let pool_computed = self.pool.header().computed_version();
|
||||
let expected = pool_vec_version + dep_version;
|
||||
@@ -167,7 +190,7 @@ impl Vecs {
|
||||
len,
|
||||
|coinbase_tag| -> Result<()> {
|
||||
let tx_index = first_tx_index_cursor.next().unwrap();
|
||||
let out_start = first_txout_index.get(tx_index.to_usize());
|
||||
let out_start = first_txout_index.get(tx_index);
|
||||
|
||||
let ti = tx_index.to_usize();
|
||||
output_count_cursor.advance(ti - output_count_cursor.position());
|
||||
@@ -176,17 +199,17 @@ impl Vecs {
|
||||
let pool = (*out_start..(*out_start + *output_count_val))
|
||||
.map(TxOutIndex::from)
|
||||
.find_map(|txout_index| {
|
||||
let ot = output_type.get(txout_index.to_usize());
|
||||
let ti = usize::from(type_index.get(txout_index.to_usize()));
|
||||
let ot = output_type.get(txout_index);
|
||||
let ti = usize::from(type_index.get(txout_index));
|
||||
match ot {
|
||||
OutputType::P2PK65 => Some(AddrBytes::from(p2pk65.get(ti))),
|
||||
OutputType::P2PK33 => Some(AddrBytes::from(p2pk33.get(ti))),
|
||||
OutputType::P2PKH => Some(AddrBytes::from(p2pkh.get(ti))),
|
||||
OutputType::P2SH => Some(AddrBytes::from(p2sh.get(ti))),
|
||||
OutputType::P2WPKH => Some(AddrBytes::from(p2wpkh.get(ti))),
|
||||
OutputType::P2WSH => Some(AddrBytes::from(p2wsh.get(ti))),
|
||||
OutputType::P2TR => Some(AddrBytes::from(p2tr.get(ti))),
|
||||
OutputType::P2A => Some(AddrBytes::from(p2a.get(ti))),
|
||||
OutputType::P2PK65 => Some(AddrBytes::from(p2pk65.get_at(ti))),
|
||||
OutputType::P2PK33 => Some(AddrBytes::from(p2pk33.get_at(ti))),
|
||||
OutputType::P2PKH => Some(AddrBytes::from(p2pkh.get_at(ti))),
|
||||
OutputType::P2SH => Some(AddrBytes::from(p2sh.get_at(ti))),
|
||||
OutputType::P2WPKH => Some(AddrBytes::from(p2wpkh.get_at(ti))),
|
||||
OutputType::P2WSH => Some(AddrBytes::from(p2wsh.get_at(ti))),
|
||||
OutputType::P2TR => Some(AddrBytes::from(p2tr.get_at(ti))),
|
||||
OutputType::P2A => Some(AddrBytes::from(p2a.get_at(ti))),
|
||||
_ => None,
|
||||
}
|
||||
.map(|bytes| Addr::try_from(&bytes).unwrap())
|
||||
|
||||
@@ -14,7 +14,9 @@ impl PoolHeights {
|
||||
let mut map: FxHashMap<PoolSlug, Vec<Height>> = FxHashMap::default();
|
||||
let reader = pool.reader();
|
||||
for h in 0..len {
|
||||
map.entry(reader.get(h)).or_default().push(Height::from(h));
|
||||
map.entry(reader.get_at(h))
|
||||
.or_default()
|
||||
.push(Height::from(h));
|
||||
}
|
||||
Self(Arc::new(RwLock::new(map)))
|
||||
}
|
||||
|
||||
@@ -63,8 +63,16 @@ impl Vecs {
|
||||
fn compute_prices(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> {
|
||||
let starting_height = indexer.safe_lengths().height;
|
||||
|
||||
let source_version =
|
||||
indexer.vecs.outputs.value.version() + indexer.vecs.outputs.output_type.version();
|
||||
let source_version = [
|
||||
indexer.vecs.transactions.txid.version(),
|
||||
indexer.vecs.transactions.first_tx_index.version(),
|
||||
indexer.vecs.outputs.first_txout_index.version(),
|
||||
indexer.vecs.transactions.first_txout_index.version(),
|
||||
indexer.vecs.outputs.value.version(),
|
||||
indexer.vecs.outputs.output_type.version(),
|
||||
]
|
||||
.into_iter()
|
||||
.sum();
|
||||
self.spot
|
||||
.cents
|
||||
.height
|
||||
|
||||
@@ -101,9 +101,9 @@ impl Vecs {
|
||||
let mut input_value = spent.value.cursor();
|
||||
let mut input_type = indexer.vecs.inputs.output_type.cursor();
|
||||
let mut input_type_index = indexer.vecs.inputs.type_index.cursor();
|
||||
let mut output_value = indexer.vecs.outputs.value.cursor();
|
||||
let mut output_type = indexer.vecs.outputs.output_type.cursor();
|
||||
let mut output_type_index = indexer.vecs.outputs.type_index.cursor();
|
||||
let mut output_value = indexer.vecs.outputs.value.reader().cursor();
|
||||
let mut output_type = indexer.vecs.outputs.output_type.reader().cursor();
|
||||
let mut output_type_index = indexer.vecs.outputs.type_index.reader().cursor();
|
||||
let mut has_op_return = features.has_op_return.cursor();
|
||||
let mut has_inscription = features.has_inscription.cursor();
|
||||
let mut tx_count = indexes.height.tx_index_count.cursor();
|
||||
|
||||
@@ -12,6 +12,7 @@ use brk_reader::{Reader, XORBytes};
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{BlockHash, Height};
|
||||
use fjall::PersistMode;
|
||||
use rayon::prelude::*;
|
||||
use tracing::{debug, error, info};
|
||||
use vecdb::{
|
||||
Exit, RawDBError, ReadOnlyClone, ReadableVec, Ro, Rw, StorageMode, WritableVec, unlikely,
|
||||
@@ -332,9 +333,9 @@ impl Indexer {
|
||||
|
||||
if !tasks.is_empty() {
|
||||
let i = Instant::now();
|
||||
for task in tasks {
|
||||
task().map_err(vecdb::RawDBError::other)?;
|
||||
}
|
||||
tasks
|
||||
.into_par_iter()
|
||||
.try_for_each(|task| task().map_err(vecdb::RawDBError::other))?;
|
||||
debug!("Stores committed in {:?}", i.elapsed());
|
||||
|
||||
let i = Instant::now();
|
||||
|
||||
@@ -78,7 +78,7 @@ impl<'a> BlockProcessor<'a> {
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.get_pushed_or_read(prev_tx_index, &self.readers.txid)
|
||||
.get_append_only(prev_tx_index, &self.readers.txid)
|
||||
.ok_or(Error::Internal("Missing txid for tx_index"))
|
||||
.inspect_err(|_| {
|
||||
error!(?tx_index, len, "Missing txid for tx_index");
|
||||
@@ -104,10 +104,10 @@ impl<'a> BlockProcessor<'a> {
|
||||
&mut self,
|
||||
txs: Vec<ComputedTx>,
|
||||
mut txouts: Vec<ProcessedOutput>,
|
||||
txins: Vec<InputSource>,
|
||||
txins: &[InputSource],
|
||||
addresses: &mut BlockAddresses,
|
||||
) -> Result<()> {
|
||||
let transaction_analyses = self.analyze_transactions(&txs, &txins, &txouts);
|
||||
let transaction_analyses = self.analyze_transactions(&txs, txins, &txouts);
|
||||
let lengths = &mut *self.lengths;
|
||||
let base_tx_index = lengths.tx_index;
|
||||
let base_txin_index = lengths.txin_index;
|
||||
|
||||
@@ -18,11 +18,11 @@ use super::{BlockProcessor, transaction::ComputedTx, txout::ProcessedOutput};
|
||||
use crate::InputsVecs;
|
||||
|
||||
impl<'a> BlockProcessor<'a> {
|
||||
pub(crate) fn process_inputs(
|
||||
pub(crate) fn process_inputs<'b>(
|
||||
&self,
|
||||
txs: &[ComputedTx],
|
||||
resolver: &mut InputResolver,
|
||||
) -> Result<Vec<InputSource>> {
|
||||
resolver: &'b mut InputResolver,
|
||||
) -> Result<&'b [InputSource]> {
|
||||
resolver.resolve(self, txs)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub(super) fn finalize_inputs(
|
||||
inputs: &mut InputsVecs,
|
||||
addr_tx_index_stores: &mut ByAddrType<Store<AddrIndexTxIndex, Unit>>,
|
||||
addr_outpoint_stores: &mut ByAddrType<Store<AddrIndexOutPoint, Unit>>,
|
||||
txins: Vec<InputSource>,
|
||||
txins: &[InputSource],
|
||||
txouts: &[ProcessedOutput],
|
||||
) -> Result<()> {
|
||||
let mut input_offset = 0;
|
||||
|
||||
@@ -12,13 +12,15 @@ use vecdb::unlikely;
|
||||
use super::InputSource;
|
||||
use crate::processor::{BlockProcessor, transaction::ComputedTx};
|
||||
|
||||
const PARALLEL_PARENT_READ_THRESHOLD: usize = 1_000;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InputResolver {
|
||||
same_block_transactions: FxHashMap<TxidPrefix, TxIndex>,
|
||||
previous_parent_indexes: FxHashMap<TxidPrefix, usize>,
|
||||
previous_parents: Vec<PreviousParent>,
|
||||
parent_locations: FxHashMap<TxidPrefix, ParentLocation>,
|
||||
previous_parent_prefixes: Vec<TxidPrefix>,
|
||||
inputs: Vec<UnresolvedInput>,
|
||||
reads: ReadBatch,
|
||||
resolved: Vec<InputSource>,
|
||||
}
|
||||
|
||||
impl InputResolver {
|
||||
@@ -26,65 +28,78 @@ impl InputResolver {
|
||||
&mut self,
|
||||
processor: &BlockProcessor<'_>,
|
||||
txs: &[ComputedTx<'_>],
|
||||
) -> Result<Vec<InputSource>> {
|
||||
) -> Result<&[InputSource]> {
|
||||
self.prepare(txs, processor.lengths.tx_index);
|
||||
self.reads.resolve(
|
||||
processor,
|
||||
&self.previous_parents,
|
||||
&self.previous_parent_prefixes,
|
||||
&self.inputs,
|
||||
processor.lengths.tx_index,
|
||||
)?;
|
||||
|
||||
let tracks_executed_legacy_sigops = processor.tracks_executed_legacy_sigops();
|
||||
let reads = &self.reads;
|
||||
let inputs = &self.inputs;
|
||||
|
||||
self.inputs
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(input_index, input)| match *input {
|
||||
UnresolvedInput::Coinbase => Ok(InputSource::Coinbase),
|
||||
UnresolvedInput::SameBlock {
|
||||
outpoint,
|
||||
txout_offset,
|
||||
} => Ok(InputSource::SameBlock {
|
||||
outpoint,
|
||||
txout_offset,
|
||||
}),
|
||||
UnresolvedInput::PreviousBlock { parent_index, vout } => {
|
||||
let parent = reads.parent(parent_index);
|
||||
let outpoint = OutPoint::new(parent.tx_index, vout);
|
||||
let (output_type, type_index) = reads.output(input_index);
|
||||
|
||||
let legacy_sigops = if tracks_executed_legacy_sigops {
|
||||
processor
|
||||
.vecs
|
||||
.scripts
|
||||
.legacy_sigops(output_type, type_index, &processor.readers.scripts)
|
||||
.ok_or(Error::Internal("Missing legacy_sigops"))?
|
||||
} else {
|
||||
SigOps::ZERO
|
||||
};
|
||||
|
||||
Ok(InputSource::PreviousBlock {
|
||||
self.resolved.clear();
|
||||
self.resolved.resize(inputs.len(), InputSource::Coinbase);
|
||||
self.resolved.par_iter_mut().enumerate().try_for_each(
|
||||
|(input_index, resolved)| -> Result<()> {
|
||||
match inputs[input_index] {
|
||||
UnresolvedInput::Coinbase => {
|
||||
*resolved = InputSource::Coinbase;
|
||||
Ok(())
|
||||
}
|
||||
UnresolvedInput::SameBlock {
|
||||
outpoint,
|
||||
output_type,
|
||||
legacy_sigops,
|
||||
type_index,
|
||||
})
|
||||
txout_offset,
|
||||
} => {
|
||||
*resolved = InputSource::SameBlock {
|
||||
outpoint,
|
||||
txout_offset,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
UnresolvedInput::PreviousBlock { parent_index, vout } => {
|
||||
let parent = reads.parent(parent_index);
|
||||
let outpoint = OutPoint::new(parent.tx_index, vout);
|
||||
let (output_type, type_index) = reads.output(input_index);
|
||||
|
||||
let legacy_sigops = if tracks_executed_legacy_sigops {
|
||||
processor
|
||||
.vecs
|
||||
.scripts
|
||||
.legacy_sigops(output_type, type_index, &processor.readers.scripts)
|
||||
.ok_or(Error::Internal("Missing legacy_sigops"))?
|
||||
} else {
|
||||
SigOps::ZERO
|
||||
};
|
||||
|
||||
*resolved = InputSource::PreviousBlock {
|
||||
outpoint,
|
||||
output_type,
|
||||
legacy_sigops,
|
||||
type_index,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(&self.resolved)
|
||||
}
|
||||
|
||||
fn prepare(&mut self, txs: &[ComputedTx<'_>], block_first_tx_index: TxIndex) {
|
||||
self.same_block_transactions.clear();
|
||||
self.previous_parent_indexes.clear();
|
||||
self.previous_parents.clear();
|
||||
self.parent_locations.clear();
|
||||
self.previous_parent_prefixes.clear();
|
||||
self.inputs.clear();
|
||||
|
||||
self.same_block_transactions.reserve(txs.len());
|
||||
self.same_block_transactions
|
||||
.extend(txs.iter().map(|tx| (tx.txid_prefix(), tx.tx_index)));
|
||||
self.parent_locations.reserve(txs.len());
|
||||
self.parent_locations.extend(
|
||||
txs.iter()
|
||||
.map(|tx| (tx.txid_prefix(), ParentLocation::SameBlock(tx.tx_index))),
|
||||
);
|
||||
|
||||
let total_inputs = txs.iter().map(|tx| tx.tx.input.len()).sum();
|
||||
self.inputs.reserve(total_inputs);
|
||||
@@ -101,22 +116,25 @@ impl InputResolver {
|
||||
let txid_prefix = TxidPrefix::from(&txid);
|
||||
let vout = Vout::from(previous_output.vout);
|
||||
|
||||
if let Some(&tx_index) = self.same_block_transactions.get(&txid_prefix) {
|
||||
let block_tx_index = usize::from(tx_index) - usize::from(block_first_tx_index);
|
||||
self.inputs.push(UnresolvedInput::SameBlock {
|
||||
outpoint: OutPoint::new(tx_index, vout),
|
||||
txout_offset: txs[block_tx_index].txout_offset(vout),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent_index = match self.previous_parent_indexes.entry(txid_prefix) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
let parent_index = match self.parent_locations.entry(txid_prefix) {
|
||||
Entry::Occupied(entry) => match *entry.get() {
|
||||
ParentLocation::SameBlock(tx_index) => {
|
||||
let block_tx_index =
|
||||
usize::from(tx_index) - usize::from(block_first_tx_index);
|
||||
self.inputs.push(UnresolvedInput::SameBlock {
|
||||
outpoint: OutPoint::new(tx_index, vout),
|
||||
txout_offset: txs[block_tx_index].txout_offset(vout),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
ParentLocation::Previous(parent_index) => parent_index.to_usize(),
|
||||
},
|
||||
Entry::Vacant(entry) => {
|
||||
let parent_index = self.previous_parents.len();
|
||||
entry.insert(parent_index);
|
||||
self.previous_parents
|
||||
.push(PreviousParent { txid, txid_prefix });
|
||||
let parent_index = self.previous_parent_prefixes.len();
|
||||
entry.insert(ParentLocation::Previous(PreviousParentIndex::new(
|
||||
parent_index,
|
||||
)));
|
||||
self.previous_parent_prefixes.push(txid_prefix);
|
||||
parent_index
|
||||
}
|
||||
};
|
||||
@@ -129,14 +147,32 @@ impl InputResolver {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PreviousParent {
|
||||
txid: Txid,
|
||||
txid_prefix: TxidPrefix,
|
||||
enum ParentLocation {
|
||||
SameBlock(TxIndex),
|
||||
Previous(PreviousParentIndex),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PreviousParentIndex(u32);
|
||||
|
||||
impl PreviousParentIndex {
|
||||
fn new(index: usize) -> Self {
|
||||
Self(
|
||||
u32::try_from(index)
|
||||
.expect("number of unique previous parents in a block must fit in u32"),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_usize(self) -> usize {
|
||||
self.0 as usize
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<ParentLocation>() == 8);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ParentRead {
|
||||
original_index: usize,
|
||||
tx_index: TxIndex,
|
||||
first_txout_index: TxOutIndex,
|
||||
}
|
||||
@@ -150,9 +186,7 @@ struct OutputRead {
|
||||
#[derive(Default)]
|
||||
struct ReadBatch {
|
||||
parents: Vec<ParentRead>,
|
||||
parent_positions: Vec<usize>,
|
||||
outputs: Vec<OutputRead>,
|
||||
output_positions: Vec<usize>,
|
||||
output_types: Vec<OutputType>,
|
||||
type_indices: Vec<TypeIndex>,
|
||||
}
|
||||
@@ -161,11 +195,11 @@ impl ReadBatch {
|
||||
fn resolve(
|
||||
&mut self,
|
||||
processor: &BlockProcessor<'_>,
|
||||
previous_parents: &[PreviousParent],
|
||||
previous_parent_prefixes: &[TxidPrefix],
|
||||
inputs: &[UnresolvedInput],
|
||||
current_tx_index: TxIndex,
|
||||
) -> Result<()> {
|
||||
self.resolve_parents(processor, previous_parents, current_tx_index)?;
|
||||
self.resolve_parents(processor, previous_parent_prefixes, current_tx_index)?;
|
||||
self.prepare_outputs(inputs);
|
||||
self.read_outputs(processor)
|
||||
}
|
||||
@@ -173,61 +207,66 @@ impl ReadBatch {
|
||||
fn resolve_parents(
|
||||
&mut self,
|
||||
processor: &BlockProcessor<'_>,
|
||||
previous_parents: &[PreviousParent],
|
||||
previous_parent_prefixes: &[TxidPrefix],
|
||||
current_tx_index: TxIndex,
|
||||
) -> Result<()> {
|
||||
let parallel_raw_reads = previous_parent_prefixes.len() >= PARALLEL_PARENT_READ_THRESHOLD;
|
||||
|
||||
self.parents.clear();
|
||||
self.parents.extend(
|
||||
(0..previous_parents.len()).map(|original_index| ParentRead {
|
||||
original_index,
|
||||
self.parents.resize(
|
||||
previous_parent_prefixes.len(),
|
||||
ParentRead {
|
||||
tx_index: TxIndex::default(),
|
||||
first_txout_index: TxOutIndex::default(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
self.parents
|
||||
.par_iter_mut()
|
||||
.zip(previous_parent_prefixes.par_iter())
|
||||
.try_for_each(|read| {
|
||||
let parent = &previous_parents[read.original_index];
|
||||
let (read, txid_prefix) = read;
|
||||
let store_result = processor
|
||||
.stores
|
||||
.txid_prefix_to_tx_index
|
||||
.get(&parent.txid_prefix)?
|
||||
.get(txid_prefix)?
|
||||
.map(|value| *value);
|
||||
|
||||
let tx_index = match store_result {
|
||||
Some(tx_index) if tx_index < current_tx_index => tx_index,
|
||||
_ => {
|
||||
error!(
|
||||
"UnknownTxid: txid={}, prefix={:?}, store_result={:?}, current_tx_index={:?}",
|
||||
parent.txid,
|
||||
parent.txid_prefix,
|
||||
store_result,
|
||||
current_tx_index
|
||||
"UnknownTxid: prefix={:?}, store_result={:?}, current_tx_index={:?}",
|
||||
txid_prefix, store_result, current_tx_index
|
||||
);
|
||||
return Err(Error::UnknownTxid);
|
||||
}
|
||||
};
|
||||
|
||||
read.tx_index = tx_index;
|
||||
if parallel_raw_reads {
|
||||
read.first_txout_index = processor
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.get_append_only(tx_index, &processor.readers.tx_index_to_first_txout_index)
|
||||
.ok_or(Error::Internal("Missing txout_index"))?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.parents.sort_unstable_by_key(|read| read.tx_index);
|
||||
self.parent_positions.clear();
|
||||
self.parent_positions.resize(self.parents.len(), 0);
|
||||
|
||||
for (position, read) in self.parents.iter_mut().enumerate() {
|
||||
self.parent_positions[read.original_index] = position;
|
||||
read.first_txout_index = processor
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.get_pushed_or_read(
|
||||
read.tx_index,
|
||||
&processor.readers.tx_index_to_first_txout_index,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing txout_index"))?;
|
||||
if !parallel_raw_reads {
|
||||
for read in &mut self.parents {
|
||||
read.first_txout_index = processor
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.get_append_only(
|
||||
read.tx_index,
|
||||
&processor.readers.tx_index_to_first_txout_index,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing txout_index"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -247,55 +286,47 @@ impl ReadBatch {
|
||||
}
|
||||
}
|
||||
|
||||
self.outputs.sort_unstable_by_key(|read| read.txout_index);
|
||||
self.output_positions.clear();
|
||||
self.output_positions.resize(inputs.len(), 0);
|
||||
|
||||
for (position, read) in self.outputs.iter().enumerate() {
|
||||
self.output_positions[read.input_index] = position;
|
||||
}
|
||||
self.output_types.clear();
|
||||
self.output_types.resize(inputs.len(), OutputType::Unknown);
|
||||
self.type_indices.clear();
|
||||
self.type_indices.resize(inputs.len(), TypeIndex::default());
|
||||
}
|
||||
|
||||
fn read_outputs(&mut self, processor: &BlockProcessor<'_>) -> Result<()> {
|
||||
self.output_types.clear();
|
||||
self.output_types.reserve(self.outputs.len());
|
||||
self.type_indices.clear();
|
||||
self.type_indices.reserve(self.outputs.len());
|
||||
|
||||
let outputs = &self.outputs;
|
||||
if outputs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output_types = &mut self.output_types;
|
||||
let type_indices = &mut self.type_indices;
|
||||
|
||||
let (output_types_result, type_indices_result) = rayon::join(
|
||||
|| -> Result<()> {
|
||||
for read in outputs {
|
||||
output_types.push(
|
||||
processor
|
||||
.vecs
|
||||
.outputs
|
||||
.output_type
|
||||
.get_pushed_or_read(
|
||||
read.txout_index,
|
||||
&processor.readers.txout_index_to_output_type,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing output_type"))?,
|
||||
);
|
||||
output_types[read.input_index] = processor
|
||||
.vecs
|
||||
.outputs
|
||||
.output_type
|
||||
.get_append_only(
|
||||
read.txout_index,
|
||||
&processor.readers.txout_index_to_output_type,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing output_type"))?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|| -> Result<()> {
|
||||
for read in outputs {
|
||||
type_indices.push(
|
||||
processor
|
||||
.vecs
|
||||
.outputs
|
||||
.type_index
|
||||
.get_pushed_or_read(
|
||||
read.txout_index,
|
||||
&processor.readers.txout_index_to_type_index,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing type_index"))?,
|
||||
);
|
||||
type_indices[read.input_index] = processor
|
||||
.vecs
|
||||
.outputs
|
||||
.type_index
|
||||
.get_append_only(
|
||||
read.txout_index,
|
||||
&processor.readers.txout_index_to_type_index,
|
||||
)
|
||||
.ok_or(Error::Internal("Missing type_index"))?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -306,12 +337,14 @@ impl ReadBatch {
|
||||
}
|
||||
|
||||
fn parent(&self, original_index: usize) -> ParentRead {
|
||||
self.parents[self.parent_positions[original_index]]
|
||||
self.parents[original_index]
|
||||
}
|
||||
|
||||
fn output(&self, input_index: usize) -> (OutputType, TypeIndex) {
|
||||
let position = self.output_positions[input_index];
|
||||
(self.output_types[position], self.type_indices[position])
|
||||
(
|
||||
self.output_types[input_index],
|
||||
self.type_indices[input_index],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use brk_types::{OutPoint, OutputType, SigOps, TypeIndex};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum InputSource {
|
||||
Coinbase,
|
||||
PreviousBlock {
|
||||
|
||||
@@ -24,14 +24,14 @@ impl AddrReaders {
|
||||
pub fn script_pubkey(&self, output_type: OutputType, type_index: TypeIndex) -> ScriptBuf {
|
||||
let idx = usize::from(type_index);
|
||||
let bytes: Option<AddrBytes> = match output_type {
|
||||
OutputType::P2PK65 => self.p2pk65.try_get(idx).map(Into::into),
|
||||
OutputType::P2PK33 => self.p2pk33.try_get(idx).map(Into::into),
|
||||
OutputType::P2PKH => self.p2pkh.try_get(idx).map(Into::into),
|
||||
OutputType::P2SH => self.p2sh.try_get(idx).map(Into::into),
|
||||
OutputType::P2WPKH => self.p2wpkh.try_get(idx).map(Into::into),
|
||||
OutputType::P2WSH => self.p2wsh.try_get(idx).map(Into::into),
|
||||
OutputType::P2TR => self.p2tr.try_get(idx).map(Into::into),
|
||||
OutputType::P2A => self.p2a.try_get(idx).map(Into::into),
|
||||
OutputType::P2PK65 => self.p2pk65.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2PK33 => self.p2pk33.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2PKH => self.p2pkh.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2SH => self.p2sh.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2WPKH => self.p2wpkh.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2WSH => self.p2wsh.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2TR => self.p2tr.try_get_at(idx).map(Into::into),
|
||||
OutputType::P2A => self.p2a.try_get_at(idx).map(Into::into),
|
||||
_ => None,
|
||||
};
|
||||
bytes.map(|b| b.to_script_pubkey()).unwrap_or_default()
|
||||
|
||||
@@ -332,13 +332,13 @@ impl Stores {
|
||||
.collect_range_at(rollback_start, rollback_end);
|
||||
|
||||
for (i, txout_index) in (rollback_start..rollback_end).enumerate() {
|
||||
let output_type = txout_index_to_output_type_reader.get(txout_index);
|
||||
let output_type = txout_index_to_output_type_reader.get_at(txout_index);
|
||||
if !output_type.is_addr() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr_type = output_type;
|
||||
let addr_index = txout_index_to_type_index_reader.get(txout_index);
|
||||
let addr_index = txout_index_to_type_index_reader.get_at(txout_index);
|
||||
let tx_index = tx_indexes[i];
|
||||
|
||||
addr_index_tx_index_to_remove.insert((addr_type, addr_index, tx_index));
|
||||
@@ -346,7 +346,7 @@ impl Stores {
|
||||
let vout = Vout::from(
|
||||
txout_index
|
||||
- tx_index_to_first_txout_index_reader
|
||||
.get(tx_index.to_usize())
|
||||
.get(tx_index)
|
||||
.to_usize(),
|
||||
);
|
||||
let outpoint = OutPoint::new(tx_index, vout);
|
||||
@@ -371,12 +371,11 @@ impl Stores {
|
||||
|
||||
let output_tx_index = outpoint.tx_index();
|
||||
let vout = outpoint.vout();
|
||||
let txout_index =
|
||||
tx_index_to_first_txout_index_reader.get(output_tx_index.to_usize()) + vout;
|
||||
let txout_index = tx_index_to_first_txout_index_reader.get(output_tx_index) + vout;
|
||||
|
||||
if txout_index < starting_lengths.txout_index {
|
||||
let output_type = txout_index_to_output_type_reader.get(txout_index.to_usize());
|
||||
let type_index = txout_index_to_type_index_reader.get(txout_index.to_usize());
|
||||
let output_type = txout_index_to_output_type_reader.get(txout_index);
|
||||
let type_index = txout_index_to_type_index_reader.get(txout_index);
|
||||
Some((outpoint, output_type, type_index, spending_tx_index))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -233,42 +233,42 @@ impl AddrsVecs {
|
||||
OutputType::P2PK65 => self
|
||||
.p2pk65
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2pk65)
|
||||
.get_append_only(type_index.into(), &readers.p2pk65)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2PK33 => self
|
||||
.p2pk33
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2pk33)
|
||||
.get_append_only(type_index.into(), &readers.p2pk33)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2PKH => self
|
||||
.p2pkh
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2pkh)
|
||||
.get_append_only(type_index.into(), &readers.p2pkh)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2SH => self
|
||||
.p2sh
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2sh)
|
||||
.get_append_only(type_index.into(), &readers.p2sh)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2WPKH => self
|
||||
.p2wpkh
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2wpkh)
|
||||
.get_append_only(type_index.into(), &readers.p2wpkh)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2WSH => self
|
||||
.p2wsh
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2wsh)
|
||||
.get_append_only(type_index.into(), &readers.p2wsh)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2TR => self
|
||||
.p2tr
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2tr)
|
||||
.get_append_only(type_index.into(), &readers.p2tr)
|
||||
.map(AddrBytes::from),
|
||||
OutputType::P2A => self
|
||||
.p2a
|
||||
.bytes
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2a)
|
||||
.get_append_only(type_index.into(), &readers.p2a)
|
||||
.map(AddrBytes::from),
|
||||
_ => unreachable!("get_bytes_by_type called with non-address type"),
|
||||
}
|
||||
@@ -302,7 +302,7 @@ impl AddrsVecs {
|
||||
Some(mut index) => {
|
||||
let reader = $addr.bytes.reader();
|
||||
Ok(Box::new(std::iter::from_fn(move || {
|
||||
reader.try_get(index.to_usize()).map(|typedbytes| {
|
||||
reader.try_get(index).map(|typedbytes| {
|
||||
let bytes = AddrBytes::from(typedbytes);
|
||||
index.increment();
|
||||
AddrHash::from(&bytes)
|
||||
|
||||
@@ -153,11 +153,11 @@ impl ScriptsVecs {
|
||||
OutputType::P2MS => self
|
||||
.p2ms
|
||||
.legacy_sigops
|
||||
.get_pushed_or_read(type_index.into(), &readers.p2ms_legacy_sigops),
|
||||
.get_append_only(type_index.into(), &readers.p2ms_legacy_sigops),
|
||||
OutputType::Unknown => self
|
||||
.unknown
|
||||
.legacy_sigops
|
||||
.get_pushed_or_read(type_index.into(), &readers.unknown_legacy_sigops),
|
||||
.get_append_only(type_index.into(), &readers.unknown_legacy_sigops),
|
||||
_ => Some(SigOps::ZERO),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,12 @@ fn main() {
|
||||
let total_outputs = indexer.vecs.outputs.value.len();
|
||||
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
|
||||
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
|
||||
let mut blocks: Vec<Block> = Vec::with_capacity(end_height - load_start);
|
||||
for h in load_start..end_height {
|
||||
|
||||
@@ -55,7 +55,12 @@ fn main() {
|
||||
let total_outputs = indexer.vecs.outputs.value.len();
|
||||
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
|
||||
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut tx_starts: Vec<usize> = Vec::new();
|
||||
|
||||
let out_path = format!("{out_dir}/oracle_outputs_{start}_{end}.csv");
|
||||
|
||||
@@ -534,7 +534,12 @@ fn main() {
|
||||
let total_outputs = indexer.vecs.outputs.value.len();
|
||||
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
|
||||
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut tx_starts: Vec<usize> = Vec::new();
|
||||
let mut values: Vec<Sats> = Vec::new();
|
||||
let mut output_types: Vec<OutputType> = Vec::new();
|
||||
|
||||
@@ -180,7 +180,12 @@ fn main() {
|
||||
// large, so the tx-indexed first_txout_index is read through a forward cursor.
|
||||
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
|
||||
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut tx_starts: Vec<usize> = Vec::new();
|
||||
|
||||
let mut year_stats: Vec<YearStats> = Vec::new();
|
||||
|
||||
@@ -842,7 +842,12 @@ fn main() {
|
||||
// large, so the tx-indexed first_txout_index is read through a forward cursor.
|
||||
let first_tx_index: Vec<TxIndex> = indexer.vecs.transactions.first_tx_index.collect();
|
||||
let out_first: Vec<TxOutIndex> = indexer.vecs.outputs.first_txout_index.collect();
|
||||
let mut txout_cursor = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let mut txout_cursor = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut tx_starts: Vec<usize> = Vec::new();
|
||||
|
||||
let mut year_stats: Vec<YearStats> = Vec::new();
|
||||
|
||||
@@ -25,8 +25,7 @@ brk_types = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
jiff = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
# quickmatch = { path = "../../../quickmatch" }
|
||||
quickmatch = "0.5.0"
|
||||
quickmatch = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
|
||||
@@ -5,6 +5,7 @@ use brk_types::{
|
||||
Addr, AddrBytes, AddrChainStats, AddrHash, AddrStats, AnyAddrDataIndexEnum, Dollars,
|
||||
OutputType, TypeIndex,
|
||||
};
|
||||
use vecdb::ReadableVec;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -40,8 +41,8 @@ impl Query {
|
||||
.distribution
|
||||
.addrs_data
|
||||
.funded
|
||||
.reader()
|
||||
.get(usize::from(index));
|
||||
.collect_one(index)
|
||||
.expect("funded address data index should be in bounds");
|
||||
let price = data.realized_price().to_dollars();
|
||||
(data, price)
|
||||
}
|
||||
@@ -50,8 +51,8 @@ impl Query {
|
||||
.distribution
|
||||
.addrs_data
|
||||
.empty
|
||||
.reader()
|
||||
.get(usize::from(index))
|
||||
.collect_one(index)
|
||||
.expect("empty address data index should be in bounds")
|
||||
.into();
|
||||
(data, Dollars::default())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use brk_error::{OptionData, Result};
|
||||
use brk_types::{Addr, AddrIndexTxIndex, Transaction, TxIndex, Txid, Unit};
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -25,7 +24,7 @@ impl Query {
|
||||
let txid_reader = self.indexer().vecs.transactions.txid.reader();
|
||||
Ok(txindices
|
||||
.into_iter()
|
||||
.map(|tx_index| txid_reader.get(tx_index.to_usize()))
|
||||
.map(|tx_index| txid_reader.get(tx_index))
|
||||
.collect())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use brk_error::{Error, OptionData, Result};
|
||||
use brk_types::{Addr, AddrIndexOutPoint, Height, TxIndex, TxStatus, Unit, Utxo, Vout};
|
||||
use vecdb::VecIndex;
|
||||
|
||||
use crate::Query;
|
||||
|
||||
@@ -36,9 +35,9 @@ impl Query {
|
||||
let mut utxos = Vec::with_capacity(outpoints.len());
|
||||
|
||||
for (tx_index, vout) in outpoints {
|
||||
let txid = txid_reader.get(tx_index.to_usize());
|
||||
let first_txout_index = first_txout_index_reader.get(tx_index.to_usize());
|
||||
let value = value_reader.get(usize::from(first_txout_index + vout));
|
||||
let txid = txid_reader.get(tx_index);
|
||||
let first_txout_index = first_txout_index_reader.get(tx_index);
|
||||
let value = value_reader.get(first_txout_index + vout);
|
||||
|
||||
let height = self.confirmed_status_height(tx_index)?;
|
||||
let status = if let Some((h, ref s)) = cached_status
|
||||
|
||||
@@ -88,8 +88,7 @@ impl Query {
|
||||
.vecs
|
||||
.transactions
|
||||
.txid
|
||||
.reader()
|
||||
.try_get(first + index)
|
||||
.collect_one_at(first + index)
|
||||
.ok_or(Error::Internal(
|
||||
"block_txid_at_index_by_height: txid index past data",
|
||||
))
|
||||
@@ -125,7 +124,7 @@ impl Query {
|
||||
|
||||
// ── Phase 1: Decode all transactions, collect outpoints ─────────
|
||||
|
||||
let mut txid_cursor = indexer.vecs.transactions.txid.cursor();
|
||||
let txid_cursor = indexer.vecs.transactions.txid.reader().cursor();
|
||||
let mut total_size_cursor = indexer.vecs.transactions.total_size.cursor();
|
||||
let mut sigops_cursor = indexer.vecs.transactions.total_sigop_cost.cursor();
|
||||
let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor();
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Query {
|
||||
let position = index.to_usize();
|
||||
let weight = weight.get(position).data()?;
|
||||
Ok(Member {
|
||||
txid: txid.get(position),
|
||||
txid: txid.get(*index),
|
||||
fee: fee.get(position).data()?,
|
||||
weight,
|
||||
vsize: VSize::from(weight),
|
||||
@@ -88,7 +88,7 @@ impl Query {
|
||||
.map(|index| {
|
||||
let position = index.to_usize();
|
||||
Ok(CpfpEntry {
|
||||
txid: txid.get(position),
|
||||
txid: txid.get(*index),
|
||||
fee: fee.get(position).data()?,
|
||||
weight: weight.get(position).data()?,
|
||||
})
|
||||
@@ -112,9 +112,14 @@ impl Query {
|
||||
let mut first_txin = indexer.vecs.transactions.first_txin_index.cursor();
|
||||
let mut input_count = computer.indexes.tx_index.input_count.cursor();
|
||||
let mut outpoint = indexer.vecs.inputs.outpoint.cursor();
|
||||
let mut first_txout = indexer.vecs.transactions.first_txout_index.cursor();
|
||||
let first_txout = indexer
|
||||
.vecs
|
||||
.transactions
|
||||
.first_txout_index
|
||||
.reader()
|
||||
.cursor();
|
||||
let mut output_count = computer.indexes.tx_index.output_count.cursor();
|
||||
let mut spent = computer.outputs.spent.txin_index.cursor();
|
||||
let spent = computer.outputs.spent.txin_index.reader().cursor();
|
||||
let mut spending_tx = indexer.vecs.inputs.tx_index.cursor();
|
||||
|
||||
let mut parents_of = |tx: TxIndex| -> Result<SmallVec<[TxIndex; 2]>> {
|
||||
|
||||
@@ -67,16 +67,14 @@ impl Query {
|
||||
if prev_tx_index >= safe.tx_index {
|
||||
return None;
|
||||
}
|
||||
let first_txout: TxOutIndex =
|
||||
first_txout_reader.try_get(usize::from(prev_tx_index))?;
|
||||
let first_txout: TxOutIndex = first_txout_reader.try_get(prev_tx_index)?;
|
||||
let txout = first_txout + *vout;
|
||||
if txout >= safe.txout_index {
|
||||
return None;
|
||||
}
|
||||
let txout_idx = usize::from(txout);
|
||||
let output_type: OutputType = output_type_reader.try_get(txout_idx)?;
|
||||
let type_index: TypeIndex = type_index_reader.try_get(txout_idx)?;
|
||||
let value: Sats = value_reader.try_get(txout_idx)?;
|
||||
let output_type: OutputType = output_type_reader.try_get(txout)?;
|
||||
let type_index: TypeIndex = type_index_reader.try_get(txout)?;
|
||||
let value: Sats = value_reader.try_get(txout)?;
|
||||
let script_pubkey = addr_readers.script_pubkey(output_type, type_index);
|
||||
Some(((*prev_txid, *vout), TxOut::from((script_pubkey, value))))
|
||||
})
|
||||
|
||||
@@ -205,8 +205,8 @@ impl Query {
|
||||
.outputs
|
||||
.spent
|
||||
.txin_index
|
||||
.reader()
|
||||
.get(usize::from(txout_index));
|
||||
.collect_one(txout_index)
|
||||
.data()?;
|
||||
|
||||
if txin_index == TxInIndex::UNSPENT {
|
||||
return Ok(TxOutspend::UNSPENT);
|
||||
@@ -235,7 +235,7 @@ impl Query {
|
||||
let mut cached_status: Option<(Height, BlockHash, Timestamp)> = None;
|
||||
let mut outspends = Vec::with_capacity(output_count);
|
||||
for i in 0..output_count {
|
||||
let txin_index = txin_index_reader.get(usize::from(first_txout + Vout::from(i)));
|
||||
let txin_index = txin_index_reader.get(first_txout + Vout::from(i));
|
||||
|
||||
if txin_index == TxInIndex::UNSPENT {
|
||||
outspends.push(TxOutspend::UNSPENT);
|
||||
@@ -249,7 +249,7 @@ impl Query {
|
||||
}
|
||||
let spending_first_txin = first_txin_cursor.get(spending_tx_index.to_usize()).data()?;
|
||||
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
|
||||
let spending_txid = txid_reader.get(spending_tx_index.to_usize());
|
||||
let spending_txid = txid_reader.get(spending_tx_index);
|
||||
let spending_height: Height = tx_heights.get_shared(spending_tx_index).data()?;
|
||||
|
||||
let (block_hash, block_time) = if let Some((h, ref bh, bt)) = cached_status
|
||||
@@ -318,10 +318,15 @@ impl Query {
|
||||
return Err(Error::UnknownTxid);
|
||||
}
|
||||
let first_txout_vec = &self.indexer().vecs.transactions.first_txout_index;
|
||||
let first = first_txout_vec.read_once(tx_index)?;
|
||||
let first_txout_reader = first_txout_vec.reader();
|
||||
let first = first_txout_reader.try_get(tx_index).ok_or(Error::Internal(
|
||||
"resolve_tx_outputs: first txout index past data",
|
||||
))?;
|
||||
let next_tx = tx_index.incremented();
|
||||
let next = if next_tx < safe.tx_index {
|
||||
first_txout_vec.read_once(next_tx)?
|
||||
first_txout_reader.try_get(next_tx).ok_or(Error::Internal(
|
||||
"resolve_tx_outputs: next first txout index past data",
|
||||
))?
|
||||
} else {
|
||||
safe.txout_index
|
||||
};
|
||||
|
||||
@@ -16,3 +16,6 @@ brk_types = { workspace = true }
|
||||
byteview = { workspace = true }
|
||||
fjall = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{Height, Version};
|
||||
use fjall::Keyspace;
|
||||
use brk_types::Height;
|
||||
|
||||
pub trait AnyStore: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn height(&self) -> Option<Height>;
|
||||
fn has(&self, height: Height) -> bool;
|
||||
fn needs(&self, height: Height) -> bool;
|
||||
fn version(&self) -> Version;
|
||||
fn export_meta(&mut self, height: Height) -> Result<()>;
|
||||
fn export_meta_if_needed(&mut self, height: Height) -> Result<()>;
|
||||
fn keyspace(&self) -> &Keyspace;
|
||||
fn commit(&mut self, height: Height) -> Result<()>;
|
||||
}
|
||||
|
||||
+69
-66
@@ -1,18 +1,6 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Debug,
|
||||
fs,
|
||||
hash::Hash,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering::Relaxed},
|
||||
},
|
||||
};
|
||||
use std::{borrow::Cow, cmp::Ordering, fmt::Debug, fs, hash::Hash, mem, ops::Range, path::Path};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_types::{Height, Version};
|
||||
@@ -32,7 +20,7 @@ pub use kind::*;
|
||||
pub use meta::*;
|
||||
pub use mode::*;
|
||||
|
||||
const MAJOR_FJALL_VERSION: Version = Version::new(3);
|
||||
const MAJOR_FJALL_VERSION: Version = Version::new(4);
|
||||
|
||||
pub fn open_database(path: &Path) -> fjall::Result<Database> {
|
||||
Database::builder(path.join("fjall"))
|
||||
@@ -44,12 +32,10 @@ pub fn open_database(path: &Path) -> fjall::Result<Database> {
|
||||
#[derive(Clone)]
|
||||
pub struct Store<K, V> {
|
||||
meta: StoreMeta,
|
||||
name: &'static str,
|
||||
keyspace: Keyspace,
|
||||
puts: FxHashMap<K, V>,
|
||||
dels: FxHashSet<K>,
|
||||
caches: Vec<FxHashMap<K, V>>,
|
||||
db_reads: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<K, V> Store<K, V>
|
||||
@@ -111,12 +97,10 @@ where
|
||||
|
||||
Ok(Self {
|
||||
meta,
|
||||
name: Box::leak(Box::new(name.to_string())),
|
||||
keyspace,
|
||||
puts: FxHashMap::default(),
|
||||
dels: FxHashSet::default(),
|
||||
caches,
|
||||
db_reads: Arc::new(AtomicU64::new(0)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,9 +163,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
self.db_reads.fetch_add(1, Relaxed);
|
||||
|
||||
if let Some(slice) = self.keyspace.get(ByteView::from(key))? {
|
||||
if let Some(slice) = self.keyspace.get_standard(ByteView::from(key))? {
|
||||
Ok(Some(Cow::Owned(V::from(ByteView::from(slice)))))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -240,16 +222,16 @@ where
|
||||
let keyspace = self.keyspace.clone();
|
||||
|
||||
Ok(Some(Box::new(move || {
|
||||
Self::ingest(&keyspace, puts.iter(), dels.iter())
|
||||
Self::ingest_owned(&keyspace, puts, dels)
|
||||
})))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> impl Iterator<Item = (K, V)> {
|
||||
self.keyspace
|
||||
.iter()
|
||||
.map(|res| res.into_inner().unwrap())
|
||||
.map(|(k, v)| (K::from(ByteView::from(&*k)), V::from(ByteView::from(&*v))))
|
||||
.iter_standard()
|
||||
.map(Result::unwrap)
|
||||
.map(|(k, v)| (K::from(ByteView::from(k)), V::from(ByteView::from(v))))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -259,9 +241,9 @@ where
|
||||
) -> impl DoubleEndedIterator<Item = (K, V)> + '_ {
|
||||
let prefix: ByteView = prefix.into();
|
||||
self.keyspace
|
||||
.prefix(&*prefix)
|
||||
.map(|res| res.into_inner().unwrap())
|
||||
.map(|(k, v)| (K::from(ByteView::from(&*k)), V::from(ByteView::from(&*v))))
|
||||
.prefix_standard(prefix)
|
||||
.map(Result::unwrap)
|
||||
.map(|(k, v)| (K::from(ByteView::from(k)), V::from(ByteView::from(v))))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -272,9 +254,9 @@ where
|
||||
let start: ByteView = range.start.into();
|
||||
let end: ByteView = range.end.into();
|
||||
self.keyspace
|
||||
.range(start..end)
|
||||
.map(|res| res.into_inner().unwrap())
|
||||
.map(|(k, v)| (K::from(ByteView::from(&*k)), V::from(ByteView::from(&*v))))
|
||||
.range_standard(start..end)
|
||||
.map(Result::unwrap)
|
||||
.map(|(k, v)| (K::from(ByteView::from(k)), V::from(ByteView::from(v))))
|
||||
}
|
||||
|
||||
pub fn approximate_len(&self) -> usize {
|
||||
@@ -286,11 +268,6 @@ where
|
||||
self.meta.has(height)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn needs(&self, height: Height) -> bool {
|
||||
self.meta.needs(height)
|
||||
}
|
||||
|
||||
fn export_meta(&mut self, height: Height) -> Result<()> {
|
||||
self.meta.export(height)?;
|
||||
Ok(())
|
||||
@@ -321,20 +298,69 @@ where
|
||||
items.sort_unstable();
|
||||
|
||||
let mut ingestion = keyspace.start_ingestion()?;
|
||||
// FxHashMap/FxHashSet keep keys unique and disjoint; sorting therefore
|
||||
// proves the strict ordering required by the prevalidated writer.
|
||||
for item in items {
|
||||
match item {
|
||||
Item::Value { key, value } => {
|
||||
ingestion.write(ByteView::from(key), ByteView::from(value))?;
|
||||
ingestion.write_prevalidated(ByteView::from(key), ByteView::from(value))?;
|
||||
}
|
||||
Item::Tomb(key) => {
|
||||
ingestion.write_weak_tombstone(ByteView::from(key))?;
|
||||
ingestion.write_prevalidated_weak_tombstone(ByteView::from(key))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ingestion.finish()?;
|
||||
// Store keyspaces are mutated only through these ingestion phases, so
|
||||
// no journaled Fjall write can race their completion.
|
||||
ingestion.finish_exclusive()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ingest_owned(keyspace: &Keyspace, puts: FxHashMap<K, V>, dels: FxHashSet<K>) -> Result<()> {
|
||||
let mut puts: Vec<_> = puts.into_iter().collect();
|
||||
let mut dels: Vec<_> = dels.into_iter().collect();
|
||||
|
||||
puts.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
|
||||
dels.sort_unstable();
|
||||
|
||||
let mut puts = puts.into_iter().peekable();
|
||||
let mut dels = dels.into_iter().peekable();
|
||||
let mut ingestion = keyspace.start_ingestion()?;
|
||||
|
||||
// The buffers are unique and disjoint, and this merge emits them in
|
||||
// strict key order, so release builds can skip re-cloning each key.
|
||||
while puts.peek().is_some() || dels.peek().is_some() {
|
||||
match (puts.peek(), dels.peek()) {
|
||||
(Some((put_key, _)), Some(del_key)) => match put_key.cmp(del_key) {
|
||||
Ordering::Less => {
|
||||
let (key, value) = puts.next().unwrap();
|
||||
ingestion.write_prevalidated(ByteView::from(key), ByteView::from(value))?;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
ingestion.write_prevalidated_weak_tombstone(ByteView::from(
|
||||
dels.next().unwrap(),
|
||||
))?;
|
||||
}
|
||||
Ordering::Equal => unreachable!("key is both inserted and deleted"),
|
||||
},
|
||||
(Some(_), None) => {
|
||||
let (key, value) = puts.next().unwrap();
|
||||
ingestion.write_prevalidated(ByteView::from(key), ByteView::from(value))?;
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
ingestion
|
||||
.write_prevalidated_weak_tombstone(ByteView::from(dels.next().unwrap()))?;
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Store keyspaces are mutated only through these ingestion phases, so
|
||||
// no journaled Fjall write can race their completion.
|
||||
ingestion.finish_exclusive()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> AnyStore for Store<K, V>
|
||||
@@ -344,38 +370,14 @@ where
|
||||
for<'a> ByteView: From<K> + From<V> + From<&'a K> + From<&'a V>,
|
||||
Self: Send + Sync,
|
||||
{
|
||||
fn keyspace(&self) -> &Keyspace {
|
||||
&self.keyspace
|
||||
}
|
||||
|
||||
fn export_meta(&mut self, height: Height) -> Result<()> {
|
||||
self.export_meta(height)
|
||||
}
|
||||
|
||||
fn export_meta_if_needed(&mut self, height: Height) -> Result<()> {
|
||||
self.export_meta_if_needed(height)
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn height(&self) -> Option<Height> {
|
||||
self.meta.height()
|
||||
}
|
||||
|
||||
fn has(&self, height: Height) -> bool {
|
||||
self.has(height)
|
||||
}
|
||||
|
||||
fn needs(&self, height: Height) -> bool {
|
||||
self.needs(height)
|
||||
}
|
||||
|
||||
fn version(&self) -> Version {
|
||||
self.meta.version()
|
||||
}
|
||||
|
||||
fn commit(&mut self, height: Height) -> Result<()> {
|
||||
self.export_meta_if_needed(height)?;
|
||||
|
||||
@@ -386,9 +388,10 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Self::ingest(&self.keyspace, puts.iter(), dels.iter())?;
|
||||
|
||||
if !self.caches.is_empty() {
|
||||
if self.caches.is_empty() {
|
||||
Self::ingest_owned(&self.keyspace, puts, dels)?;
|
||||
} else {
|
||||
Self::ingest(&self.keyspace, puts.iter(), dels.iter())?;
|
||||
self.caches.pop();
|
||||
self.caches.insert(0, puts);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use super::Height;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoreMeta {
|
||||
pathbuf: PathBuf,
|
||||
version: Version,
|
||||
height: Option<Height>,
|
||||
}
|
||||
|
||||
@@ -41,19 +40,14 @@ impl StoreMeta {
|
||||
|
||||
let slf = Self {
|
||||
pathbuf: path.to_owned(),
|
||||
version,
|
||||
height: Height::try_from(Self::path_height_(path).as_path()).ok(),
|
||||
};
|
||||
|
||||
slf.version.write(&slf.path_version())?;
|
||||
version.write(&slf.path_version())?;
|
||||
|
||||
Ok((slf, partition))
|
||||
}
|
||||
|
||||
pub fn version(&self) -> Version {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn export(&mut self, height: Height) -> io::Result<()> {
|
||||
self.height = Some(height);
|
||||
height.write(&self.path_height())
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use brk_store::{Kind, Mode, Store, open_database};
|
||||
use brk_types::{AddrIndexTxIndex, Height, TxIndex, TypeIndex, Unit, Version};
|
||||
use fjall::PersistMode;
|
||||
|
||||
fn key(address: u32, transaction: u32) -> AddrIndexTxIndex {
|
||||
AddrIndexTxIndex::from((TypeIndex::new(address), TxIndex::new(transaction)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_ingest_merges_puts_and_tombstones() -> brk_error::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path();
|
||||
|
||||
{
|
||||
let db = open_database(path)?;
|
||||
let mut store = Store::import(
|
||||
&db,
|
||||
path,
|
||||
"owned_ingest",
|
||||
Version::ZERO,
|
||||
Mode::Any,
|
||||
Kind::Vec,
|
||||
)?;
|
||||
|
||||
store.insert(key(1, 1), Unit);
|
||||
store.insert(key(2, 2), Unit);
|
||||
store.take_pending_ingest(Height::from(0_u32))?.unwrap()()?;
|
||||
|
||||
store.remove(key(1, 1));
|
||||
store.remove(key(3, 3));
|
||||
store.insert(key(4, 4), Unit);
|
||||
store.take_pending_ingest(Height::from(1_u32))?.unwrap()()?;
|
||||
db.persist(PersistMode::SyncData)?;
|
||||
|
||||
assert!(store.get(&key(1, 1))?.is_none());
|
||||
assert!(store.get(&key(2, 2))?.is_some());
|
||||
assert!(store.get(&key(3, 3))?.is_none());
|
||||
assert!(store.get(&key(4, 4))?.is_some());
|
||||
}
|
||||
|
||||
{
|
||||
let db = open_database(path)?;
|
||||
let store: Store<AddrIndexTxIndex, Unit> = Store::import(
|
||||
&db,
|
||||
path,
|
||||
"owned_ingest",
|
||||
Version::ZERO,
|
||||
Mode::Any,
|
||||
Kind::Vec,
|
||||
)?;
|
||||
|
||||
assert!(store.get(&key(1, 1))?.is_none());
|
||||
assert!(store.get(&key(2, 2))?.is_some());
|
||||
assert!(store.get(&key(3, 3))?.is_none());
|
||||
assert!(store.get(&key(4, 4))?.is_some());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,6 @@ use std::hash::{Hash, Hasher};
|
||||
|
||||
use byteview::ByteView;
|
||||
use serde::Serialize;
|
||||
use vecdb::Bytes;
|
||||
|
||||
use crate::{AddrIndexTxIndex, Vout};
|
||||
|
||||
@@ -16,6 +15,14 @@ pub struct AddrIndexOutPoint {
|
||||
}
|
||||
|
||||
impl AddrIndexOutPoint {
|
||||
#[inline]
|
||||
pub(crate) fn to_be_bytes(self) -> [u8; 10] {
|
||||
let mut bytes = [0; 10];
|
||||
bytes[..8].copy_from_slice(&self.addr_index_tx_index.to_be_bytes());
|
||||
bytes[8..].copy_from_slice(&self.vout.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tx_index(&self) -> TxIndex {
|
||||
self.addr_index_tx_index.tx_index()
|
||||
@@ -28,11 +35,10 @@ impl AddrIndexOutPoint {
|
||||
}
|
||||
|
||||
impl Hash for AddrIndexOutPoint {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let mut buf = [0u8; 10];
|
||||
buf[0..8].copy_from_slice(&self.addr_index_tx_index.to_bytes());
|
||||
buf[8..].copy_from_slice(&self.vout.to_bytes());
|
||||
state.write(&buf);
|
||||
self.addr_index_tx_index.hash(state);
|
||||
self.vout.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +71,27 @@ impl From<AddrIndexOutPoint> for ByteView {
|
||||
impl From<&AddrIndexOutPoint> for ByteView {
|
||||
#[inline]
|
||||
fn from(value: &AddrIndexOutPoint) -> Self {
|
||||
ByteView::from(
|
||||
[
|
||||
&ByteView::from(value.addr_index_tx_index),
|
||||
value.vout.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
ByteView::from(value.to_be_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn byte_encoding_is_stable_and_roundtrips() {
|
||||
let value = AddrIndexOutPoint::from((
|
||||
TypeIndex::new(0x0102_0304),
|
||||
OutPoint::new(TxIndex::new(0x0506_0708), Vout::from(0x090a_u16)),
|
||||
));
|
||||
let bytes = ByteView::from(value);
|
||||
|
||||
assert_eq!(
|
||||
&*bytes,
|
||||
&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
"the LSM key encoding is part of the persisted format",
|
||||
);
|
||||
assert_eq!(AddrIndexOutPoint::from(bytes), value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ use super::{TxIndex, TypeIndex};
|
||||
pub struct AddrIndexTxIndex(u64);
|
||||
|
||||
impl AddrIndexTxIndex {
|
||||
#[inline]
|
||||
pub(crate) fn to_be_bytes(self) -> [u8; 8] {
|
||||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn addr_index(&self) -> u32 {
|
||||
(self.0 >> 32) as u32
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ use std::sync::OnceLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::PoolSlug;
|
||||
use crate::{PoolSlug, Version};
|
||||
|
||||
use super::Pool;
|
||||
|
||||
/// Increment when pool IDs, payout addresses, or coinbase tags change.
|
||||
pub const POOL_ATTRIBUTION_VERSION: Version = Version::ONE;
|
||||
|
||||
const JSON_DATA: &str = include_str!("../pools-v2.json");
|
||||
const TESTNET_IDS: &[u16] = &[145, 146, 149, 150, 156, 163];
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "5s", terminate-after = 3 }
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
mutants
|
||||
mutants.out
|
||||
|
||||
.fjall_data
|
||||
.data
|
||||
.test
|
||||
/old_*
|
||||
|
||||
.directory
|
||||
@@ -0,0 +1,3 @@
|
||||
reorder_imports = true
|
||||
# group_imports = "StdExternalCrate"
|
||||
# imports_granularity = "crate"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.showUnlinkedFileNotification": false
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# 3.1.0
|
||||
|
||||
- [feat] Implemented support for compaction filters (custom logic during compactions)
|
||||
- [msrv] Reduced MSRV to 1.90
|
||||
|
||||
# 3.0.0
|
||||
|
||||
- [feat] Implemented new block format in `lsm-tree`
|
||||
- [feat] Bookkeep LSM-tree changes (flushes, compactions) in `Version` history
|
||||
- [feat] Prefix truncation inside data & index blocks
|
||||
- [feat] Allow unpinning filter blocks
|
||||
- [feat] Implemented partitioned filters
|
||||
- [feat] Allow calling bulk ingestion on non-empty keyspaces
|
||||
- [feat] Introduced level-based configuration policies for most configuration parameters
|
||||
- [feat] Journal compression for large values
|
||||
- [feat] Database locking using the new Rust file locking API
|
||||
- [feat] Rewritten key-value separation to run during compactions, instead of dedicated GC runs
|
||||
- [feat] Full file checksums to allow fast database corruption checks (in the future)
|
||||
- [feat] Checksum check on block & blob reads
|
||||
- [api] Make Ingestion API more flexible
|
||||
- [feat] Shortening eligible sequence numbers when compacting into the last level to save disk space
|
||||
- [api] Change constructor to `Database::builder` instead of `Config::new`
|
||||
- [api] Changed naming of keyspace->database, and partition->keyspace
|
||||
- [api] Change transaction feature flags to be separate structs, `OptimisticTxDatabase` and `SingleWriterTxDatabase`
|
||||
- [api] Changed snapshot error type, fixes #156
|
||||
- [api] Unified transactions read operations and snapshots with `Readable` trait
|
||||
- [api] Guard API for iterator values
|
||||
- [api] Removed old garbage collection APIs
|
||||
- [api] `metrics` feature flag for cache hit rates etc. (will be exposed in the future)
|
||||
- [api] Change `bytes` feature flag to `bytes_1` to pin its version
|
||||
- [api] Make read operations in optimistic write transactions non-mut
|
||||
- [fix] Consider blob files in FIFO compaction size limit, fixes #133
|
||||
- [perf] Use a single hash per key for filters, instead of two
|
||||
- [perf] Improve leveled compaction scoring
|
||||
- [perf] Improve leveled compaction picking to use less hashing and heap allocations
|
||||
- [perf] Use `quick-cache` for file descriptor caching
|
||||
- [perf] Promote levels immediately to L6 to get rid of tombstones easily
|
||||
- [perf] Rewritten maintenance task bookkeeping, and write stalling mechanisms to be less aggressive
|
||||
- [perf] Allow `lsm-tree` flushes to merge multiple sealed memtables into L0, if necessary
|
||||
- [perf] Skip heap allocation in blob memtable inserts
|
||||
- [perf] Skip compression when rewriting compressed blob files
|
||||
- [msrv] Increased MSRV to **1.91**
|
||||
- [misc] Blob file descriptor caching
|
||||
- [misc] Use Rust native `path::absolute`, removing `path-absolutize` dependency
|
||||
- [misc] Remove `std-semaphore` dependency
|
||||
- [misc] Remove `miniz` (will be replaced in the future)
|
||||
- [misc] Use `byteorder-lite` as drop-in replacement for `byteorder`
|
||||
- [refactor] Changed background workers to be a single thread pool
|
||||
- [internal] Store keyspace configurations in a meta keyspace, instead of individual binary config files
|
||||
- [internal] Use `sfa` for most file scaffolding in `lsm-tree`
|
||||
@@ -0,0 +1,11 @@
|
||||
# Contributing
|
||||
|
||||
## License
|
||||
|
||||
By contributing to this project, you agree that your contributions will be licensed under the project's license (MIT OR Apache-2.0).
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
## Looking for issues?
|
||||
|
||||
https://github.com/fjall-rs/fjall/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
|
||||
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "fjall"
|
||||
description = "Log-structured, embeddable key-value storage engine"
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
readme.workspace = true
|
||||
include = ["src/**/*", "LICENSE-APACHE", "LICENSE-MIT", "README.md"]
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords = ["database", "key-value", "lsm", "rocksdb", "leveldb"]
|
||||
categories = ["data-structures", "database-implementations", "algorithms"]
|
||||
|
||||
[lib]
|
||||
name = "fjall"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["lz4"]
|
||||
lz4 = ["lsm-tree/lz4", "dep:lz4_flex"]
|
||||
bytes_1 = ["lsm-tree/bytes_1"]
|
||||
metrics = ["lsm-tree/metrics"]
|
||||
__internal_whitebox = []
|
||||
|
||||
[dependencies]
|
||||
byteorder = { package = "byteorder-lite", version = "0.1.0" }
|
||||
byteview = { workspace = true }
|
||||
lsm-tree = { workspace = true, default-features = false, features = [] }
|
||||
log = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
dashmap = "6.1.0"
|
||||
xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
|
||||
lz4_flex = { workspace = true, features = ["checked-decode"], optional = true }
|
||||
flume = { version = "0.12.0", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
nanoid = "0.4.0"
|
||||
test-log = "0.2.18"
|
||||
rand = "0.10.0"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["__internal_whitebox"]
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 fjall-rs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,207 @@
|
||||
<p align="center">
|
||||
<img src="/kawaii.png" height="200">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/fjall-rs/fjall/actions/workflows/test.yml">
|
||||
<img src="https://github.com/fjall-rs/fjall/actions/workflows/test.yml/badge.svg" alt="CI" />
|
||||
</a>
|
||||
<a href="https://docs.rs/fjall">
|
||||
<img src="https://img.shields.io/docsrs/fjall?color=green" alt="docs.rs" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/fjall">
|
||||
<img src="https://img.shields.io/crates/v/fjall?color=blue" alt="Crates.io" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/MSRV-1.90.0-blue" alt="MSRV" />
|
||||
<a href="https://deps.rs/repo/github/fjall-rs/fjall">
|
||||
<img src="https://deps.rs/repo/github/fjall-rs/fjall/status.svg" alt="dependency status" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/HvYGp4NFFk">
|
||||
<img src="https://img.shields.io/discord/1240426554111164486" alt="Discord" />
|
||||
</a>
|
||||
<a href="https://bsky.app/profile/fjallrs.bsky.social">
|
||||
<img src="https://img.shields.io/badge/bluesky-blue" alt="Bluesky" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
*Fjall* _(Nordic: "Mountain")_ is a log-structured, embeddable key-value storage engine written in Rust.
|
||||
It features:
|
||||
|
||||
- A thread-safe BTreeMap-like API
|
||||
- 100% safe & stable Rust
|
||||
- LSM-tree-based storage similar to `RocksDB`
|
||||
- Range & prefix searching with forward and reverse iteration
|
||||
- Multiple keyspaces (a.k.a. column families) with cross-keyspace atomic semantics
|
||||
- Built-in compression (default = `LZ4`)
|
||||
- Serializable transactions (optional)
|
||||
- Key-value separation for large blob use cases (optional)
|
||||
- Custom compaction filters to run custom logic during compactions (optional)
|
||||
- Automatic background maintenance
|
||||
|
||||
It is not:
|
||||
|
||||
- A standalone database server
|
||||
- A relational or wide-column database: it has no built-in notion of columns or query language
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://sqlsync.dev">
|
||||
<picture>
|
||||
<source width="240" alt="Orbitinghail" media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/fjall-rs/fjall-rs.github.io/d22fcb1e6966ce08327ea3bf6cf2ea86a840b071/public/logos/orbitinghail.svg" />
|
||||
<source width="240" alt="Orbitinghail" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/fjall-rs/fjall-rs.github.io/d22fcb1e6966ce08327ea3bf6cf2ea86a840b071/public/logos/orbitinghail_dark.svg" />
|
||||
<img width="240" alt="Orbitinghail" src="https://raw.githubusercontent.com/fjall-rs/fjall-rs.github.io/d22fcb1e6966ce08327ea3bf6cf2ea86a840b071/public/logos/orbitinghail_dark.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Basic usage
|
||||
|
||||
```bash
|
||||
cargo add fjall
|
||||
```
|
||||
|
||||
```rust
|
||||
use fjall::{Database, KeyspaceCreateOptions, PersistMode};
|
||||
|
||||
fn main() -> fjall::Result<()> {
|
||||
// A database may contain multiple keyspaces
|
||||
// You should probably only use a single database for your application
|
||||
let db = Database::builder(path).open()?;
|
||||
// TxDatabase::builder for transactional semantics
|
||||
|
||||
// Each keyspace is its own physical LSM-tree, and thus isolated from other keyspaces
|
||||
let items = db.keyspace("my_items", KeyspaceCreateOptions::default)?;
|
||||
|
||||
// Write some data
|
||||
items.insert("a", "hello")?;
|
||||
|
||||
// And retrieve it
|
||||
let bytes = items.get("a")?;
|
||||
|
||||
// Or remove it again
|
||||
items.remove("a")?;
|
||||
|
||||
// Search by prefix
|
||||
for kv in items.prefix("prefix") {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Search by range
|
||||
for kv in items.range("a"..="z") {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Iterators implement DoubleEndedIterator, so you can search backwards, too!
|
||||
for kv in items.prefix("prefix").rev() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Sync the journal to disk to make sure data is definitely durable
|
||||
// When the database is dropped, it will try to persist with `PersistMode::SyncAll` automatically
|
||||
db.persist(PersistMode::SyncAll)
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Like any typical key-value store, keys are stored in lexicographic order.
|
||||
> If you are storing integer keys (e.g. timeseries data), you should use the big endian form to have predictable ordering.
|
||||
|
||||
## Durability
|
||||
|
||||
To support different kinds of workloads, Fjall is agnostic about the type of durability
|
||||
your application needs.
|
||||
After writing data (`insert`, `remove` or committing a write batch/transaction), you can choose to call [`Database::persist`](https://docs.rs/fjall/latest/fjall/struct.Database.html#method.persist) which takes a [`PersistMode`](https://docs.rs/fjall/latest/fjall/enum.PersistMode.html) parameter.
|
||||
By default, any operation will flush to OS buffers, but **not** to disk.
|
||||
This matches RocksDB's default durability.
|
||||
Also, when dropped, the database will try to persist the journal *to disk* synchronously.
|
||||
|
||||
## Multithreading, Async and Multiprocess
|
||||
|
||||
> [!WARNING]
|
||||
> A single database may **not** be loaded in parallel from separate *processes*.
|
||||
|
||||
Fjall is internally synchronized for multi-*threaded* access, so you can clone around the `Database` and `Keyspace`s as needed, without needing to lock yourself.
|
||||
|
||||
For an async example, see the [`tokio`](https://github.com/fjall-rs/fjall/tree/main/examples/tokio) example.
|
||||
|
||||
## Memory usage
|
||||
|
||||
Generally, memory for loaded data, indexes etc. is managed on a per-block basis, and capped by the block cache capacity.
|
||||
Note that this also applies to returned values: When you hold a `Slice`, it keeps the backing buffer alive (which may be a block).
|
||||
If you know that you are going to keep a value around for a long time, you may want to copy it out into a new `Vec<u8>`, `Box<[u8]>`, `Arc<[u8]>` or new `Slice` (using `Slice::new`).
|
||||
|
||||
> [!NOTE]
|
||||
> It is recommended to configure the block cache capacity to be ~20-25% of the available memory - or more **if** the data set fits _fully_ into memory.
|
||||
|
||||
Additionally, orthogonally to the block cache, each `Keyspace` has its own write buffer (["Memtable"](https://docs.rs/fjall/latest/fjall/struct.KeyspaceCreateOptions.html#method.max_memtable_size)) which is the unit of data flushed back into the "proper" index structure.
|
||||
|
||||
## Error handling
|
||||
|
||||
Fjall returns an [error enum](https://docs.rs/fjall/latest/fjall/enum.Error.html), however these variants are mostly used for debugging and tracing purposes, so your application is not expected to handle specific errors.
|
||||
|
||||
It's best to let the application crash and restart, which is the [safest way to recover from transient I/O errors](https://ramalagappan.github.io/pdfs/papers/cuttlefs.pdf).
|
||||
|
||||
## Transactional modes
|
||||
|
||||
The backing store (`lsm-tree`) is a MVCC key-value store, allowing repeatable snapshot reads.
|
||||
However this isolation level can not do read-modify-write operations without the chance of lost updates.
|
||||
Also, `WriteBatch` does not allow reading the intermediary state back as you would expect from a proper transaction.
|
||||
For that reason, if you need transactional semantics, you need to use one of the transactional database implementation (`OptimisticTxDatabase` or `SingleWriterTxDatabase`).
|
||||
|
||||
TL;DR: Fjall supports both transactional and non-transactional workloads.
|
||||
Chances are you want to use a transactional database, unless you know your workload does not need serializable transaction semantics.
|
||||
|
||||
### Single writer
|
||||
|
||||
Opens a transactional database for single-writer (serialized) transactions.
|
||||
Single writer means only a single **write** transaction can run at a time.
|
||||
This is trivially serializable because it _literally_ serializes write transactions.
|
||||
|
||||
### Optimistic
|
||||
|
||||
Opens a transactional database for multi-writer, serializable transactions.
|
||||
Conflict checking is done using optimistic concurrency control, meaning transactions can conflict and may have to be rerun.
|
||||
|
||||
## Feature flags
|
||||
|
||||
### lz4
|
||||
|
||||
Allows using `LZ4` compression, powered by [`lz4_flex`](https://github.com/PSeitz/lz4_flex).
|
||||
|
||||
*Enabled by default.*
|
||||
|
||||
### bytes_1
|
||||
|
||||
Uses [`bytes`](https://github.com/tokio-rs/bytes) 1.x as the underlying `Slice` type.
|
||||
Otherwise, [`byteview`](https://github.com/fjall-rs/byteview) is used instead.
|
||||
|
||||
*Disabled by default.*
|
||||
|
||||
## Stable disk format
|
||||
|
||||
Future breaking changes will result in a major version bump and a migration path.
|
||||
|
||||
For the underlying LSM-tree implementation, see: <https://crates.io/crates/lsm-tree>.
|
||||
|
||||
## Examples
|
||||
|
||||
[See here](https://github.com/fjall-rs/fjall/tree/main/examples) for practical examples.
|
||||
|
||||
## Contributing
|
||||
|
||||
How can you help?
|
||||
|
||||
- [Ask a question](https://github.com/fjall-rs/fjall/discussions/new?category=q-a)
|
||||
- or join the Discord server: [https://discord.com/invite/HvYGp4NFFk](https://discord.com/invite/HvYGp4NFFk)
|
||||
- [Post benchmarks and things you created](https://github.com/fjall-rs/fjall/discussions/new?category=show-and-tell)
|
||||
- [Open a PR](https://github.com/fjall-rs/fjall/compare),
|
||||
- [See open issues to pick up here](https://github.com/search?q=org%3Afjall-rs+label%3A%22help+wanted%22+state%3Aopen+&type=issues)
|
||||
- [Open an issue](https://github.com/fjall-rs/fjall/issues/new) (bug report, weirdness)
|
||||
|
||||
## License
|
||||
|
||||
All source code is licensed under MIT OR Apache-2.0.
|
||||
|
||||
All contributions are to be licensed as MIT OR Apache-2.0.
|
||||
@@ -0,0 +1,64 @@
|
||||
let machines = [
|
||||
# Fly.io performance
|
||||
# "fly.performance.1x",
|
||||
# "fly.performance.2x",
|
||||
"fly.performance.4x",
|
||||
# "fly.performance.8x",
|
||||
# "fly.performance.16x",
|
||||
|
||||
# EC2 T2
|
||||
# "aws.ec2.t2.nano",
|
||||
# "aws.ec2.t2.micro",
|
||||
# "aws.ec2.t2.small",
|
||||
# "aws.ec2.t2.medium",
|
||||
# "aws.ec2.t2.large",
|
||||
# "aws.ec2.t2.xlarge",
|
||||
# "aws.ec2.t2.2xlarge",
|
||||
|
||||
# EC2 T3
|
||||
# "aws.ec2.t3.nano",
|
||||
# "aws.ec2.t3.micro",
|
||||
# "aws.ec2.t3.small",
|
||||
"aws.ec2.t3.medium",
|
||||
# "aws.ec2.t3.large",
|
||||
# "aws.ec2.t3.xlarge",
|
||||
# "aws.ec2.t3.2xlarge",
|
||||
|
||||
# EC2 T3a
|
||||
# "aws.ec2.t3a.nano",
|
||||
# "aws.ec2.t3a.micro",
|
||||
# "aws.ec2.t3a.small",
|
||||
# "aws.ec2.t3a.medium",
|
||||
# "aws.ec2.t3a.large",
|
||||
# "aws.ec2.t3a.xlarge",
|
||||
# "aws.ec2.t3a.2xlarge",
|
||||
|
||||
# EC2 T4g
|
||||
# "aws.ec2.t4g.nano",
|
||||
# "aws.ec2.t4g.micro",
|
||||
# "aws.ec2.t4g.small",
|
||||
# "aws.ec2.t4g.medium",
|
||||
# "aws.ec2.t4g.large",
|
||||
# "aws.ec2.t4g.xlarge",
|
||||
# "aws.ec2.t4g.2xlarge",
|
||||
|
||||
# EC2 M4
|
||||
# "aws.ec2.m4.large",
|
||||
]
|
||||
|
||||
let table = $env.TABLE_NAME
|
||||
let commit = $env.COMMIT
|
||||
|
||||
print $"Queuing ($commit)"
|
||||
|
||||
for machine in $machines {
|
||||
let q_pk = $"q#($machine)"
|
||||
|
||||
print $"Adding queue item for ($machine)"
|
||||
let item = {
|
||||
pk: { S: $q_pk },
|
||||
sk: { S: $commit },
|
||||
version: { S: "2" },
|
||||
}
|
||||
aws dynamodb put-item --table-name $table --item ($item | to json)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const examplesFolder = "examples";
|
||||
|
||||
for (const exampleName of await readdir(examplesFolder)) {
|
||||
const folder = resolve(examplesFolder, exampleName);
|
||||
|
||||
{
|
||||
console.error(`Testing ${exampleName}`);
|
||||
|
||||
const proc = spawn("cargo test", {
|
||||
cwd: folder,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
proc.stdout.on("data", buf => console.log(String(buf)));
|
||||
proc.stderr.on("data", buf => console.error(String(buf)));
|
||||
|
||||
await new Promise((resolve, _) => {
|
||||
proc.on("exit", () => {
|
||||
if (proc.exitCode > 0) {
|
||||
console.error(`${exampleName} FAILED`);
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (existsSync(resolve(folder, ".run"))) {
|
||||
console.error(`Running ${exampleName}`);
|
||||
|
||||
const proc = spawn("cargo run", {
|
||||
cwd: folder,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
proc.stdout.on("data", buf => console.log(String(buf)));
|
||||
proc.stderr.on("data", buf => console.error(String(buf)));
|
||||
|
||||
await new Promise((resolve, _) => {
|
||||
proc.on("exit", () => {
|
||||
if (proc.exitCode > 0) {
|
||||
console.error(`${exampleName} FAILED`);
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
console.error(`${exampleName} OK`);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::Keyspace;
|
||||
use lsm_tree::{UserKey, UserValue, ValueType};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Item {
|
||||
/// Keyspace
|
||||
pub keyspace: Keyspace,
|
||||
|
||||
/// User-defined key - an arbitrary byte array
|
||||
///
|
||||
/// Supports up to 2^16 bytes
|
||||
pub key: UserKey,
|
||||
|
||||
/// User-defined value - an arbitrary byte array
|
||||
///
|
||||
/// Supports up to 65535 bytes
|
||||
pub value: UserValue,
|
||||
|
||||
/// Tombstone marker - if this is true, the value has been deleted
|
||||
pub value_type: ValueType,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Item {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}:{:?}:{} => {:?}",
|
||||
self.keyspace.id,
|
||||
self.key,
|
||||
match self.value_type {
|
||||
ValueType::Value => "V",
|
||||
ValueType::Tombstone => "T",
|
||||
ValueType::WeakTombstone => "W",
|
||||
ValueType::Indirection => "Vb",
|
||||
},
|
||||
self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn new<K: Into<UserKey>, V: Into<UserValue>>(
|
||||
keyspace: Keyspace,
|
||||
key: K,
|
||||
value: V,
|
||||
value_type: ValueType,
|
||||
) -> Self {
|
||||
let k = key.into();
|
||||
let v = value.into();
|
||||
|
||||
assert!(!k.is_empty());
|
||||
|
||||
assert!(
|
||||
u16::try_from(k.len()).is_ok(),
|
||||
"Keys can be up to 65535 bytes long"
|
||||
);
|
||||
assert!(
|
||||
u32::try_from(v.len()).is_ok(),
|
||||
"Values can be up to 2^32 bytes long"
|
||||
);
|
||||
|
||||
Self {
|
||||
keyspace,
|
||||
key: k,
|
||||
value: v,
|
||||
value_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
pub mod item;
|
||||
|
||||
use crate::{Database, Keyspace, PersistMode};
|
||||
use item::Item;
|
||||
use lsm_tree::{AbstractTree, UserKey, UserValue, ValueType};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// An atomic write batch
|
||||
///
|
||||
/// Allows atomically writing across keyspaces inside the [`Database`].
|
||||
pub struct WriteBatch {
|
||||
pub(crate) data: Vec<Item>,
|
||||
db: Database,
|
||||
durability: Option<PersistMode>,
|
||||
}
|
||||
|
||||
impl WriteBatch {
|
||||
/// Initializes a new write batch.
|
||||
///
|
||||
/// This function is called by [`Database::batch`].
|
||||
pub(crate) fn new(db: Database) -> Self {
|
||||
Self {
|
||||
data: Vec::new(),
|
||||
db,
|
||||
durability: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a new write batch with preallocated capacity.
|
||||
///
|
||||
/// ### Note
|
||||
///
|
||||
/// "Capacity" refers to the number of batch item slots, not their size in memory.
|
||||
#[must_use]
|
||||
pub fn with_capacity(db: Database, capacity: usize) -> Self {
|
||||
Self {
|
||||
data: Vec::with_capacity(capacity),
|
||||
db,
|
||||
durability: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the number of batched items.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no batches items (yet).
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Sets the durability level.
|
||||
#[must_use]
|
||||
pub fn durability(mut self, mode: Option<PersistMode>) -> Self {
|
||||
self.durability = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Inserts a key-value pair into the batch.
|
||||
pub fn insert<K: Into<UserKey>, V: Into<UserValue>>(&mut self, p: &Keyspace, key: K, value: V) {
|
||||
self.data
|
||||
.push(Item::new(p.clone(), key, value, ValueType::Value));
|
||||
}
|
||||
|
||||
/// Removes a key-value pair.
|
||||
pub fn remove<K: Into<UserKey>>(&mut self, p: &Keyspace, key: K) {
|
||||
self.data
|
||||
.push(Item::new(p.clone(), key, vec![], ValueType::Tombstone));
|
||||
}
|
||||
|
||||
/// Adds a weak tombstone marker for a key.
|
||||
///
|
||||
/// The tombstone marker of this delete operation will vanish when it
|
||||
/// collides with its corresponding insertion.
|
||||
/// This may cause older versions of the value to be resurrected, so it should
|
||||
/// only be used and preferred in scenarios where a key is only ever written once.
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This function is currently experimental.
|
||||
#[doc(hidden)]
|
||||
pub fn remove_weak<K: Into<UserKey>>(&mut self, p: &Keyspace, key: K) {
|
||||
self.data
|
||||
.push(Item::new(p.clone(), key, vec![], ValueType::WeakTombstone));
|
||||
}
|
||||
|
||||
/// Commits the batch to the [`Database`] atomically.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn commit(mut self) -> crate::Result<()> {
|
||||
if self.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::trace!("batch: Acquiring journal writer");
|
||||
let mut journal_writer = self.db.supervisor.journal.get_writer()?;
|
||||
|
||||
// IMPORTANT: Check the poisoned flag after getting journal mutex, otherwise TOCTOU
|
||||
if self.db.is_poisoned.is_poisoned() {
|
||||
return Err(crate::Error::Poisoned);
|
||||
}
|
||||
|
||||
let batch_seqno = self.db.supervisor.seqno.next();
|
||||
|
||||
journal_writer.write_batch(self.data.iter(), self.data.len(), batch_seqno)?;
|
||||
|
||||
if let Some(mode) = self.durability {
|
||||
if let Err(e) = journal_writer.persist(mode) {
|
||||
self.db.is_poisoned.poison();
|
||||
|
||||
log::error!(
|
||||
"persist failed, which is a FATAL, and possibly hardware-related, failure: {e:?}"
|
||||
);
|
||||
|
||||
return Err(crate::Error::Poisoned);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe we can use a stack alloc hashset/vec here, such as smallset
|
||||
#[expect(clippy::mutable_key_type)]
|
||||
let mut keyspaces_with_possible_stall = HashSet::new();
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let keyspaces = self
|
||||
.db
|
||||
.supervisor
|
||||
.keyspaces
|
||||
.read()
|
||||
.expect("lock is poisoned");
|
||||
|
||||
let mut batch_size = 0u64;
|
||||
|
||||
log::trace!("Applying batch (size={}) to memtable(s)", self.data.len());
|
||||
|
||||
for item in std::mem::take(&mut self.data) {
|
||||
// TODO: need a better, generic write op
|
||||
let (item_size, _) = match item.value_type {
|
||||
ValueType::Value => item.keyspace.tree.insert(item.key, item.value, batch_seqno),
|
||||
ValueType::Tombstone => item.keyspace.tree.remove(item.key, batch_seqno),
|
||||
ValueType::WeakTombstone => item.keyspace.tree.remove_weak(item.key, batch_seqno),
|
||||
ValueType::Indirection => unreachable!(),
|
||||
};
|
||||
|
||||
batch_size += item_size;
|
||||
|
||||
// IMPORTANT: Clone the handle, because we don't want to keep the keyspaces lock open
|
||||
keyspaces_with_possible_stall.insert(item.keyspace.clone());
|
||||
}
|
||||
|
||||
self.db.supervisor.snapshot_tracker.publish(batch_seqno);
|
||||
|
||||
drop(journal_writer);
|
||||
|
||||
log::trace!("batch: Freed journal writer");
|
||||
|
||||
drop(keyspaces);
|
||||
|
||||
// IMPORTANT: Add batch size to current write buffer size
|
||||
// Otherwise write buffer growth is unbounded when using batches
|
||||
self.db.supervisor.write_buffer_size.allocate(batch_size);
|
||||
|
||||
// Check each affected keyspace for write stall/halt
|
||||
for keyspace in &keyspaces_with_possible_stall {
|
||||
let memtable_size = keyspace.tree.active_memtable().size();
|
||||
keyspace.check_memtable_rotate(memtable_size);
|
||||
keyspace.local_backpressure();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{db_config::CompactionFilterAssigner, tx::single_writer::Openable, Config};
|
||||
use lsm_tree::{Cache, CompressionType, DescriptorTable};
|
||||
use std::{marker::PhantomData, path::Path, sync::Arc};
|
||||
|
||||
/// Database builder
|
||||
pub struct Builder<O: Openable> {
|
||||
inner: Config,
|
||||
_phantom: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<O: Openable> Builder<O> {
|
||||
pub(crate) fn new(path: &Path) -> Self {
|
||||
Self {
|
||||
inner: Config::new(path),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn into_config(self) -> Config {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Opens the database, creating it if it does not exist.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors if an I/O error occurred, or if the database can not be opened.
|
||||
pub fn open(self) -> crate::Result<O> {
|
||||
O::open(self.inner)
|
||||
}
|
||||
|
||||
/// Sets the cache capacity in bytes.
|
||||
///
|
||||
/// It is recommended to configure the block cache capacity to be ~20-25% of the available memory - or more **if** the data set _fully_ fits into memory.
|
||||
#[must_use]
|
||||
pub fn cache_size(mut self, size_bytes: u64) -> Self {
|
||||
self.inner.cache = Arc::new(Cache::with_capacity_bytes(size_bytes));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the compression type to use for large values that are written into the journal file.
|
||||
#[must_use]
|
||||
pub fn journal_compression(mut self, comp: CompressionType) -> Self {
|
||||
self.inner.journal_compression_type = comp;
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false`, write batches or transactions automatically flush data to the operating system.
|
||||
///
|
||||
/// Default = false
|
||||
///
|
||||
/// Set to `true` to handle persistence manually, e.g. manually using `PersistMode::SyncData` for ACID transactions.
|
||||
#[must_use]
|
||||
pub fn manual_journal_persist(mut self, flag: bool) -> Self {
|
||||
self.inner.manual_journal_persist = flag;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the number of worker threads.
|
||||
///
|
||||
/// Default = min(# CPU cores, 4)
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics, if below 1.
|
||||
#[must_use]
|
||||
pub fn worker_threads(self, n: usize) -> Self {
|
||||
#[cfg(not(test))]
|
||||
assert!(n > 0, "worker count must be at least 1");
|
||||
|
||||
self.worker_threads_unchecked(n)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn worker_threads_unchecked(mut self, n: usize) -> Self {
|
||||
self.inner.worker_threads = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the upper limit for cached file descriptors.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Setting to None is currently not supported.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if n < 10 or `None`.
|
||||
#[must_use]
|
||||
pub fn max_cached_files(mut self, n: Option<usize>) -> Self {
|
||||
self.inner.descriptor_table = n.map(|n| Arc::new(DescriptorTable::new(n)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum size of all journals in bytes.
|
||||
///
|
||||
/// Default = 512 MiB
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if < 64 MiB.
|
||||
///
|
||||
/// Same as `max_total_wal_size` in `RocksDB`.
|
||||
#[must_use]
|
||||
pub fn max_journaling_size(mut self, bytes: u64) -> Self {
|
||||
assert!(bytes >= 64 * 1_024 * 1_024);
|
||||
|
||||
self.inner.max_journaling_size_in_bytes = bytes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum size of all memtables in bytes.
|
||||
///
|
||||
/// Similar to `db_write_buffer_size` in `RocksDB`, however it is disabled by default in `RocksDB`.
|
||||
///
|
||||
/// Set to `u64::MAX` or `0` to disable it.
|
||||
///
|
||||
/// Default = off
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if bytes < 1 MiB.
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
#[deprecated = "todo"]
|
||||
pub fn max_write_buffer_size(mut self, bytes: Option<u64>) -> Self {
|
||||
if let Some(bytes) = bytes {
|
||||
assert!(bytes >= 1_024 * 1_024);
|
||||
}
|
||||
|
||||
self.inner.max_write_buffer_size_in_bytes = bytes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Database` to clean upon drop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{PersistMode, Database, KeyspaceCreateOptions};
|
||||
/// # let folder = tempfile::tempdir()?.into_path();
|
||||
/// let db = Database::builder(&folder).temporary(true).open()?;
|
||||
///
|
||||
/// assert!(folder.try_exists()?);
|
||||
/// drop(db);
|
||||
/// assert!(!folder.try_exists()?);
|
||||
/// #
|
||||
/// # Ok::<_, fjall::Error>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn temporary(mut self, flag: bool) -> Self {
|
||||
self.inner.clean_path_on_drop = flag;
|
||||
self
|
||||
}
|
||||
|
||||
/// Installs a factory that assigns compaction filters to new or recovered keyspaces.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{PersistMode, Database, KeyspaceCreateOptions};
|
||||
/// # use std::sync::Arc;
|
||||
/// # let folder = tempfile::tempdir()?.keep();
|
||||
/// use lsm_tree::compaction::filter::Factory;
|
||||
///
|
||||
/// let db = Database::builder(&folder)
|
||||
/// .temporary(true)
|
||||
/// .with_compaction_filter_factories(
|
||||
/// Arc::new(|keyspace| {
|
||||
/// // Match on the keyspace name to assign specific compaction filters
|
||||
/// todo!()
|
||||
/// })
|
||||
/// )
|
||||
/// .open()?;
|
||||
///
|
||||
/// #
|
||||
/// # Ok::<_, fjall::Error>(())
|
||||
/// ```
|
||||
pub fn with_compaction_filter_factories(mut self, f: CompactionFilterAssigner) -> Self {
|
||||
self.inner.compaction_filter_factory_assigner = Some(f);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
pub(crate) mod worker;
|
||||
|
||||
pub use lsm_tree::compaction::{Fifo, Leveled, Levelled};
|
||||
|
||||
/// Compaction filter utilities
|
||||
pub mod filter {
|
||||
pub use lsm_tree::compaction::filter::{
|
||||
CompactionFilter, Context, Factory, ItemAccessor, Verdict,
|
||||
};
|
||||
|
||||
/// Alias for compaction filter return type
|
||||
pub type CompactionFilterResult = lsm_tree::Result<Verdict>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{snapshot_tracker::SnapshotTracker, stats::Stats, Keyspace};
|
||||
use lsm_tree::AbstractTree;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Runs a single run of compaction.
|
||||
pub fn run(
|
||||
keyspace: &Keyspace,
|
||||
snapshot_tracker: &SnapshotTracker,
|
||||
stats: &Stats,
|
||||
) -> crate::Result<()> {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
|
||||
if keyspace.is_deleted.load(Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"Checking compaction strategy for keyspace {:?}",
|
||||
keyspace.name,
|
||||
);
|
||||
|
||||
let strategy = keyspace.config.compaction_strategy.clone();
|
||||
|
||||
stats.active_compaction_count.fetch_add(1, Relaxed);
|
||||
|
||||
log::debug!("Compacting keyspace {:?}", keyspace.name);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
if let Err(e) = keyspace
|
||||
.tree
|
||||
.compact(strategy.clone(), snapshot_tracker.get_seqno_safe_to_gc())
|
||||
{
|
||||
log::error!("Compaction failed: {e:?}");
|
||||
stats.active_compaction_count.fetch_sub(1, Relaxed);
|
||||
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
// TODO: we need feedback from the compaction strategy...
|
||||
// TODO: if there is nothing more to do, we should clear the compaction_manager semaphore
|
||||
|
||||
// NOTE: Throttle a bit to avoid a storm of compaction choice attempts
|
||||
// (in case of write throttling)
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
stats
|
||||
.time_compacting
|
||||
.fetch_add(start.elapsed().as_micros() as u64, Relaxed);
|
||||
|
||||
stats.active_compaction_count.fetch_sub(1, Relaxed);
|
||||
stats.compactions_completed.fetch_add(1, Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,921 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{
|
||||
batch::WriteBatch,
|
||||
db_config::Config,
|
||||
file::{fsync_directory, KEYSPACES_FOLDER, LOCK_FILE, VERSION_MARKER},
|
||||
flush::manager::FlushManager,
|
||||
journal::{manager::JournalManager, writer::PersistMode, Journal},
|
||||
keyspace::{name::is_valid_keyspace_name, KeyspaceKey},
|
||||
locked_file::LockedFileGuard,
|
||||
meta_keyspace::MetaKeyspace,
|
||||
poison::{PoisonDart, PoisonSignal},
|
||||
recovery::{recover_keyspaces, recover_sealed_memtables},
|
||||
snapshot::Snapshot,
|
||||
snapshot_tracker::SnapshotTracker,
|
||||
stats::Stats,
|
||||
supervisor::{Supervisor, SupervisorInner},
|
||||
tx::single_writer::Openable,
|
||||
version::FormatVersion,
|
||||
worker_pool::{WorkerMessage, WorkerPool},
|
||||
write_buffer_manager::WriteBufferManager,
|
||||
HashMap, Keyspace, KeyspaceCreateOptions,
|
||||
};
|
||||
use lsm_tree::{AbstractTree, SequenceNumberCounter};
|
||||
use std::{
|
||||
fs::remove_dir_all,
|
||||
path::Path,
|
||||
sync::{atomic::AtomicUsize, Arc, RwLock},
|
||||
};
|
||||
|
||||
pub type Keyspaces = HashMap<KeyspaceKey, Keyspace>;
|
||||
|
||||
pub struct DatabaseInner {
|
||||
pub(crate) meta_keyspace: MetaKeyspace,
|
||||
|
||||
/// Database configuration
|
||||
#[doc(hidden)]
|
||||
pub config: Config,
|
||||
|
||||
#[doc(hidden)]
|
||||
pub supervisor: Supervisor,
|
||||
|
||||
/// Stop signal when database is dropped to stop background threads
|
||||
pub(crate) stop_signal: lsm_tree::stop_signal::StopSignal,
|
||||
|
||||
/// Counter of background threads
|
||||
pub(crate) active_thread_counter: Arc<AtomicUsize>,
|
||||
|
||||
/// True if fsync failed
|
||||
pub(crate) is_poisoned: PoisonSignal,
|
||||
|
||||
pub(crate) stats: Arc<Stats>,
|
||||
|
||||
pub(crate) keyspace_id_counter: SequenceNumberCounter,
|
||||
|
||||
pub worker_pool: WorkerPool,
|
||||
|
||||
pub(crate) lock_file: LockedFileGuard,
|
||||
}
|
||||
|
||||
impl Drop for DatabaseInner {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("Dropping database");
|
||||
|
||||
self.stop_signal.send();
|
||||
|
||||
let _ = self.worker_pool.rx.drain().count();
|
||||
|
||||
while self
|
||||
.active_thread_counter
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
> 0
|
||||
{
|
||||
let _ = self.worker_pool.sender.send(WorkerMessage::Close);
|
||||
std::thread::sleep(std::time::Duration::from_micros(10));
|
||||
}
|
||||
|
||||
// Drain again after threads are closed
|
||||
let _ = self.worker_pool.rx.drain().count();
|
||||
|
||||
// IMPORTANT: Break cyclic Arcs
|
||||
self.supervisor.flush_manager.clear();
|
||||
self.supervisor
|
||||
.keyspaces
|
||||
.write()
|
||||
.expect("lock is poisoned")
|
||||
.clear();
|
||||
self.supervisor
|
||||
.journal_manager
|
||||
.write()
|
||||
.expect("lock is poisoned")
|
||||
.clear();
|
||||
|
||||
if self.config.clean_path_on_drop {
|
||||
log::info!(
|
||||
"Deleting database because temporary=true: {}",
|
||||
self.config.path.display(),
|
||||
);
|
||||
|
||||
if let Err(err) = remove_dir_all(&self.config.path) {
|
||||
log::warn!(
|
||||
"Failed to clean up path: {} - {err}",
|
||||
self.config.path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::decrement_drop_counter();
|
||||
}
|
||||
}
|
||||
|
||||
/// A database is a single logical database
|
||||
/// which can house multiple keyspaces
|
||||
///
|
||||
/// In your application, you should create a single database
|
||||
/// and keep it around for as long as needed
|
||||
/// (as long as you are using its keyspaces).
|
||||
#[derive(Clone)]
|
||||
#[doc(alias = "database")]
|
||||
#[doc(alias = "collection")]
|
||||
pub struct Database(pub(crate) Arc<DatabaseInner>);
|
||||
|
||||
impl std::ops::Deref for Database {
|
||||
type Target = DatabaseInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Openable for Database {
|
||||
fn open(config: Config) -> crate::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::open(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Opens a cross-keyspace snapshot.
|
||||
///
|
||||
/// # Caution
|
||||
///
|
||||
/// Note that for serializable semantics you need to use a transactional database instead.
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> Snapshot {
|
||||
Snapshot::new(self.supervisor.snapshot_tracker.open())
|
||||
}
|
||||
|
||||
/// Creates a new database builder to create or open a database at `path`.
|
||||
pub fn builder(path: impl AsRef<Path>) -> crate::DatabaseBuilder<Self> {
|
||||
crate::DatabaseBuilder::new(path.as_ref())
|
||||
}
|
||||
|
||||
/// Initializes a new atomic write batch.
|
||||
///
|
||||
/// Items may be written to multiple keyspaces, which
|
||||
/// will be be updated atomically when the batch is committed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// let mut batch = db.batch();
|
||||
///
|
||||
/// assert_eq!(tree.len()?, 0);
|
||||
/// batch.insert(&tree, "1", "abc");
|
||||
/// batch.insert(&tree, "3", "abc");
|
||||
/// batch.insert(&tree, "5", "abc");
|
||||
///
|
||||
/// assert_eq!(tree.len()?, 0);
|
||||
///
|
||||
/// batch.commit()?;
|
||||
/// assert_eq!(tree.len()?, 3);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn batch(&self) -> WriteBatch {
|
||||
let mut batch = WriteBatch::new(self.clone());
|
||||
|
||||
if !self.config.manual_journal_persist {
|
||||
batch = batch.durability(Some(PersistMode::Buffer));
|
||||
}
|
||||
|
||||
batch
|
||||
}
|
||||
|
||||
// TODO: refactor: accessor to stats(), so we don't have that many methods in DB
|
||||
|
||||
/// Returns the current write buffer size (active + sealed memtables).
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This is a non-stable API currently.
|
||||
#[must_use]
|
||||
#[doc(hidden)]
|
||||
pub fn write_buffer_size(&self) -> u64 {
|
||||
self.supervisor.write_buffer_size.get()
|
||||
}
|
||||
|
||||
/// Returns the number of queued memtable flush tasks.
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This is a non-stable API currently.
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn outstanding_flushes(&self) -> usize {
|
||||
self.supervisor.flush_manager.len()
|
||||
}
|
||||
|
||||
/// Returns the time all compactions took until now.
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This is a non-stable API currently.
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn time_compacting(&self) -> std::time::Duration {
|
||||
let us = self
|
||||
.stats
|
||||
.time_compacting
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
std::time::Duration::from_micros(us)
|
||||
}
|
||||
|
||||
/// Returns the number of active compactions currently running.
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This is a non-stable API currently.
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn active_compactions(&self) -> usize {
|
||||
self.stats
|
||||
.active_compaction_count
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns the number of completed compactions.
|
||||
///
|
||||
/// # Experimental
|
||||
///
|
||||
/// This is a non-stable API currently.
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn compactions_completed(&self) -> usize {
|
||||
self.stats
|
||||
.compactions_completed
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns the number of journals on disk.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// assert_eq!(1, db.journal_count());
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn journal_count(&self) -> usize {
|
||||
self.supervisor
|
||||
.journal_manager
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.journal_count()
|
||||
}
|
||||
|
||||
/// Returns the disk space usage of the journal.
|
||||
#[doc(hidden)]
|
||||
pub fn journal_disk_space(&self) -> crate::Result<u64> {
|
||||
Ok(self.supervisor.journal.get_writer()?.len()?
|
||||
+ self
|
||||
.supervisor
|
||||
.journal_manager
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.disk_space_used())
|
||||
}
|
||||
|
||||
/// Returns the disk space usage of the entire database.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let _tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// assert!(db.disk_space()? > 0);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
pub fn disk_space(&self) -> crate::Result<u64> {
|
||||
let journal_size = self.journal_disk_space()?;
|
||||
|
||||
let keyspaces_size = self
|
||||
.supervisor
|
||||
.keyspaces
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.values()
|
||||
.map(Keyspace::disk_space)
|
||||
.sum::<u64>();
|
||||
|
||||
Ok(journal_size + keyspaces_size)
|
||||
}
|
||||
|
||||
/// Flushes the active journal. The durability depends on the [`PersistMode`]
|
||||
/// used.
|
||||
///
|
||||
/// Persisting only affects durability, NOT consistency! Even without flushing
|
||||
/// data is crash-safe.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{PersistMode, Database, KeyspaceCreateOptions};
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// let db = Database::builder(folder).open()?;
|
||||
/// let items = db.keyspace("my_items", KeyspaceCreateOptions::default)?;
|
||||
///
|
||||
/// items.insert("a", "hello")?;
|
||||
///
|
||||
/// db.persist(PersistMode::SyncAll)?;
|
||||
/// #
|
||||
/// # Ok::<_, fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error, if an IO error occurred.
|
||||
pub fn persist(&self, mode: PersistMode) -> crate::Result<()> {
|
||||
if self.is_poisoned.is_poisoned() {
|
||||
return Err(crate::Error::Poisoned);
|
||||
}
|
||||
|
||||
if let Err(e) = self.supervisor.journal.persist(mode) {
|
||||
self.is_poisoned.poison();
|
||||
|
||||
log::error!(
|
||||
"flush failed, which is a FATAL, and possibly hardware-related, failure: {e:?}"
|
||||
);
|
||||
|
||||
return Err(crate::Error::Poisoned);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn cache_capacity(&self) -> u64 {
|
||||
self.config.cache.capacity()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use]
|
||||
pub fn cache_size(&self) -> u64 {
|
||||
self.config.cache.size()
|
||||
}
|
||||
|
||||
/// Opens a database in the given directory.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error, if an IO error occurred.
|
||||
pub fn open(config: Config) -> crate::Result<Self> {
|
||||
log::debug!(
|
||||
"cache capacity={}MiB",
|
||||
config.cache.capacity() / 1_024 / 1_024,
|
||||
);
|
||||
|
||||
let db = Self::create_or_recover(config)?;
|
||||
// db.start_background_threads()?;
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::increment_drop_counter();
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Same as [`Database::open`], but does not start background threads.
|
||||
///
|
||||
/// Needed to open a database without threads for testing.
|
||||
///
|
||||
/// Should not be user-facing.
|
||||
#[doc(hidden)]
|
||||
pub fn create_or_recover(config: Config) -> crate::Result<Self> {
|
||||
if config.path.join(VERSION_MARKER).try_exists()? {
|
||||
Self::recover(config)
|
||||
} else {
|
||||
Self::create_new(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroys the keyspace, removing all data associated with it.
|
||||
///
|
||||
/// The keyspace folder will not be deleted until all references to it are dropped,
|
||||
/// so calling this is safe, even if the keyspace is still accessed in another thread.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
pub fn delete_keyspace(&self, handle: Keyspace) -> crate::Result<()> {
|
||||
self.meta_keyspace.remove_keyspace(&handle.name)?;
|
||||
|
||||
handle
|
||||
.is_deleted
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates or opens a keyspace.
|
||||
///
|
||||
/// If the keyspace does not yet exist, it will be created configured with `create_options`.
|
||||
/// Otherwise simply a handle to the existing keyspace will be returned.
|
||||
///
|
||||
/// Keyspace names can be up to 255 characters long and can not be empty.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error, if an IO error occurred.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the keyspace name is invalid.
|
||||
pub fn keyspace(
|
||||
&self,
|
||||
name: &str,
|
||||
create_options: impl FnOnce() -> KeyspaceCreateOptions,
|
||||
) -> crate::Result<Keyspace> {
|
||||
assert!(is_valid_keyspace_name(name));
|
||||
|
||||
let keyspaces = self.supervisor.keyspaces.write().expect("lock is poisoned");
|
||||
|
||||
Ok(if let Some(keyspace) = keyspaces.get(name) {
|
||||
keyspace.clone()
|
||||
} else {
|
||||
let name: KeyspaceKey = name.into();
|
||||
|
||||
let keyspace_id = self.keyspace_id_counter.next();
|
||||
|
||||
let mut opts = create_options();
|
||||
|
||||
// Install compaction filter factory if needed
|
||||
if let Some(f) = self
|
||||
.config
|
||||
.compaction_filter_factory_assigner
|
||||
.as_ref()
|
||||
.and_then(|f| f(&name))
|
||||
{
|
||||
opts = opts.with_compaction_filter_factory(f);
|
||||
}
|
||||
|
||||
let handle = Keyspace::create_new(keyspace_id, self, name.clone(), opts)?;
|
||||
|
||||
self.meta_keyspace
|
||||
.create_keyspace(keyspace_id, &name, handle.clone(), keyspaces)?;
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::increment_drop_counter();
|
||||
|
||||
handle
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of keyspaces.
|
||||
#[must_use]
|
||||
pub fn keyspace_count(&self) -> usize {
|
||||
self.supervisor
|
||||
.keyspaces
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.len()
|
||||
}
|
||||
|
||||
/// Gets a list of all keyspace names in the database.
|
||||
#[must_use]
|
||||
pub fn list_keyspace_names(&self) -> Vec<KeyspaceKey> {
|
||||
self.supervisor
|
||||
.keyspaces
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns `true` if the keyspace with the given name exists.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// assert!(!db.keyspace_exists("default"));
|
||||
/// db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// assert!(db.keyspace_exists("default"));
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn keyspace_exists(&self, name: &str) -> bool {
|
||||
self.meta_keyspace.keyspace_exists(name)
|
||||
}
|
||||
|
||||
/// Gets the would-be-next sequence number.
|
||||
#[must_use]
|
||||
#[doc(hidden)]
|
||||
pub fn seqno(&self) -> crate::SeqNo {
|
||||
self.supervisor.seqno.get()
|
||||
}
|
||||
|
||||
/// Gets the currently visible sequence number.
|
||||
#[must_use]
|
||||
#[doc(hidden)]
|
||||
pub fn visible_seqno(&self) -> crate::SeqNo {
|
||||
self.supervisor.snapshot_tracker.get()
|
||||
}
|
||||
|
||||
fn check_version<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
let bytes = std::fs::read(path.as_ref().join(VERSION_MARKER))?;
|
||||
|
||||
if let Some(version) = FormatVersion::parse_file_header(&bytes) {
|
||||
if version == FormatVersion::V2 {
|
||||
log::error!(
|
||||
"It looks like you are trying to open a V2 database - the database needs a manual migration, a tool is available at https://github.com/fjall-rs/migrate-v2-v3."
|
||||
);
|
||||
}
|
||||
if version as u8 > 4 {
|
||||
log::error!(
|
||||
"It looks like you are trying to open a database from the future. Are you a time traveller?"
|
||||
);
|
||||
}
|
||||
if version != FormatVersion::V4 {
|
||||
return Err(crate::Error::InvalidVersion(Some(version)));
|
||||
}
|
||||
} else {
|
||||
return Err(crate::Error::InvalidVersion(None));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recovers existing database from directory.
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[doc(hidden)]
|
||||
pub fn recover(config: Config) -> crate::Result<Self> {
|
||||
log::info!("Recovering database at {}", config.path.display());
|
||||
|
||||
// Check version
|
||||
Self::check_version(&config.path)?;
|
||||
|
||||
let lock_file = LockedFileGuard::try_acquire(&config.path.join(LOCK_FILE))?;
|
||||
|
||||
// TODO:
|
||||
// let recovery_mode = config.journal_recovery_mode;
|
||||
|
||||
// Reload active journal
|
||||
let journal_recovery = Journal::recover(
|
||||
&config.path,
|
||||
config.journal_compression_type,
|
||||
config.journal_compression_threshold,
|
||||
)?;
|
||||
log::debug!("journal recovery result: {journal_recovery:#?}");
|
||||
|
||||
let active_journal = Arc::new(journal_recovery.active);
|
||||
active_journal.get_writer()?.persist(PersistMode::SyncAll)?;
|
||||
|
||||
let sealed_journals = journal_recovery.sealed;
|
||||
|
||||
let journal_manager = JournalManager::new();
|
||||
|
||||
let seqno = SequenceNumberCounter::default();
|
||||
let visible_seqno = SequenceNumberCounter::default();
|
||||
|
||||
let meta_tree = lsm_tree::Config::new(
|
||||
config.path.join(KEYSPACES_FOLDER).join("0"),
|
||||
seqno.clone(),
|
||||
visible_seqno.clone(),
|
||||
)
|
||||
.use_cache(config.cache.clone())
|
||||
.use_descriptor_table(config.descriptor_table.clone())
|
||||
.expect_point_read_hits(true)
|
||||
.data_block_size_policy(crate::config::BlockSizePolicy::all(4_096))
|
||||
.data_block_hash_ratio_policy(crate::config::HashRatioPolicy::all(8.0))
|
||||
.data_block_compression_policy(crate::config::CompressionPolicy::disabled())
|
||||
.data_block_restart_interval_policy(crate::config::RestartIntervalPolicy::all(1))
|
||||
.index_block_compression_policy(crate::config::CompressionPolicy::disabled())
|
||||
.filter_policy(crate::config::FilterPolicy::new([
|
||||
lsm_tree::config::FilterPolicyEntry::Bloom(
|
||||
lsm_tree::config::BloomConstructionPolicy::FalsePositiveRate(0.0001),
|
||||
),
|
||||
lsm_tree::config::FilterPolicyEntry::Bloom(
|
||||
lsm_tree::config::BloomConstructionPolicy::FalsePositiveRate(0.01),
|
||||
),
|
||||
]))
|
||||
.open()?;
|
||||
|
||||
let keyspaces = Arc::new(RwLock::default());
|
||||
|
||||
let meta_keyspace = MetaKeyspace::new(
|
||||
meta_tree,
|
||||
keyspaces.clone(),
|
||||
seqno.clone(),
|
||||
visible_seqno.clone(),
|
||||
);
|
||||
|
||||
let supervisor = Supervisor::new(SupervisorInner {
|
||||
db_config: config.clone(),
|
||||
keyspaces,
|
||||
flush_manager: FlushManager::new(),
|
||||
write_buffer_size: WriteBufferManager::default(),
|
||||
snapshot_tracker: SnapshotTracker::new(visible_seqno),
|
||||
journal: active_journal,
|
||||
journal_manager: Arc::new(RwLock::new(journal_manager)),
|
||||
seqno,
|
||||
});
|
||||
|
||||
let active_thread_counter = Arc::<AtomicUsize>::default();
|
||||
let stats = Arc::<Stats>::default();
|
||||
|
||||
// Construct (empty) database, then fill back with keyspace data
|
||||
let inner = DatabaseInner {
|
||||
supervisor,
|
||||
worker_pool: WorkerPool::prepare(),
|
||||
keyspace_id_counter: SequenceNumberCounter::new(1),
|
||||
meta_keyspace: meta_keyspace.clone(),
|
||||
config,
|
||||
stop_signal: lsm_tree::stop_signal::StopSignal::default(),
|
||||
active_thread_counter,
|
||||
is_poisoned: PoisonSignal::default(),
|
||||
stats,
|
||||
lock_file,
|
||||
};
|
||||
|
||||
let db = Self(Arc::new(inner));
|
||||
|
||||
// Recover keyspaces
|
||||
recover_keyspaces(&db, &meta_keyspace)?;
|
||||
|
||||
// Recover sealed memtables by walking through old journals
|
||||
recover_sealed_memtables(
|
||||
&db,
|
||||
&sealed_journals
|
||||
.into_iter()
|
||||
.map(|(_, x)| x)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
{
|
||||
#[expect(clippy::expect_used)]
|
||||
let keyspaces = db.supervisor.keyspaces.read().expect("lock is poisoned");
|
||||
|
||||
// NOTE: If this triggers, the last sealed memtable
|
||||
// was not correctly rotated
|
||||
for keyspace in keyspaces.values() {
|
||||
if keyspace.tree.active_memtable().size() > 0 {
|
||||
log::error!(
|
||||
"Active memtable is not empty after recovery for keyspace {:?} - recovery failed",
|
||||
keyspace.name
|
||||
);
|
||||
return Err(crate::Error::Unrecoverable);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: We only need to recover the active journal, if it actually existed before
|
||||
// nothing to recover, if we just created it
|
||||
if !journal_recovery.was_active_created {
|
||||
log::trace!("Recovering active memtables from active journal");
|
||||
|
||||
let reader = db.supervisor.journal.get_reader()?;
|
||||
|
||||
for batch in reader {
|
||||
let batch = batch?;
|
||||
|
||||
for item in batch.items {
|
||||
let Some(keyspace_name) = db.meta_keyspace.resolve_id(item.keyspace_id)?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(keyspace) = keyspaces.get(&keyspace_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let tree = &keyspace.tree;
|
||||
|
||||
match item.value_type {
|
||||
lsm_tree::ValueType::Value => {
|
||||
tree.insert(item.key, item.value, batch.seqno);
|
||||
}
|
||||
lsm_tree::ValueType::Tombstone => {
|
||||
tree.remove(item.key, batch.seqno);
|
||||
}
|
||||
lsm_tree::ValueType::WeakTombstone => {
|
||||
tree.remove_weak(item.key, batch.seqno);
|
||||
}
|
||||
lsm_tree::ValueType::Indirection => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for keyspace_id in &batch.cleared_keyspaces {
|
||||
let Some(keyspace_name) = db.meta_keyspace.resolve_id(*keyspace_id)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(keyspace) = keyspaces.get(&keyspace_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
keyspace.tree.clear().ok();
|
||||
}
|
||||
}
|
||||
|
||||
for keyspace in keyspaces.values() {
|
||||
let size = keyspace.tree.active_memtable().size();
|
||||
|
||||
log::trace!(
|
||||
"Recovered active memtable of size {size}B for keyspace {:?} ({} items)",
|
||||
keyspace.name,
|
||||
keyspace.tree.active_memtable().len(),
|
||||
);
|
||||
|
||||
// IMPORTANT: Add active memtable size to current write buffer size
|
||||
db.supervisor.write_buffer_size.allocate(size);
|
||||
|
||||
// Recover seqno
|
||||
let maybe_next_seqno = keyspace
|
||||
.tree
|
||||
.get_highest_seqno()
|
||||
.map(|x| x + 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
db.supervisor.seqno.fetch_max(maybe_next_seqno);
|
||||
log::debug!("Database seqno is now {}", db.supervisor.seqno.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.supervisor
|
||||
.snapshot_tracker
|
||||
.set(db.supervisor.seqno.get());
|
||||
|
||||
db.supervisor.snapshot_tracker.gc();
|
||||
|
||||
for keyspace in db
|
||||
.supervisor
|
||||
.keyspaces
|
||||
.read()
|
||||
.expect("lock is poisoned")
|
||||
.values()
|
||||
{
|
||||
if keyspace.tree.sealed_memtable_count() > 0 {
|
||||
log::debug!(
|
||||
"Queuing keyspace {:?} to get flushed because sealed memtables > 0",
|
||||
keyspace.name(),
|
||||
);
|
||||
|
||||
// IMPORTANT: Add task to flush manager, so it can be flushed
|
||||
db.supervisor
|
||||
.flush_manager
|
||||
.enqueue(Arc::new(crate::flush::Task {
|
||||
keyspace: keyspace.clone(),
|
||||
}));
|
||||
|
||||
keyspace.worker_messager.send(WorkerMessage::Flush).ok();
|
||||
} else if keyspace.tree.l0_run_count() > 0 {
|
||||
log::debug!(
|
||||
"Queuing keyspace {:?} to maybe get compacted because L0 runs > 0",
|
||||
keyspace.name(),
|
||||
);
|
||||
|
||||
keyspace
|
||||
.worker_messager
|
||||
.send(WorkerMessage::Compact(keyspace.clone()))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
db.worker_pool.start(
|
||||
db.config.worker_threads,
|
||||
&db.supervisor,
|
||||
&db.stats,
|
||||
&PoisonDart::new(db.is_poisoned.clone()),
|
||||
&db.active_thread_counter,
|
||||
)?;
|
||||
|
||||
log::trace!("Database recovery successful");
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn create_new(config: Config) -> crate::Result<Self> {
|
||||
log::info!("Creating database at {}", config.path.display());
|
||||
|
||||
std::fs::create_dir_all(&config.path)?;
|
||||
|
||||
let lock_file = LockedFileGuard::create_new(&config.path.join(LOCK_FILE))?;
|
||||
|
||||
let journal_folder_path = &config.path;
|
||||
let keyspaces_folder_path = config.path.join(KEYSPACES_FOLDER);
|
||||
|
||||
std::fs::create_dir_all(&keyspaces_folder_path)?;
|
||||
|
||||
let active_journal_path = journal_folder_path.join("0.jnl");
|
||||
let journal = Journal::create_new(&active_journal_path)?.with_compression(
|
||||
config.journal_compression_type,
|
||||
config.journal_compression_threshold,
|
||||
);
|
||||
let journal = Arc::new(journal);
|
||||
|
||||
// NOTE: Lastly, fsync version marker, which contains the version
|
||||
let mut marker = std::fs::File::create_new(config.path.join(VERSION_MARKER))?;
|
||||
FormatVersion::V4.write_file_header(&mut marker)?;
|
||||
marker.sync_all()?;
|
||||
|
||||
// IMPORTANT: fsync folders on Unix
|
||||
fsync_directory(&keyspaces_folder_path)?;
|
||||
fsync_directory(&config.path)?;
|
||||
|
||||
let seqno = SequenceNumberCounter::default();
|
||||
let visible_seqno = SequenceNumberCounter::default();
|
||||
|
||||
let meta_tree = lsm_tree::Config::new(
|
||||
config.path.join(KEYSPACES_FOLDER).join("0"),
|
||||
seqno.clone(),
|
||||
visible_seqno.clone(),
|
||||
)
|
||||
.use_cache(config.cache.clone())
|
||||
.use_descriptor_table(config.descriptor_table.clone())
|
||||
.expect_point_read_hits(true)
|
||||
.data_block_size_policy(crate::config::BlockSizePolicy::all(4_096))
|
||||
.data_block_hash_ratio_policy(crate::config::HashRatioPolicy::all(8.0))
|
||||
.data_block_compression_policy(crate::config::CompressionPolicy::disabled())
|
||||
.data_block_restart_interval_policy(crate::config::RestartIntervalPolicy::all(1))
|
||||
.index_block_compression_policy(crate::config::CompressionPolicy::disabled())
|
||||
.filter_policy(crate::config::FilterPolicy::new([
|
||||
lsm_tree::config::FilterPolicyEntry::Bloom(
|
||||
lsm_tree::config::BloomConstructionPolicy::FalsePositiveRate(0.0001),
|
||||
),
|
||||
lsm_tree::config::FilterPolicyEntry::Bloom(
|
||||
lsm_tree::config::BloomConstructionPolicy::FalsePositiveRate(0.01),
|
||||
),
|
||||
]))
|
||||
.open()?;
|
||||
|
||||
let keyspaces = Arc::new(RwLock::default());
|
||||
|
||||
let meta_keyspace = MetaKeyspace::new(
|
||||
meta_tree,
|
||||
keyspaces.clone(),
|
||||
seqno.clone(),
|
||||
visible_seqno.clone(),
|
||||
);
|
||||
|
||||
let supervisor = Supervisor::new(SupervisorInner {
|
||||
db_config: config.clone(),
|
||||
keyspaces,
|
||||
flush_manager: FlushManager::new(),
|
||||
write_buffer_size: WriteBufferManager::default(),
|
||||
snapshot_tracker: SnapshotTracker::new(visible_seqno),
|
||||
journal,
|
||||
journal_manager: Arc::new(RwLock::new(JournalManager::new())),
|
||||
seqno,
|
||||
});
|
||||
|
||||
let active_thread_counter = Arc::<AtomicUsize>::default();
|
||||
let stats = Arc::<Stats>::default();
|
||||
|
||||
let inner = DatabaseInner {
|
||||
supervisor,
|
||||
worker_pool: WorkerPool::prepare(),
|
||||
keyspace_id_counter: SequenceNumberCounter::new(1),
|
||||
meta_keyspace,
|
||||
config,
|
||||
stop_signal: lsm_tree::stop_signal::StopSignal::default(),
|
||||
active_thread_counter,
|
||||
is_poisoned: PoisonSignal::default(),
|
||||
stats,
|
||||
lock_file,
|
||||
};
|
||||
|
||||
let db = Self(Arc::new(inner));
|
||||
|
||||
db.worker_pool.start(
|
||||
db.config.worker_threads,
|
||||
&db.supervisor,
|
||||
&db.stats,
|
||||
&PoisonDart::new(db.is_poisoned.clone()),
|
||||
&db.active_thread_counter,
|
||||
)?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::path::absolute_path;
|
||||
use lsm_tree::{Cache, CompressionType, DescriptorTable};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub type CompactionFilterAssigner =
|
||||
Arc<dyn Fn(&str) -> Option<Arc<dyn lsm_tree::compaction::filter::Factory>> + Send + Sync>;
|
||||
|
||||
/// Global database configuration
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
/// Base path of database
|
||||
pub(crate) path: PathBuf,
|
||||
|
||||
/// When true, the path will be deleted upon drop
|
||||
pub(crate) clean_path_on_drop: bool,
|
||||
|
||||
#[doc(hidden)]
|
||||
pub cache: Arc<Cache>,
|
||||
|
||||
/// Descriptor table that will be shared between keyspaces
|
||||
pub(crate) descriptor_table: Option<Arc<DescriptorTable>>,
|
||||
|
||||
/// Max size of all journals in bytes
|
||||
pub(crate) max_journaling_size_in_bytes: u64, // TODO: should be configurable during runtime: AtomicU64
|
||||
|
||||
/// Max size of all active memtables
|
||||
///
|
||||
/// This can be used to cap the memory usage if there are
|
||||
/// many (possibly inactive) keyspaces.
|
||||
pub(crate) max_write_buffer_size_in_bytes: Option<u64>, // TODO: should be configurable during runtime: AtomicU64
|
||||
|
||||
pub(crate) manual_journal_persist: bool,
|
||||
|
||||
/// Number of concurrent worker threads
|
||||
pub(crate) worker_threads: usize,
|
||||
|
||||
pub(crate) journal_compression_type: CompressionType,
|
||||
|
||||
pub(crate) journal_compression_threshold: usize,
|
||||
|
||||
// pub(crate) journal_recovery_mode: RecoveryMode,
|
||||
//
|
||||
pub(crate) compaction_filter_factory_assigner: Option<CompactionFilterAssigner>,
|
||||
}
|
||||
|
||||
const DEFAULT_CPU_CORES: usize = 4;
|
||||
|
||||
fn get_open_file_limit() -> usize {
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
return 900;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return 400;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return 150;
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates a new configuration
|
||||
pub fn new(path: &Path) -> Self {
|
||||
let queried_cores = std::thread::available_parallelism().map(usize::from);
|
||||
let worker_threads = queried_cores.unwrap_or(1).min(DEFAULT_CPU_CORES);
|
||||
|
||||
Self {
|
||||
path: absolute_path(path),
|
||||
clean_path_on_drop: false,
|
||||
descriptor_table: Some(Arc::new(DescriptorTable::new(get_open_file_limit()))),
|
||||
max_write_buffer_size_in_bytes: None,
|
||||
max_journaling_size_in_bytes: /* 512 MiB */ 512 * 1_024 * 1_024,
|
||||
worker_threads,
|
||||
// journal_recovery_mode: RecoveryMode::default(),
|
||||
manual_journal_persist: false,
|
||||
|
||||
#[cfg(not(feature = "lz4"))]
|
||||
journal_compression_type: CompressionType::None,
|
||||
|
||||
#[cfg(feature = "lz4")]
|
||||
journal_compression_type: CompressionType::Lz4,
|
||||
|
||||
journal_compression_threshold: 4_096,
|
||||
|
||||
cache: Arc::new(Cache::with_capacity_bytes(/* 32 MiB */ 32 * 1_024 * 1_024)),
|
||||
|
||||
compaction_filter_factory_assigner: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
use crate::{Database, KeyspaceCreateOptions, KvSeparationOptions};
|
||||
use test_log::test;
|
||||
|
||||
#[test_log::test]
|
||||
fn clear_recover_sealed() -> crate::Result<()> {
|
||||
use crate::{Database, KeyspaceCreateOptions};
|
||||
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
{
|
||||
let db = Database::builder(&folder).open()?;
|
||||
|
||||
let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
assert!(tree.is_empty()?);
|
||||
|
||||
tree.insert("a", "a")?;
|
||||
assert!(tree.contains_key("a")?);
|
||||
|
||||
tree.clear()?;
|
||||
assert!(tree.is_empty()?);
|
||||
|
||||
tree.rotate_memtable_and_wait()?;
|
||||
assert!(tree.is_empty()?);
|
||||
db.supervisor.journal.get_writer()?.rotate()?;
|
||||
|
||||
tree.insert("b", "a")?;
|
||||
assert!(tree.contains_key("b")?);
|
||||
}
|
||||
|
||||
{
|
||||
let db = Database::builder(&folder).open()?;
|
||||
|
||||
let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
|
||||
assert!(!tree.contains_key("a")?);
|
||||
assert!(tree.contains_key("b")?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: investigate: flaky on macOS???
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
#[test]
|
||||
#[ignore = "restore"]
|
||||
fn whitebox_db_drop() -> crate::Result<()> {
|
||||
use crate::Database;
|
||||
|
||||
{
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
let db = Database::builder(&folder).open()?;
|
||||
assert_eq!(5, crate::drop::load_drop_counter());
|
||||
|
||||
drop(db);
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
}
|
||||
|
||||
{
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
let db = Database::builder(&folder).open()?;
|
||||
assert_eq!(5, crate::drop::load_drop_counter());
|
||||
|
||||
let tree = db.keyspace("default", Default::default)?;
|
||||
assert_eq!(6, crate::drop::load_drop_counter());
|
||||
|
||||
drop(tree);
|
||||
drop(db);
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
}
|
||||
|
||||
{
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
let db = Database::builder(&folder).open()?;
|
||||
assert_eq!(5, crate::drop::load_drop_counter());
|
||||
|
||||
let _tree = db.keyspace("default", Default::default)?;
|
||||
assert_eq!(6, crate::drop::load_drop_counter());
|
||||
|
||||
let _tree2 = db.keyspace("different", Default::default)?;
|
||||
assert_eq!(7, crate::drop::load_drop_counter());
|
||||
}
|
||||
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
#[test]
|
||||
#[ignore = "restore"]
|
||||
fn whitebox_db_drop_2() -> crate::Result<()> {
|
||||
use crate::{Database, KeyspaceCreateOptions};
|
||||
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
{
|
||||
let db = Database::builder(&folder).open()?;
|
||||
|
||||
let tree = db.keyspace("tree", KeyspaceCreateOptions::default)?;
|
||||
let tree2 = db.keyspace("tree1", KeyspaceCreateOptions::default)?;
|
||||
|
||||
tree.insert("a", "a")?;
|
||||
tree2.insert("b", "b")?;
|
||||
|
||||
tree.rotate_memtable_and_wait()?;
|
||||
}
|
||||
|
||||
assert_eq!(0, crate::drop::load_drop_counter());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_exotic_keyspace_names() -> crate::Result<()> {
|
||||
let folder = tempfile::tempdir()?;
|
||||
let db = Database::builder(&folder).open()?;
|
||||
|
||||
for name in ["hello$world", "hello#world", "hello.world", "hello_world"] {
|
||||
let tree = db.keyspace(name, KeyspaceCreateOptions::default)?;
|
||||
tree.insert("a", "a")?;
|
||||
assert_eq!(1, tree.len()?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn recover_sealed_smoke_test() -> crate::Result<()> {
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
for i in 0_u128..3 {
|
||||
let db = Database::create_or_recover(Database::builder(folder.path()).into_config())?;
|
||||
|
||||
let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
|
||||
assert_eq!(i, tree.len()?.try_into().unwrap());
|
||||
|
||||
tree.insert(i.to_be_bytes(), i.to_be_bytes())?;
|
||||
assert_eq!(i + 1, tree.len()?.try_into().unwrap());
|
||||
|
||||
tree.rotate_memtable_and_wait()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn recover_sealed_order() -> crate::Result<()> {
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
{
|
||||
let db = Database::builder(folder.path())
|
||||
.worker_threads_unchecked(0)
|
||||
.open()?;
|
||||
|
||||
let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
|
||||
tree.insert("a", "a")?;
|
||||
tree.rotate_memtable()?;
|
||||
|
||||
tree.insert("a", "b")?;
|
||||
tree.rotate_memtable()?;
|
||||
|
||||
tree.insert("a", "c")?;
|
||||
tree.rotate_memtable()?;
|
||||
}
|
||||
|
||||
{
|
||||
let db = Database::create_or_recover(Database::builder(folder.path()).into_config())?;
|
||||
|
||||
let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
|
||||
assert_eq!(b"c", &*tree.get("a")?.unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn recover_sealed_blob() -> crate::Result<()> {
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
for i in 0_u128..3 {
|
||||
let db = Database::create_or_recover(Database::builder(folder.path()).into_config())?;
|
||||
|
||||
let tree = db.keyspace("default", || {
|
||||
KeyspaceCreateOptions::default()
|
||||
.max_memtable_size(1_000)
|
||||
.with_kv_separation(Some(KvSeparationOptions::default()))
|
||||
})?;
|
||||
|
||||
assert_eq!(i, tree.len()?.try_into().unwrap());
|
||||
|
||||
tree.insert(i.to_be_bytes(), i.to_be_bytes().repeat(1_024))?;
|
||||
assert_eq!(i + 1, tree.len()?.try_into().unwrap());
|
||||
|
||||
tree.rotate_memtable_and_wait()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn recover_sealed_pair_1() -> crate::Result<()> {
|
||||
let folder = tempfile::tempdir()?;
|
||||
|
||||
for i in 0_u128..3 {
|
||||
let db = Database::create_or_recover(Database::builder(folder.path()).into_config())?;
|
||||
|
||||
let tree = db.keyspace("default", || {
|
||||
KeyspaceCreateOptions::default().max_memtable_size(1_000)
|
||||
})?;
|
||||
let tree2 = db.keyspace("default2", || {
|
||||
KeyspaceCreateOptions::default()
|
||||
.max_memtable_size(1_000)
|
||||
.with_kv_separation(Some(KvSeparationOptions::default()))
|
||||
})?;
|
||||
|
||||
assert_eq!(i, tree.len()?.try_into().unwrap());
|
||||
assert_eq!(i, tree2.len()?.try_into().unwrap());
|
||||
|
||||
let mut batch = db.batch();
|
||||
batch.insert(&tree, i.to_be_bytes(), i.to_be_bytes());
|
||||
batch.insert(&tree2, i.to_be_bytes(), i.to_be_bytes().repeat(1_024));
|
||||
batch.commit()?;
|
||||
|
||||
assert_eq!(i + 1, tree.len()?.try_into().unwrap());
|
||||
assert_eq!(i + 1, tree2.len()?.try_into().unwrap());
|
||||
|
||||
tree.rotate_memtable_and_wait()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
use std::sync::{atomic::AtomicUsize, OnceLock};
|
||||
|
||||
static DROP_COUNTER: OnceLock<AtomicUsize> = OnceLock::new();
|
||||
|
||||
pub fn increment_drop_counter() {
|
||||
get_drop_counter().fetch_add(1, Relaxed);
|
||||
}
|
||||
|
||||
pub fn decrement_drop_counter() {
|
||||
get_drop_counter().fetch_sub(1, Relaxed);
|
||||
}
|
||||
|
||||
pub fn get_drop_counter<'a>() -> &'a AtomicUsize {
|
||||
DROP_COUNTER.get_or_init(AtomicUsize::default)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load_drop_counter() -> usize {
|
||||
get_drop_counter().load(Relaxed)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{
|
||||
journal::error::RecoveryError as JournalRecoveryError, version::FormatVersion, CompressionType,
|
||||
};
|
||||
|
||||
/// Errors that may occur in the storage engine
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Error inside LSM-tree
|
||||
Storage(lsm_tree::Error),
|
||||
|
||||
/// I/O error
|
||||
Io(std::io::Error),
|
||||
|
||||
/// Error during journal recovery
|
||||
JournalRecovery(JournalRecoveryError),
|
||||
|
||||
/// Invalid or unparsable data format version
|
||||
InvalidVersion(Option<FormatVersion>),
|
||||
|
||||
/// Decompression failed
|
||||
Decompress(CompressionType),
|
||||
|
||||
/// Invalid journal trailer detected
|
||||
InvalidTrailer,
|
||||
|
||||
/// Invalid tag detected during decoding
|
||||
InvalidTag((&'static str, u8)),
|
||||
|
||||
/// A previous flush / commit operation failed, indicating a hardware-related failure
|
||||
///
|
||||
/// Future writes will not be accepted as consistency cannot be guaranteed.
|
||||
///
|
||||
/// **At this point, it's best to let the application crash and try to recover.**
|
||||
///
|
||||
/// More info: <https://www.usenix.org/system/files/atc20-rebello.pdf>
|
||||
Poisoned,
|
||||
|
||||
/// Keyspace is deleted
|
||||
KeyspaceDeleted,
|
||||
|
||||
/// Database is locked.
|
||||
Locked,
|
||||
|
||||
/// Database is unrecoverable, see logs for details
|
||||
Unrecoverable,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "FjallError: {self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(inner: std::io::Error) -> Self {
|
||||
Self::Io(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsm_tree::Error> for Error {
|
||||
fn from(inner: lsm_tree::Error) -> Self {
|
||||
Self::Storage(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Storage(inner) => Some(inner),
|
||||
Self::Io(inner) => Some(inner),
|
||||
Self::JournalRecovery(inner) => Some(inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result helper type
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub const MAGIC_BYTES: &[u8] = &[b'F', b'J', b'L', 3];
|
||||
|
||||
pub const KEYSPACES_FOLDER: &str = "keyspaces";
|
||||
|
||||
pub const LOCK_FILE: &str = "lock";
|
||||
pub const VERSION_MARKER: &str = "version";
|
||||
|
||||
pub const LSM_CURRENT_VERSION_MARKER: &str = "current";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn fsync_directory<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let file = std::fs::File::open(path).inspect_err(|e| {
|
||||
log::error!("Failed to open directory at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
debug_assert!(file.metadata()?.is_dir());
|
||||
|
||||
file.sync_all().inspect_err(|e| {
|
||||
log::error!("Failed to fsync directory at {}: {e:?}", path.display());
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn fsync_directory<P: AsRef<Path>>(_path: P) -> std::io::Result<()> {
|
||||
// Cannot fsync directory on Windows
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::flush::Task;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct FlushManager {
|
||||
sender: flume::Sender<Arc<Task>>,
|
||||
receiver: flume::Receiver<Arc<Task>>,
|
||||
}
|
||||
|
||||
impl FlushManager {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = flume::bounded(1_000);
|
||||
|
||||
Self {
|
||||
sender: tx,
|
||||
receiver: rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.receiver.len()
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
let _ = self.receiver.drain().count();
|
||||
}
|
||||
|
||||
pub fn wait_for_empty(&self) {
|
||||
while !self.receiver.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue(&self, task: Arc<Task>) {
|
||||
self.sender.send(task).ok();
|
||||
}
|
||||
|
||||
pub fn dequeue(&self) -> Option<Arc<Task>> {
|
||||
self.receiver.try_recv().ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
pub mod manager;
|
||||
pub mod task;
|
||||
pub mod worker;
|
||||
|
||||
pub use task::Task;
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::Keyspace;
|
||||
|
||||
pub struct Task {
|
||||
pub(crate) keyspace: Keyspace,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Task {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "FlushTask({})", self.keyspace.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{
|
||||
flush::Task, snapshot_tracker::SnapshotTracker, stats::Stats,
|
||||
write_buffer_manager::WriteBufferManager,
|
||||
};
|
||||
use lsm_tree::AbstractTree;
|
||||
|
||||
/// Runs flush logic.
|
||||
pub fn run(
|
||||
task: &Task,
|
||||
write_buffer_manager: &WriteBufferManager,
|
||||
snapshot_tracker: &SnapshotTracker,
|
||||
_stats: &Stats,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Flushing keyspace {:?}", task.keyspace.name);
|
||||
|
||||
let gc_watermark = snapshot_tracker.get_seqno_safe_to_gc();
|
||||
|
||||
let flush_lock = task.keyspace.tree.get_flush_lock();
|
||||
|
||||
match task
|
||||
.keyspace
|
||||
.tree
|
||||
.flush(&flush_lock, gc_watermark)
|
||||
.inspect_err(|e| {
|
||||
log::error!("Flush error: {e:?}");
|
||||
})? {
|
||||
Some(flushed_bytes) => {
|
||||
write_buffer_manager.free(flushed_bytes);
|
||||
|
||||
log::debug!("Flush completed");
|
||||
}
|
||||
None => {
|
||||
log::trace!("Flush did not return a table");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use lsm_tree::{Guard as _Guard, UserKey, UserValue};
|
||||
|
||||
/// Guard to access key-value pairs
|
||||
pub struct Guard(pub(crate) lsm_tree::IterGuardImpl);
|
||||
|
||||
impl Guard {
|
||||
// TODO: is_ok?
|
||||
|
||||
/// Accesses the key-value pair if the predicate returns `true`.
|
||||
///
|
||||
/// The predicate receives the key - if returning `false`, the value
|
||||
/// may not be loaded if the tree is key-value separated.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// tree.insert("abc", "my_value")?;
|
||||
///
|
||||
/// let (k,v) = tree.prefix("a")
|
||||
/// .next()
|
||||
/// .unwrap()
|
||||
/// .into_inner_if(|key| key.starts_with(b"a"))?;
|
||||
///
|
||||
/// assert_eq!(b"abc", &*k);
|
||||
/// assert_eq!(Some(b"my_value".as_slice()), v.as_deref());
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
pub fn into_inner_if(
|
||||
self,
|
||||
pred: impl Fn(&crate::UserKey) -> bool,
|
||||
) -> crate::Result<(UserKey, Option<UserValue>)> {
|
||||
self.0.into_inner_if(pred).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the key-value tuple.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// tree.insert("a", "my_value")?;
|
||||
///
|
||||
/// let (k,v) = tree.prefix("a")
|
||||
/// .next()
|
||||
/// .unwrap()
|
||||
/// .into_inner()?;
|
||||
///
|
||||
/// assert_eq!(b"a", &*k);
|
||||
/// assert_eq!(b"my_value", &*v);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
pub fn into_inner(self) -> crate::Result<crate::KvPair> {
|
||||
self.0.into_inner().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the key.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// tree.insert("a", "my_value")?;
|
||||
///
|
||||
/// let item = tree.prefix("a").next().unwrap().key()?;
|
||||
/// assert_eq!(b"a", &*item);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
pub fn key(self) -> crate::Result<crate::UserKey> {
|
||||
self.0.key().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the value size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// tree.insert("a", "my_value")?;
|
||||
///
|
||||
/// let item = tree.prefix("a").next().unwrap().size()?;
|
||||
/// assert_eq!(8, item);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
pub fn size(self) -> crate::Result<u32> {
|
||||
self.0.size().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use fjall::{Database, KeyspaceCreateOptions};
|
||||
/// #
|
||||
/// # let folder = tempfile::tempdir()?;
|
||||
/// # let db = Database::builder(folder).open()?;
|
||||
/// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?;
|
||||
/// tree.insert("a", "my_value")?;
|
||||
///
|
||||
/// let item = tree.prefix("a").next().unwrap().value()?;
|
||||
/// assert_eq!(b"my_value", &*item);
|
||||
/// #
|
||||
/// # Ok::<(), fjall::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if an IO error occurs.
|
||||
pub fn value(self) -> crate::Result<crate::UserValue> {
|
||||
self.0.value().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{worker_pool::WorkerMessage, Keyspace};
|
||||
use lsm_tree::{AnyIngestion, UserKey, UserValue};
|
||||
|
||||
pub struct Ingestion<'a> {
|
||||
keyspace: &'a Keyspace,
|
||||
inner: AnyIngestion<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Ingestion<'a> {
|
||||
pub fn new(keyspace: &'a Keyspace) -> crate::Result<Self> {
|
||||
let inner = keyspace.tree.ingestion()?;
|
||||
Ok(Self { keyspace, inner })
|
||||
}
|
||||
|
||||
pub fn write<K: Into<UserKey>, V: Into<UserValue>>(
|
||||
&mut self,
|
||||
key: K,
|
||||
value: V,
|
||||
) -> crate::Result<()> {
|
||||
self.inner.write(key, value).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn write_prevalidated<K: Into<UserKey>, V: Into<UserValue>>(
|
||||
&mut self,
|
||||
key: K,
|
||||
value: V,
|
||||
) -> crate::Result<()> {
|
||||
self.inner
|
||||
.write_prevalidated(key, value)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn write_tombstone<K: Into<UserKey>>(&mut self, key: K) -> crate::Result<()> {
|
||||
self.inner.write_tombstone(key).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn write_weak_tombstone<K: Into<UserKey>>(&mut self, key: K) -> crate::Result<()> {
|
||||
self.inner.write_weak_tombstone(key).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn write_prevalidated_weak_tombstone<K: Into<UserKey>>(
|
||||
&mut self,
|
||||
key: K,
|
||||
) -> crate::Result<()> {
|
||||
self.inner
|
||||
.write_prevalidated_weak_tombstone(key)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn finish(self) -> crate::Result<()> {
|
||||
// NOTE: We hold to avoid a race condition with concurrent writes:
|
||||
//
|
||||
// write ingest
|
||||
// lock journal
|
||||
// |
|
||||
// next seqno=1
|
||||
// |
|
||||
// --------------finish
|
||||
// flush
|
||||
// seqno=2
|
||||
// register
|
||||
// |
|
||||
// -----------------
|
||||
// |
|
||||
// insert seqno=1
|
||||
let _journal_lock = self.keyspace.supervisor.journal.get_writer();
|
||||
|
||||
self.finish_inner()
|
||||
}
|
||||
|
||||
/// Finishes the ingestion without taking the global journal writer lock.
|
||||
///
|
||||
/// The caller must ensure that no journaled writes can run concurrently in
|
||||
/// the same database. Exclusive ingestions into independent keyspaces may
|
||||
/// still finish concurrently.
|
||||
pub fn finish_exclusive(self) -> crate::Result<()> {
|
||||
self.finish_inner()
|
||||
}
|
||||
|
||||
fn finish_inner(self) -> crate::Result<()> {
|
||||
self.inner
|
||||
.finish()
|
||||
.inspect(|()| {
|
||||
self.keyspace
|
||||
.worker_messager
|
||||
.try_send(WorkerMessage::Compact(self.keyspace.clone()))
|
||||
.ok();
|
||||
|
||||
self.keyspace.supervisor.snapshot_tracker.gc();
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{snapshot_nonce::SnapshotNonce, Guard};
|
||||
|
||||
type InnerIter = Box<dyn DoubleEndedIterator<Item = lsm_tree::IterGuardImpl> + Send + 'static>;
|
||||
|
||||
/// A wrapper around iterators that keep a snapshot alive
|
||||
//
|
||||
// We need to hold the snapshot nonce so the GC watermark does not
|
||||
// move past this snapshot nonce, removing data that may still be read.
|
||||
//
|
||||
// Additionally, this struct also maps lsm-tree's Guards to "our" Guards.
|
||||
pub struct Iter {
|
||||
iter: InnerIter,
|
||||
|
||||
#[expect(unused)]
|
||||
nonce: SnapshotNonce,
|
||||
}
|
||||
|
||||
impl Iter {
|
||||
pub(crate) fn new(nonce: SnapshotNonce, iter: InnerIter) -> Self {
|
||||
Self { iter, nonce }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Iter {
|
||||
type Item = crate::Guard;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(Guard)
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for Iter {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next_back().map(Guard)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use super::reader::JournalReader;
|
||||
use crate::{journal::entry::Entry, keyspace::InternalKeyspaceId, JournalRecoveryError};
|
||||
use lsm_tree::{SeqNo, UserKey, UserValue, ValueType};
|
||||
use std::{fs::OpenOptions, hash::Hasher};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReadBatchItem {
|
||||
pub keyspace_id: InternalKeyspaceId,
|
||||
pub key: UserKey,
|
||||
pub value: UserValue,
|
||||
pub value_type: ValueType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Batch {
|
||||
pub(crate) seqno: SeqNo,
|
||||
pub(crate) items: Vec<ReadBatchItem>,
|
||||
pub(crate) cleared_keyspaces: Vec<InternalKeyspaceId>,
|
||||
}
|
||||
|
||||
#[expect(clippy::module_name_repetitions)]
|
||||
pub struct JournalBatchReader {
|
||||
reader: JournalReader,
|
||||
items: Vec<ReadBatchItem>,
|
||||
cleared_keyspaces: Vec<InternalKeyspaceId>,
|
||||
is_in_batch: bool,
|
||||
batch_counter: u32,
|
||||
batch_seqno: SeqNo,
|
||||
last_valid_pos: u64,
|
||||
checksum_builder: xxhash_rust::xxh3::Xxh3,
|
||||
}
|
||||
|
||||
impl JournalBatchReader {
|
||||
pub fn new(reader: JournalReader) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
items: Vec::with_capacity(10),
|
||||
cleared_keyspaces: Vec::new(),
|
||||
checksum_builder: xxhash_rust::xxh3::Xxh3::new(),
|
||||
is_in_batch: false,
|
||||
batch_seqno: 0,
|
||||
last_valid_pos: 0,
|
||||
batch_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: reallocate space
|
||||
fn truncate_to(&self, last_valid_pos: u64) -> crate::Result<()> {
|
||||
log::trace!("Truncating journal to {last_valid_pos}");
|
||||
|
||||
// TODO: on windows, reading file probably needs to be closed first...?
|
||||
|
||||
let file = OpenOptions::new().write(true).open(&self.reader.path)?;
|
||||
file.set_len(last_valid_pos)?;
|
||||
file.sync_all()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_close(&self) -> crate::Result<()> {
|
||||
if self.is_in_batch {
|
||||
log::debug!("Invalid batch: missing terminator, but last batch, so probably incomplete, discarding to keep atomicity");
|
||||
|
||||
// Discard batch
|
||||
self.truncate_to(self.last_valid_pos)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for JournalBatchReader {
|
||||
type Item = crate::Result<Batch>;
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use crate::Error::JournalRecovery;
|
||||
|
||||
loop {
|
||||
let Some(item) = self.reader.next() else {
|
||||
fail_iter!(self.on_close());
|
||||
return None;
|
||||
};
|
||||
let item = fail_iter!(item);
|
||||
|
||||
let journal_file_pos = self.reader.last_valid_pos;
|
||||
|
||||
match item {
|
||||
Entry::Start { item_count, seqno } => {
|
||||
if self.is_in_batch {
|
||||
log::debug!("Invalid batch: found batch start inside batch");
|
||||
|
||||
// Discard batch
|
||||
fail_iter!(self.truncate_to(self.last_valid_pos));
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
self.is_in_batch = true;
|
||||
self.batch_counter = item_count;
|
||||
self.batch_seqno = seqno;
|
||||
}
|
||||
Entry::End(expected_checksum) => {
|
||||
if self.batch_counter > 0 {
|
||||
log::error!("Invalid batch: insufficient length");
|
||||
return Some(Err(JournalRecovery(
|
||||
JournalRecoveryError::InsufficientLength,
|
||||
)));
|
||||
}
|
||||
|
||||
if !self.is_in_batch {
|
||||
log::error!("Invalid batch: found end marker without start marker");
|
||||
|
||||
// Discard batch
|
||||
fail_iter!(self.truncate_to(self.last_valid_pos));
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
let got_checksum = self.checksum_builder.finish();
|
||||
self.checksum_builder = xxhash_rust::xxh3::Xxh3::new();
|
||||
|
||||
if got_checksum != expected_checksum {
|
||||
log::error!("Invalid batch: checksum check failed, expected: {expected_checksum}, got: {got_checksum}");
|
||||
return Some(Err(JournalRecovery(JournalRecoveryError::ChecksumMismatch)));
|
||||
}
|
||||
|
||||
// Reset all variables
|
||||
self.is_in_batch = false;
|
||||
self.batch_counter = 0;
|
||||
|
||||
self.last_valid_pos = journal_file_pos;
|
||||
|
||||
let items = std::mem::take(&mut self.items);
|
||||
let cleared_keyspaces = std::mem::take(&mut self.cleared_keyspaces);
|
||||
return Some(Ok(Batch {
|
||||
seqno: self.batch_seqno,
|
||||
items,
|
||||
cleared_keyspaces,
|
||||
}));
|
||||
}
|
||||
Entry::Item {
|
||||
keyspace_id,
|
||||
key,
|
||||
value,
|
||||
value_type,
|
||||
compression,
|
||||
} => {
|
||||
let item = Entry::Item {
|
||||
keyspace_id,
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
value_type,
|
||||
compression,
|
||||
};
|
||||
let mut bytes = Vec::with_capacity(100);
|
||||
fail_iter!(item.encode_into(&mut bytes));
|
||||
|
||||
self.checksum_builder.update(&bytes);
|
||||
|
||||
if !self.is_in_batch {
|
||||
log::debug!("Invalid batch: found end marker without start marker");
|
||||
|
||||
// Discard batch
|
||||
fail_iter!(self.truncate_to(self.last_valid_pos));
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.batch_counter == 0 {
|
||||
log::error!("Invalid batch: Expected end marker (too many items in batch)");
|
||||
return Some(Err(JournalRecovery(JournalRecoveryError::TooManyItems)));
|
||||
}
|
||||
|
||||
self.batch_counter -= 1;
|
||||
|
||||
self.items.push(ReadBatchItem {
|
||||
keyspace_id,
|
||||
key,
|
||||
value,
|
||||
value_type,
|
||||
});
|
||||
}
|
||||
Entry::Clear { keyspace_id } => {
|
||||
let entry = Entry::Clear { keyspace_id };
|
||||
let mut bytes = Vec::with_capacity(16);
|
||||
fail_iter!(entry.encode_into(&mut bytes));
|
||||
|
||||
self.checksum_builder.update(&bytes);
|
||||
|
||||
if !self.is_in_batch {
|
||||
log::debug!("Invalid batch: found clear marker without start marker");
|
||||
|
||||
// Discard batch
|
||||
fail_iter!(self.truncate_to(self.last_valid_pos));
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.batch_counter == 0 {
|
||||
log::error!("Invalid batch: Expected end marker (too many items in batch)");
|
||||
return Some(Err(JournalRecovery(JournalRecoveryError::TooManyItems)));
|
||||
}
|
||||
|
||||
self.batch_counter -= 1;
|
||||
|
||||
self.cleared_keyspaces.push(keyspace_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::{file::MAGIC_BYTES, keyspace::InternalKeyspaceId, Slice};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use lsm_tree::{
|
||||
coding::{Decode, Encode},
|
||||
CompressionType, SeqNo, UserKey, UserValue, ValueType,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
/// Journal entry. Every batch is composed as a Start, followed by N items, followed by an End.
|
||||
///
|
||||
/// - The start entry contains the numbers of items. If the numbers of items following doesn't match, the batch is broken.
|
||||
///
|
||||
/// - The end entry contains a checksum value. If the checksum of the items doesn't match that, the batch is broken.
|
||||
///
|
||||
/// - The end entry terminates each batch with the magic string: [`TRAILER_MAGIC`].
|
||||
///
|
||||
/// - If a start entry is detected, while inside a batch, the batch is broken.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Entry {
|
||||
Start {
|
||||
item_count: u32,
|
||||
seqno: SeqNo,
|
||||
},
|
||||
Item {
|
||||
keyspace_id: InternalKeyspaceId,
|
||||
key: UserKey,
|
||||
value: UserValue,
|
||||
value_type: ValueType,
|
||||
compression: CompressionType,
|
||||
},
|
||||
End(u64),
|
||||
Clear {
|
||||
keyspace_id: InternalKeyspaceId,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn serialize_marker_item<W: Write>(
|
||||
writer: &mut W,
|
||||
keyspace_id: InternalKeyspaceId,
|
||||
key: &[u8],
|
||||
value: &[u8],
|
||||
value_type: ValueType,
|
||||
compression: CompressionType,
|
||||
) -> Result<(), lsm_tree::Error> {
|
||||
writer.write_u8(Tag::Item.into())?;
|
||||
|
||||
writer.write_u8(u8::from(value_type))?;
|
||||
|
||||
compression.encode_into(writer)?;
|
||||
|
||||
let compressed_value = match compression {
|
||||
CompressionType::None => std::borrow::Cow::Borrowed(value),
|
||||
|
||||
#[cfg(feature = "lz4")]
|
||||
CompressionType::Lz4 => {
|
||||
let compressed = lz4_flex::compress(value);
|
||||
std::borrow::Cow::Owned(compressed)
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Truncation is okay and actually needed
|
||||
writer.write_u64::<LittleEndian>(keyspace_id)?;
|
||||
|
||||
// NOTE: Truncation is okay and actually needed
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
writer.write_u16::<LittleEndian>(key.len() as u16)?;
|
||||
|
||||
// NOTE: Truncation is okay and actually needed
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
writer.write_u32::<LittleEndian>(value.len() as u32)?;
|
||||
|
||||
// NOTE: Truncation is okay and actually needed
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
writer.write_u32::<LittleEndian>(compressed_value.len() as u32)?;
|
||||
|
||||
writer.write_all(key)?;
|
||||
|
||||
writer.write_all(&compressed_value)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub enum Tag {
|
||||
Start = 1,
|
||||
Item = 2,
|
||||
End = 3,
|
||||
Clear = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Tag {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
use Tag::{Clear, End, Item, Start};
|
||||
|
||||
match value {
|
||||
1 => Ok(Start),
|
||||
2 => Ok(Item),
|
||||
3 => Ok(End),
|
||||
4 => Ok(Clear),
|
||||
_ => Err(crate::Error::InvalidTag(("JournalMarkerTag", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tag> for u8 {
|
||||
fn from(val: Tag) -> Self {
|
||||
val as Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
#[cfg(test)]
|
||||
pub fn encode_into_vec(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
self.encode_into(&mut buf).expect("should encode");
|
||||
buf
|
||||
}
|
||||
|
||||
pub(crate) fn encode_into<W: Write>(&self, writer: &mut W) -> Result<(), crate::Error> {
|
||||
use Entry::{Clear, End, Item, Start};
|
||||
|
||||
match self {
|
||||
Start { item_count, seqno } => {
|
||||
writer.write_u8(Tag::Start.into())?;
|
||||
writer.write_u32::<LittleEndian>(*item_count)?;
|
||||
writer.write_u64::<LittleEndian>(*seqno)?;
|
||||
}
|
||||
Item {
|
||||
keyspace_id,
|
||||
key,
|
||||
value,
|
||||
value_type,
|
||||
compression,
|
||||
} => {
|
||||
serialize_marker_item(writer, *keyspace_id, key, value, *value_type, *compression)?;
|
||||
}
|
||||
End(val) => {
|
||||
writer.write_u8(Tag::End.into())?;
|
||||
writer.write_u64::<LittleEndian>(*val)?;
|
||||
|
||||
// NOTE: Write some fixed trailer bytes so we know the end marker is fully written
|
||||
// Otherwise we couldn't know if the checksum value is maybe mangled
|
||||
// (only partially written, with the rest being padding zeroes)
|
||||
writer.write_all(MAGIC_BYTES)?;
|
||||
}
|
||||
Clear { keyspace_id } => {
|
||||
writer.write_u8(Tag::Clear.into())?;
|
||||
writer.write_u64::<LittleEndian>(*keyspace_id)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn decode_from<R: Read>(reader: &mut R) -> Result<Self, crate::Error> {
|
||||
match reader.read_u8()?.try_into()? {
|
||||
Tag::Start => {
|
||||
let item_count = reader.read_u32::<LittleEndian>()?;
|
||||
let seqno = reader.read_u64::<LittleEndian>()?;
|
||||
Ok(Self::Start { item_count, seqno })
|
||||
}
|
||||
Tag::Item => {
|
||||
let value_type = reader.read_u8()?;
|
||||
let value_type = value_type
|
||||
.try_into()
|
||||
.map_err(|()| lsm_tree::Error::InvalidTag(("ValueType", value_type)))?;
|
||||
|
||||
let compression = CompressionType::decode_from(reader)?;
|
||||
|
||||
// Read keyspace ID
|
||||
let keyspace_id = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
// Read key len
|
||||
let key_len = reader.read_u16::<LittleEndian>()?;
|
||||
|
||||
// Read real value size
|
||||
let value_len = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
// Read on-disk value size
|
||||
let on_disk_value_len = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let key = Slice::from_reader(reader, usize::from(key_len))?;
|
||||
|
||||
let value = match compression {
|
||||
CompressionType::None => {
|
||||
debug_assert_eq!(value_len, on_disk_value_len);
|
||||
Slice::from_reader(reader, on_disk_value_len as usize)?
|
||||
}
|
||||
|
||||
#[cfg(feature = "lz4")]
|
||||
CompressionType::Lz4 => {
|
||||
let compressed_value =
|
||||
Slice::from_reader(reader, on_disk_value_len as usize)?;
|
||||
|
||||
#[warn(unsafe_code)]
|
||||
let mut value = unsafe { Slice::builder_unzeroed(value_len as usize) };
|
||||
|
||||
let size = lz4_flex::decompress_into(&compressed_value, &mut value)
|
||||
.map_err(|e| {
|
||||
log::error!("LZ4 decompression failed: {e}");
|
||||
crate::Error::Decompress(CompressionType::Lz4)
|
||||
})?;
|
||||
|
||||
if size != value.len() {
|
||||
log::error!("Decompressed size does not match expected value size");
|
||||
return Err(crate::Error::Decompress(CompressionType::Lz4));
|
||||
}
|
||||
|
||||
Slice::from(value.freeze())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self::Item {
|
||||
keyspace_id,
|
||||
key,
|
||||
value,
|
||||
value_type,
|
||||
compression,
|
||||
})
|
||||
}
|
||||
Tag::End => {
|
||||
let checksum = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
// Check trailer
|
||||
let mut magic = [0u8; MAGIC_BYTES.len()];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
if magic != MAGIC_BYTES {
|
||||
return Err(crate::Error::InvalidTrailer);
|
||||
}
|
||||
|
||||
Ok(Self::End(checksum))
|
||||
}
|
||||
Tag::Clear => {
|
||||
let keyspace_id = reader.read_u64::<LittleEndian>()?;
|
||||
Ok(Self::Clear { keyspace_id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_and_deserialize_success() -> crate::Result<()> {
|
||||
let item = Entry::Item {
|
||||
keyspace_id: 0,
|
||||
key: vec![1, 2, 3].into(),
|
||||
value: vec![].into(),
|
||||
value_type: ValueType::Value,
|
||||
compression: CompressionType::None,
|
||||
};
|
||||
|
||||
let serialized_data = item.encode_into_vec();
|
||||
let mut reader = &serialized_data[..];
|
||||
let deserialized_item = Entry::decode_from(&mut reader)?;
|
||||
|
||||
assert_eq!(item, deserialized_item);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_deserialize() {
|
||||
let invalid_data = [Tag::Start as u8; 1]; // Should be followed by a u32
|
||||
|
||||
// Try to deserialize with invalid data
|
||||
let mut reader = &invalid_data[..];
|
||||
let result = Entry::decode_from(&mut reader);
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("should error"),
|
||||
Err(error) => match error {
|
||||
crate::Error::Io(e) => match e.kind() {
|
||||
std::io::ErrorKind::UnexpectedEof => {}
|
||||
_ => panic!("should throw UnexpectedEof"),
|
||||
},
|
||||
_ => panic!("should throw UnexpectedEof"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_tag() {
|
||||
let invalid_data = [5u8; 1]; // Invalid tag
|
||||
|
||||
// Try to deserialize with invalid data
|
||||
let mut reader = &invalid_data[..];
|
||||
let result = Entry::decode_from(&mut reader);
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("should error"),
|
||||
Err(error) => match error {
|
||||
crate::Error::InvalidTag(("JournalMarkerTag", 5)) => {}
|
||||
_ => panic!("should throw InvalidTag"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
/// Errors that can occur during journal recovery
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[expect(clippy::module_name_repetitions)]
|
||||
pub enum RecoveryError {
|
||||
/// Batch had less items than expected, so it's incomplete
|
||||
InsufficientLength,
|
||||
|
||||
/* /// Batch was not terminated, so it's possibly incomplete
|
||||
MissingTerminator, */
|
||||
/// Too many items in batch
|
||||
TooManyItems,
|
||||
|
||||
/// The checksum value does not match the expected value
|
||||
ChecksumMismatch,
|
||||
|
||||
/// An unparseable journal file name was encountered
|
||||
InvalidFileName,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RecoveryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "RecoveryError({self:?})")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecoveryError {}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use super::writer::Writer;
|
||||
use crate::Keyspace;
|
||||
use lsm_tree::{AbstractTree, SeqNo};
|
||||
use std::{path::PathBuf, sync::MutexGuard};
|
||||
|
||||
/// Stores the highest seqno of a keyspace found in a journal.
|
||||
#[derive(Clone)]
|
||||
pub struct EvictionWatermark {
|
||||
pub(crate) keyspace: Keyspace,
|
||||
pub(crate) lsn: SeqNo,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EvictionWatermark {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.keyspace.name, self.lsn)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Item {
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) size_in_bytes: u64,
|
||||
pub(crate) watermarks: Vec<EvictionWatermark>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Item {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"JournalManagerItem {:?} => {:#?}",
|
||||
self.path, self.watermarks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`JournalManager`] keeps track of sealed journals that are being flushed.
|
||||
///
|
||||
/// Each journal may contain items of different keyspaces.
|
||||
#[expect(clippy::module_name_repetitions)]
|
||||
#[derive(Debug)]
|
||||
pub struct JournalManager {
|
||||
items: Vec<Item>,
|
||||
disk_space_in_bytes: u64,
|
||||
}
|
||||
|
||||
impl Drop for JournalManager {
|
||||
fn drop(&mut self) {
|
||||
log::trace!("Dropping journal manager");
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::decrement_drop_counter();
|
||||
}
|
||||
}
|
||||
|
||||
impl JournalManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::increment_drop_counter();
|
||||
|
||||
Self {
|
||||
items: Vec::with_capacity(10),
|
||||
disk_space_in_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn enqueue(&mut self, item: Item) {
|
||||
self.disk_space_in_bytes = self.disk_space_in_bytes.saturating_add(item.size_in_bytes);
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
/// Returns the number of journals
|
||||
pub(crate) fn journal_count(&self) -> usize {
|
||||
// NOTE: + 1 = active journal
|
||||
self.sealed_journal_count() + 1
|
||||
}
|
||||
|
||||
/// Returns the number of sealed journals
|
||||
pub(crate) fn sealed_journal_count(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
/// Returns the number of bytes used on disk by journals
|
||||
pub(crate) fn disk_space_used(&self) -> u64 {
|
||||
self.disk_space_in_bytes
|
||||
}
|
||||
|
||||
/// Gets keyspaces to be flushed so that the oldest journal can be safely evicted
|
||||
pub(crate) fn get_keyspaces_to_flush_for_oldest_journal_eviction(&self) -> Vec<Keyspace> {
|
||||
let mut items = vec![];
|
||||
|
||||
if let Some(item) = self.items.first() {
|
||||
for item in &item.watermarks {
|
||||
let Some(partition_seqno) = item.keyspace.tree.get_highest_persisted_seqno() else {
|
||||
items.push(item.keyspace.clone());
|
||||
continue;
|
||||
};
|
||||
|
||||
if partition_seqno < item.lsn {
|
||||
items.push(item.keyspace.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
/// Performs maintenance, maybe deleting some old journals
|
||||
pub(crate) fn maintenance(&mut self) -> crate::Result<()> {
|
||||
log::debug!("Running journal maintenance");
|
||||
|
||||
loop {
|
||||
let Some(item) = self.items.first() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// TODO: unit test: check deleted keyspace does not prevent journal eviction
|
||||
for item in &item.watermarks {
|
||||
// Only check keyspace seqno if not deleted
|
||||
if !item
|
||||
.keyspace
|
||||
.is_deleted
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
let Some(keyspace_seqno) = item.keyspace.tree.get_highest_persisted_seqno()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if keyspace_seqno < item.lsn {
|
||||
log::trace!(
|
||||
"Keyspace {:?} not flushed enough to evict journal",
|
||||
item.keyspace.name,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Once the LSN of *every* keyspace's tables [1] is higher than the journal's stored keyspace seqno,
|
||||
// it can be deleted from disk, as we know the entire journal has been flushed to tables [2].
|
||||
//
|
||||
// [1] We cannot use the keyspace's max seqno, because the memtable will get writes, which increase the seqno.
|
||||
// We *need* to check the tables specifically, they are the source of truth for flushed data.
|
||||
//
|
||||
// [2] Checking the seqno is safe because the queues inside the flush manager are FIFO.
|
||||
//
|
||||
// IMPORTANT: On recovery, the journals need to be flushed from oldest to newest.
|
||||
log::trace!("Removing fully flushed journal at {}", item.path.display());
|
||||
|
||||
std::fs::remove_file(&item.path).inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to clean up stale journal file at {}: {e:?}",
|
||||
item.path.display(),
|
||||
);
|
||||
})?;
|
||||
|
||||
self.disk_space_in_bytes = self.disk_space_in_bytes.saturating_sub(item.size_in_bytes);
|
||||
self.items.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rotate_journal(
|
||||
&mut self,
|
||||
journal_writer: &mut MutexGuard<Writer>,
|
||||
watermarks: Vec<EvictionWatermark>,
|
||||
) -> crate::Result<()> {
|
||||
let journal_size = journal_writer.len()?;
|
||||
|
||||
let (sealed_path, _) = journal_writer.rotate()?;
|
||||
|
||||
self.enqueue(Item {
|
||||
path: sealed_path,
|
||||
watermarks,
|
||||
size_in_bytes: journal_size,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
pub mod batch_reader;
|
||||
pub mod entry;
|
||||
pub mod error;
|
||||
pub mod manager;
|
||||
pub mod reader;
|
||||
mod recovery;
|
||||
pub mod writer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use self::writer::PersistMode;
|
||||
use crate::file::fsync_directory;
|
||||
use batch_reader::JournalBatchReader;
|
||||
use lsm_tree::CompressionType;
|
||||
use reader::JournalReader;
|
||||
use recovery::{recover_journals, RecoveryResult};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Mutex, MutexGuard},
|
||||
};
|
||||
use writer::Writer;
|
||||
|
||||
pub struct Journal {
|
||||
writer: Mutex<Writer>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Journal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.path()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|_| String::from("<failed to read path>"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Journal {
|
||||
fn drop(&mut self) {
|
||||
log::trace!("Dropping journal, trying to flush");
|
||||
|
||||
match self.persist(PersistMode::SyncAll) {
|
||||
Ok(()) => {
|
||||
log::trace!("Flushed journal successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Flush error on drop: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::decrement_drop_counter();
|
||||
}
|
||||
}
|
||||
|
||||
impl Journal {
|
||||
pub fn with_compression(self, comp: CompressionType, threshold: usize) -> Self {
|
||||
{
|
||||
let mut writer = self.writer.lock().expect("lock is poisoned");
|
||||
writer.set_compression(comp, threshold);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn from_file<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
writer: Mutex::new(Writer::from_file(path)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_new<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
log::trace!("Creating new journal at {}", path.display());
|
||||
|
||||
let folder = path.parent().expect("parent should exist");
|
||||
|
||||
std::fs::create_dir_all(folder).inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to create journal folder at {}: {e:?}",
|
||||
path.display(),
|
||||
);
|
||||
})?;
|
||||
|
||||
let writer = Writer::create_new(path)?;
|
||||
|
||||
// IMPORTANT: fsync folder on Unix
|
||||
fsync_directory(folder)?;
|
||||
|
||||
#[cfg(feature = "__internal_whitebox")]
|
||||
crate::drop::increment_drop_counter();
|
||||
|
||||
Ok(Self {
|
||||
writer: Mutex::new(writer),
|
||||
})
|
||||
}
|
||||
|
||||
/// Hands out write access to the journal.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the journal writer is poisoned.
|
||||
pub(crate) fn get_writer(&self) -> crate::Result<MutexGuard<'_, Writer>> {
|
||||
self.writer.lock().map_err(|_| crate::Error::Poisoned)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> crate::Result<PathBuf> {
|
||||
Ok(self.get_writer()?.path.clone())
|
||||
}
|
||||
|
||||
pub fn get_reader(&self) -> crate::Result<JournalBatchReader> {
|
||||
let raw_reader = JournalReader::new(self.path()?)?;
|
||||
Ok(JournalBatchReader::new(raw_reader))
|
||||
}
|
||||
|
||||
/// Persists the journal.
|
||||
pub fn persist(&self, mode: PersistMode) -> crate::Result<()> {
|
||||
let mut journal_writer = self.get_writer()?;
|
||||
journal_writer.persist(mode).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn recover<P: AsRef<Path>>(
|
||||
path: P,
|
||||
compression: CompressionType,
|
||||
compression_threshold: usize,
|
||||
) -> crate::Result<RecoveryResult> {
|
||||
recover_journals(path, compression, compression_threshold)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use super::entry::Entry;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, Seek},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Reads and emits through the entries in a journal file, but doesn't
|
||||
/// check the validity of batches
|
||||
///
|
||||
/// Will truncate the file to the last valid position to prevent corrupt
|
||||
/// bytes at the end of the file, which would jeopardize future writes into the file.
|
||||
#[expect(clippy::module_name_repetitions)]
|
||||
pub struct JournalReader {
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) reader: BufReader<File>,
|
||||
pub(crate) last_valid_pos: u64,
|
||||
}
|
||||
|
||||
impl JournalReader {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
|
||||
let file = OpenOptions::new().read(true).write(true).open(&path)?;
|
||||
|
||||
Ok(Self {
|
||||
path: path.as_ref().into(),
|
||||
reader: BufReader::new(file),
|
||||
last_valid_pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn truncate_file(&mut self, pos: u64) -> crate::Result<()> {
|
||||
log::debug!("truncating journal to {pos}");
|
||||
self.reader.get_mut().set_len(pos)?;
|
||||
self.reader.get_mut().sync_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maybe_truncate_file_to_last_valid_pos(&mut self) -> crate::Result<()> {
|
||||
let stream_pos = self.reader.stream_position()?;
|
||||
|
||||
if stream_pos > self.last_valid_pos {
|
||||
self.truncate_file(self.last_valid_pos)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for JournalReader {
|
||||
type Item = crate::Result<Entry>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match Entry::decode_from(&mut self.reader) {
|
||||
Ok(item) => {
|
||||
self.last_valid_pos = fail_iter!(self.reader.stream_position());
|
||||
Some(Ok(item))
|
||||
}
|
||||
Err(e) => {
|
||||
if let crate::Error::Io(e) = e {
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::UnexpectedEof | std::io::ErrorKind::Other => {
|
||||
fail_iter!(self.maybe_truncate_file_to_last_valid_pos());
|
||||
None
|
||||
}
|
||||
_ => Some(Err(crate::Error::Io(e))),
|
||||
}
|
||||
} else {
|
||||
fail_iter!(self.maybe_truncate_file_to_last_valid_pos());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use super::Journal;
|
||||
use lsm_tree::CompressionType;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub type JournalId = u64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecoveryResult {
|
||||
pub(crate) active: Journal,
|
||||
pub(crate) sealed: Vec<(JournalId, PathBuf)>,
|
||||
pub(crate) was_active_created: bool,
|
||||
}
|
||||
|
||||
pub fn recover_journals<P: AsRef<Path>>(
|
||||
path: P,
|
||||
compression: CompressionType,
|
||||
compression_threshold: usize,
|
||||
) -> crate::Result<RecoveryResult> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut max_journal_id: JournalId = 0;
|
||||
let mut journal_fragments = Vec::<(JournalId, PathBuf)>::new();
|
||||
|
||||
log::trace!("Got journal fragments: {journal_fragments:#?}");
|
||||
|
||||
for dirent in std::fs::read_dir(path)? {
|
||||
let dirent = dirent?;
|
||||
let path = dirent.path();
|
||||
let filename = dirent.file_name();
|
||||
|
||||
let Some(filename) = filename.to_str() else {
|
||||
log::error!("Invalid journal file name: {}", filename.display());
|
||||
return Err(crate::Error::JournalRecovery(
|
||||
crate::JournalRecoveryError::InvalidFileName,
|
||||
));
|
||||
};
|
||||
|
||||
if !std::path::Path::new(filename)
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("jnl"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(dirent.file_type()?.is_file());
|
||||
|
||||
let Some(basename) = filename.strip_suffix(".jnl") else {
|
||||
log::error!("Invalid journal file name: {filename}");
|
||||
return Err(crate::Error::JournalRecovery(
|
||||
crate::JournalRecoveryError::InvalidFileName,
|
||||
));
|
||||
};
|
||||
|
||||
let journal_id = basename.parse::<JournalId>().map_err(|_| {
|
||||
log::error!("Invalid journal file name: {filename}");
|
||||
crate::Error::JournalRecovery(crate::JournalRecoveryError::InvalidFileName)
|
||||
})?;
|
||||
|
||||
max_journal_id = max_journal_id.max(journal_id);
|
||||
|
||||
journal_fragments.push((journal_id, path));
|
||||
}
|
||||
|
||||
// NOTE: Sort ascending, so the last item is the active journal
|
||||
journal_fragments.sort_by_key(|(a, _)| *a);
|
||||
|
||||
log::trace!("Recovered {journal_fragments:#?}");
|
||||
|
||||
Ok(match journal_fragments.pop() {
|
||||
Some((_, active)) => RecoveryResult {
|
||||
active: Journal::from_file(active)?
|
||||
.with_compression(compression, compression_threshold),
|
||||
sealed: journal_fragments,
|
||||
was_active_created: false,
|
||||
},
|
||||
None => RecoveryResult {
|
||||
active: {
|
||||
let id: JournalId = max_journal_id + 1;
|
||||
|
||||
Journal::create_new(path.join(id.to_string()))?
|
||||
.with_compression(compression, compression_threshold)
|
||||
},
|
||||
sealed: vec![],
|
||||
was_active_created: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
use super::*;
|
||||
use crate::batch::item::Item as BatchItem;
|
||||
use entry::Entry;
|
||||
use lsm_tree::ValueType;
|
||||
use std::io::Write;
|
||||
use tempfile::tempdir;
|
||||
use test_log::test;
|
||||
|
||||
impl PartialEq<BatchItem> for crate::journal::batch_reader::ReadBatchItem {
|
||||
fn eq(&self, other: &BatchItem) -> bool {
|
||||
self.keyspace_id == other.keyspace.id
|
||||
&& self.key == other.key
|
||||
&& self.value == other.value
|
||||
&& self.value_type == other.value_type
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<crate::journal::batch_reader::ReadBatchItem> for BatchItem {
|
||||
fn eq(&self, other: &crate::journal::batch_reader::ReadBatchItem) -> bool {
|
||||
other.eq(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::redundant_clone)]
|
||||
fn journal_rotation() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
let next_path = dir2.path().join("1.jnl");
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
let mut writer = journal.get_writer()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace.clone(), *b"a", *b"a", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"b", *b"b", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
0,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
}
|
||||
|
||||
assert!(path.try_exists()?);
|
||||
assert!(next_path.try_exists()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::redundant_clone)]
|
||||
fn journal_recovery_active() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace0 = db.keyspace("default", Default::default)?;
|
||||
let keyspace1 = db.keyspace("default1", Default::default)?;
|
||||
let keyspace2 = db.keyspace("default2", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
let next_path = dir2.path().join("1.jnl");
|
||||
let next_next_path = dir2.path().join("2.jnl");
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
let mut writer = journal.get_writer()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace0.clone(), *b"a", *b"a", ValueType::Value),
|
||||
BatchItem::new(keyspace0.clone(), *b"b", *b"b", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
0,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace1.clone(), *b"c", *b"c", ValueType::Value),
|
||||
BatchItem::new(keyspace1.clone(), *b"d", *b"d", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
1,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace2.clone(), *b"c", *b"c", ValueType::Value),
|
||||
BatchItem::new(keyspace2.clone(), *b"d", *b"d", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
1,
|
||||
)?;
|
||||
}
|
||||
|
||||
assert!(path.try_exists()?);
|
||||
assert!(next_path.try_exists()?);
|
||||
assert!(next_next_path.try_exists()?);
|
||||
|
||||
let journal_recovered = Journal::recover(dir2, CompressionType::None, 0)?;
|
||||
assert_eq!(journal_recovered.active.path()?, next_next_path);
|
||||
assert_eq!(journal_recovered.sealed, &[(0, path), (1, next_path)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "lz4")]
|
||||
#[expect(clippy::redundant_clone)]
|
||||
fn journal_recovery_active_lz4() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace0 = db.keyspace("default", Default::default)?;
|
||||
let keyspace1 = db.keyspace("default1", Default::default)?;
|
||||
let keyspace2 = db.keyspace("default2", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
let next_path = dir2.path().join("1.jnl");
|
||||
let next_next_path = dir2.path().join("2.jnl");
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?.with_compression(CompressionType::Lz4, 1);
|
||||
let mut writer = journal.get_writer()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace0.clone(), *b"a", *b"a", ValueType::Value),
|
||||
BatchItem::new(keyspace0.clone(), *b"b", *b"b", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
0,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace1.clone(), *b"c", *b"c", ValueType::Value),
|
||||
BatchItem::new(keyspace1.clone(), *b"d", *b"d", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
1,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace2.clone(), *b"c", *b"c", ValueType::Value),
|
||||
BatchItem::new(keyspace2.clone(), *b"d", *b"d", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
1,
|
||||
)?;
|
||||
}
|
||||
|
||||
assert!(path.try_exists()?);
|
||||
assert!(next_path.try_exists()?);
|
||||
assert!(next_next_path.try_exists()?);
|
||||
|
||||
let journal_recovered = Journal::recover(dir2, CompressionType::None, 0)?;
|
||||
assert_eq!(journal_recovered.active.path()?, next_next_path);
|
||||
assert_eq!(journal_recovered.sealed, &[(0, path), (1, next_path)]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::redundant_clone)]
|
||||
fn journal_recovery_no_active() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
let next_path = dir2.path().join("1.jnl");
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
|
||||
{
|
||||
let mut writer = journal.get_writer()?;
|
||||
|
||||
writer.write_batch(
|
||||
[
|
||||
BatchItem::new(keyspace.clone(), *b"a", *b"a", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"b", *b"b", ValueType::Value),
|
||||
]
|
||||
.iter(),
|
||||
2,
|
||||
0,
|
||||
)?;
|
||||
writer.rotate()?;
|
||||
}
|
||||
|
||||
// NOTE: Delete the new, active journal -> old journal will be
|
||||
// reused as active on next recovery
|
||||
std::fs::remove_file(&next_path)?;
|
||||
}
|
||||
|
||||
assert!(path.try_exists()?);
|
||||
assert!(!next_path.try_exists()?);
|
||||
|
||||
let journal_recovered = Journal::recover(dir2, CompressionType::None, 0)?;
|
||||
assert_eq!(journal_recovered.active.path()?, path);
|
||||
assert_eq!(journal_recovered.sealed, &[]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used, clippy::redundant_clone)]
|
||||
fn journal_truncation_corrupt_bytes() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
|
||||
let values = [
|
||||
BatchItem::new(keyspace.clone(), *b"abc", *b"def", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"yxc", *b"ghj", ValueType::Value),
|
||||
];
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
journal
|
||||
.get_writer()?
|
||||
.write_batch(values.iter(), values.len(), 0)?;
|
||||
}
|
||||
|
||||
{
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
file.write_all(b"09pmu35w3a9mp53bao9upw3ab5up")?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
for _ in 0..5 {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
file.write_all(b"09pmu35w3a9mp53bao9upw3ab5up")?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used, clippy::redundant_clone)]
|
||||
fn journal_truncation_repeating_start_marker() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
|
||||
let values = [
|
||||
BatchItem::new(keyspace.clone(), *b"abc", *b"def", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"yxc", *b"ghj", ValueType::Value),
|
||||
];
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
journal
|
||||
.get_writer()?
|
||||
.write_batch(values.iter(), values.len(), 0)?;
|
||||
}
|
||||
|
||||
{
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::Start {
|
||||
item_count: 2,
|
||||
seqno: 64,
|
||||
}
|
||||
.encode_into(&mut file)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
for _ in 0..5 {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::Start {
|
||||
item_count: 2,
|
||||
seqno: 64,
|
||||
}
|
||||
.encode_into(&mut file)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used, clippy::redundant_clone)]
|
||||
fn journal_truncation_repeating_end_marker() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
|
||||
let values = [
|
||||
BatchItem::new(keyspace.clone(), *b"abc", *b"def", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"yxc", *b"ghj", ValueType::Value),
|
||||
];
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
journal
|
||||
.get_writer()?
|
||||
.write_batch(values.iter(), values.len(), 0)?;
|
||||
}
|
||||
|
||||
{
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::End(5432).encode_into(&mut file)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
for _ in 0..5 {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::End(5432).encode_into(&mut file)?;
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expect(clippy::unwrap_used, clippy::redundant_clone)]
|
||||
fn journal_truncation_repeating_item_marker() -> crate::Result<()> {
|
||||
let dir1 = tempdir()?;
|
||||
let db = crate::Database::builder(&dir1).open()?;
|
||||
let keyspace = db.keyspace("default", Default::default)?;
|
||||
|
||||
let dir2 = tempdir()?;
|
||||
let path = dir2.path().join("0.jnl");
|
||||
|
||||
let values = [
|
||||
BatchItem::new(keyspace.clone(), *b"abc", *b"def", ValueType::Value),
|
||||
BatchItem::new(keyspace.clone(), *b"yxc", *b"ghj", ValueType::Value),
|
||||
];
|
||||
|
||||
{
|
||||
let journal = Journal::create_new(&path)?;
|
||||
journal
|
||||
.get_writer()?
|
||||
.write_batch(values.iter(), values.len(), 0)?;
|
||||
}
|
||||
|
||||
{
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
{
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::Item {
|
||||
keyspace_id: 0,
|
||||
key: (*b"zzz").into(),
|
||||
value: (*b"").into(),
|
||||
value_type: ValueType::Tombstone,
|
||||
compression: lsm_tree::CompressionType::None,
|
||||
}
|
||||
.encode_into(&mut file)?;
|
||||
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
// Mangle journal
|
||||
for _ in 0..5 {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
Entry::Item {
|
||||
keyspace_id: 0,
|
||||
key: (*b"zzz").into(),
|
||||
value: (*b"").into(),
|
||||
value_type: ValueType::Tombstone,
|
||||
compression: lsm_tree::CompressionType::None,
|
||||
}
|
||||
.encode_into(&mut file)?;
|
||||
|
||||
file.sync_all()?;
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let journal = Journal::from_file(&path)?;
|
||||
let reader = journal.get_reader()?;
|
||||
let collected = reader.flatten().collect::<Vec<_>>();
|
||||
assert_eq!(values.to_vec(), collected.first().unwrap().items);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use super::entry::{serialize_marker_item, Entry};
|
||||
use crate::{
|
||||
batch::item::Item as BatchItem, file::fsync_directory, journal::recovery::JournalId,
|
||||
keyspace::InternalKeyspaceId,
|
||||
};
|
||||
use lsm_tree::{CompressionType, SeqNo, ValueType};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
hash::Hasher,
|
||||
io::{BufWriter, Seek, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
// TODO: this should be a database configuration
|
||||
pub const PRE_ALLOCATED_BYTES: u64 = 64 * 1_024 * 1_024;
|
||||
|
||||
pub const JOURNAL_BUFFER_BYTES: usize = 8 * 1_024;
|
||||
|
||||
pub struct Writer {
|
||||
pub(crate) path: PathBuf,
|
||||
file: BufWriter<File>,
|
||||
buf: Vec<u8>,
|
||||
is_buffer_dirty: bool,
|
||||
|
||||
compression: CompressionType,
|
||||
compression_threshold: usize,
|
||||
}
|
||||
|
||||
/// The persist mode allows setting the durability guarantee of previous writes
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PersistMode {
|
||||
/// Flushes data to OS buffers. This allows the OS to write out data in case of an
|
||||
/// application crash.
|
||||
///
|
||||
/// When this function returns, data is **not** guaranteed to be persisted in case
|
||||
/// of a power loss event or OS crash.
|
||||
Buffer,
|
||||
|
||||
/// Flushes data using `fdatasync`.
|
||||
///
|
||||
/// Use if you know that `fdatasync` is sufficient for your file system and/or operating system.
|
||||
SyncData,
|
||||
|
||||
/// Flushes data + metadata using `fsync`.
|
||||
SyncAll,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn set_compression(&mut self, comp: CompressionType, threshold: usize) {
|
||||
self.compression = comp;
|
||||
self.compression_threshold = threshold;
|
||||
}
|
||||
|
||||
pub fn pos(&mut self) -> crate::Result<u64> {
|
||||
self.file.stream_position().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> crate::Result<u64> {
|
||||
Ok(self.file.get_ref().metadata()?.len())
|
||||
}
|
||||
|
||||
pub fn rotate(&mut self) -> crate::Result<(PathBuf, PathBuf)> {
|
||||
self.persist(PersistMode::SyncAll)?;
|
||||
|
||||
log::debug!(
|
||||
"Sealing active journal at {}, len={}B",
|
||||
self.path.display(),
|
||||
self.path
|
||||
.metadata()
|
||||
.inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to get file metadata of journal file at {}: {e:?}",
|
||||
self.path.display()
|
||||
);
|
||||
})?
|
||||
.len(),
|
||||
);
|
||||
|
||||
let prev_path = self.path.clone();
|
||||
|
||||
let folder = self
|
||||
.path
|
||||
.parent()
|
||||
.expect("should have parent")
|
||||
.to_path_buf();
|
||||
|
||||
let Some(basename) = self
|
||||
.path
|
||||
.file_name()
|
||||
.expect("should be valid file name")
|
||||
.to_str()
|
||||
.expect("should be valid utf-8")
|
||||
.strip_suffix(".jnl")
|
||||
else {
|
||||
log::error!("Invalid journal file name: {}", self.path.display());
|
||||
return Err(crate::Error::JournalRecovery(
|
||||
crate::JournalRecoveryError::InvalidFileName,
|
||||
));
|
||||
};
|
||||
|
||||
let journal_id = basename.parse::<JournalId>().map_err(|_| {
|
||||
log::error!("Invalid journal file name: {}", self.path.display());
|
||||
crate::Error::JournalRecovery(crate::JournalRecoveryError::InvalidFileName)
|
||||
})?;
|
||||
|
||||
let new_path = folder.join(format!("{}.jnl", journal_id + 1));
|
||||
log::debug!("Rotating active journal to {}", new_path.display());
|
||||
|
||||
let comp = self.compression;
|
||||
let compt = self.compression_threshold;
|
||||
*self = Self::create_new(new_path.clone())?;
|
||||
self.set_compression(comp, compt);
|
||||
|
||||
// IMPORTANT: fsync folder on Unix
|
||||
fsync_directory(&folder)?;
|
||||
|
||||
Ok((prev_path, new_path))
|
||||
}
|
||||
|
||||
pub fn create_new<P: Into<PathBuf>>(path: P) -> crate::Result<Self> {
|
||||
let path = path.into();
|
||||
|
||||
let file = File::create_new(&path).inspect_err(|e| {
|
||||
log::error!("Failed to create journal file at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
file.set_len(PRE_ALLOCATED_BYTES).inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to set journal file size to {PRE_ALLOCATED_BYTES}B at {}: {e:?}",
|
||||
path.display(),
|
||||
);
|
||||
})?;
|
||||
|
||||
file.sync_all().inspect_err(|e| {
|
||||
log::error!("Failed to fsync journal file at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
file: BufWriter::new(file),
|
||||
buf: Vec::new(),
|
||||
is_buffer_dirty: false,
|
||||
compression: CompressionType::None,
|
||||
compression_threshold: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.try_exists()? {
|
||||
let file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to create journal file at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
file.set_len(PRE_ALLOCATED_BYTES).inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to set journal file size to {PRE_ALLOCATED_BYTES}B at {}: {e:?}",
|
||||
path.display(),
|
||||
);
|
||||
})?;
|
||||
|
||||
file.sync_all().inspect_err(|e| {
|
||||
log::error!("Failed to fsync journal file at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
return Ok(Self {
|
||||
path: path.into(),
|
||||
file: BufWriter::with_capacity(JOURNAL_BUFFER_BYTES, file),
|
||||
buf: Vec::new(),
|
||||
is_buffer_dirty: false,
|
||||
compression: CompressionType::None,
|
||||
compression_threshold: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(path)
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to open journal file at {}: {e:?}", path.display());
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
path: path.into(),
|
||||
file: BufWriter::with_capacity(JOURNAL_BUFFER_BYTES, file),
|
||||
buf: Vec::new(),
|
||||
is_buffer_dirty: false,
|
||||
compression: CompressionType::None,
|
||||
compression_threshold: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Persists the journal file.
|
||||
pub(crate) fn persist(&mut self, mode: PersistMode) -> std::io::Result<()> {
|
||||
log::trace!(
|
||||
"Persisting journal at {} with mode={mode:?}",
|
||||
self.path.display(),
|
||||
);
|
||||
|
||||
if self.is_buffer_dirty {
|
||||
self.file.flush().inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to flush journal IO buffers at {}: {e:?}",
|
||||
self.path.display(),
|
||||
);
|
||||
})?;
|
||||
self.is_buffer_dirty = false;
|
||||
}
|
||||
|
||||
match mode {
|
||||
PersistMode::SyncAll => self.file.get_mut().sync_all().inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to fsync journal file at {}: {e:?}",
|
||||
self.path.display(),
|
||||
);
|
||||
}),
|
||||
PersistMode::SyncData => self.file.get_mut().sync_data().inspect_err(|e| {
|
||||
log::error!(
|
||||
"Failed to fsyncdata journal file at {}: {e:?}",
|
||||
self.path.display(),
|
||||
);
|
||||
}),
|
||||
PersistMode::Buffer => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a batch start marker to the journal
|
||||
fn write_start(&mut self, item_count: u32, seqno: SeqNo) -> Result<usize, crate::Error> {
|
||||
debug_assert!(self.buf.is_empty());
|
||||
|
||||
Entry::Start { item_count, seqno }.encode_into(&mut self.buf)?;
|
||||
|
||||
self.file.write_all(&self.buf)?;
|
||||
|
||||
Ok(self.buf.len())
|
||||
}
|
||||
|
||||
/// Writes a batch end marker to the journal
|
||||
fn write_end(&mut self, checksum: u64) -> Result<usize, crate::Error> {
|
||||
debug_assert!(self.buf.is_empty());
|
||||
|
||||
Entry::End(checksum).encode_into(&mut self.buf)?;
|
||||
|
||||
self.file.write_all(&self.buf)?;
|
||||
|
||||
Ok(self.buf.len())
|
||||
}
|
||||
|
||||
pub(crate) fn write_raw(
|
||||
&mut self,
|
||||
keyspace_id: InternalKeyspaceId,
|
||||
key: &[u8],
|
||||
value: &[u8],
|
||||
value_type: ValueType,
|
||||
seqno: u64,
|
||||
) -> crate::Result<usize> {
|
||||
self.is_buffer_dirty = true;
|
||||
|
||||
let mut hasher = xxhash_rust::xxh3::Xxh3::default();
|
||||
let mut byte_count = 0;
|
||||
|
||||
self.buf.clear();
|
||||
byte_count += self.write_start(1, seqno)?;
|
||||
self.buf.clear();
|
||||
|
||||
serialize_marker_item(
|
||||
&mut self.buf,
|
||||
keyspace_id,
|
||||
key,
|
||||
value,
|
||||
value_type,
|
||||
if self.compression_threshold > 0 && value.len() >= self.compression_threshold {
|
||||
self.compression
|
||||
} else {
|
||||
CompressionType::None
|
||||
},
|
||||
)?;
|
||||
|
||||
self.file.write_all(&self.buf)?;
|
||||
|
||||
hasher.update(&self.buf);
|
||||
byte_count += self.buf.len();
|
||||
|
||||
self.buf.clear();
|
||||
let checksum = hasher.finish();
|
||||
byte_count += self.write_end(checksum)?;
|
||||
|
||||
Ok(byte_count)
|
||||
}
|
||||
|
||||
pub(crate) fn write_clear(
|
||||
&mut self,
|
||||
keyspace_id: InternalKeyspaceId,
|
||||
seqno: SeqNo,
|
||||
) -> crate::Result<usize> {
|
||||
self.is_buffer_dirty = true;
|
||||
|
||||
let mut hasher = xxhash_rust::xxh3::Xxh3::default();
|
||||
let mut byte_count = 0;
|
||||
|
||||
self.buf.clear();
|
||||
byte_count += self.write_start(1, seqno)?;
|
||||
self.buf.clear();
|
||||
|
||||
Entry::Clear { keyspace_id }.encode_into(&mut self.buf)?;
|
||||
self.file.write_all(&self.buf)?;
|
||||
hasher.update(&self.buf);
|
||||
byte_count += self.buf.len();
|
||||
|
||||
self.buf.clear();
|
||||
let checksum = hasher.finish();
|
||||
byte_count += self.write_end(checksum)?;
|
||||
|
||||
Ok(byte_count)
|
||||
}
|
||||
|
||||
pub fn write_batch<'a>(
|
||||
&mut self,
|
||||
items: impl Iterator<Item = &'a BatchItem>,
|
||||
batch_size: usize,
|
||||
seqno: SeqNo,
|
||||
) -> crate::Result<usize> {
|
||||
if batch_size == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
self.is_buffer_dirty = true;
|
||||
|
||||
self.buf.clear();
|
||||
|
||||
// NOTE: entries.len() is surely never > u32::MAX
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
let item_count = batch_size as u32;
|
||||
|
||||
let mut hasher = xxhash_rust::xxh3::Xxh3::default();
|
||||
let mut byte_count = 0;
|
||||
|
||||
byte_count += self.write_start(item_count, seqno)?;
|
||||
self.buf.clear();
|
||||
|
||||
for item in items {
|
||||
debug_assert!(self.buf.is_empty());
|
||||
|
||||
serialize_marker_item(
|
||||
&mut self.buf,
|
||||
item.keyspace.id,
|
||||
&item.key,
|
||||
&item.value,
|
||||
item.value_type,
|
||||
if self.compression_threshold > 0 && item.value.len() >= self.compression_threshold
|
||||
{
|
||||
self.compression
|
||||
} else {
|
||||
CompressionType::None
|
||||
},
|
||||
)?;
|
||||
|
||||
self.file.write_all(&self.buf)?;
|
||||
|
||||
hasher.update(&self.buf);
|
||||
byte_count += self.buf.len();
|
||||
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
let checksum = hasher.finish();
|
||||
byte_count += self.write_end(checksum)?;
|
||||
|
||||
Ok(byte_count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::keyspace::config::{DecodeConfig, EncodeConfig};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
impl EncodeConfig for crate::config::BlockSizePolicy {
|
||||
fn encode(&self) -> crate::Slice {
|
||||
let mut v = vec![];
|
||||
|
||||
// NOTE: Policies are limited to 255 entries
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_u8(self.len() as u8)
|
||||
.expect("cannot fail writing into a vec");
|
||||
|
||||
for item in self.iter() {
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_u32::<LittleEndian>(*item)
|
||||
.expect("cannot fail writing into a vec");
|
||||
}
|
||||
|
||||
v.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodeConfig for crate::config::BlockSizePolicy {
|
||||
fn decode(mut bytes: &[u8]) -> crate::Result<Self> {
|
||||
let len = bytes.read_u8()?;
|
||||
|
||||
let mut v = vec![];
|
||||
|
||||
for _ in 0..len {
|
||||
v.push(bytes.read_u32::<LittleEndian>()?);
|
||||
}
|
||||
|
||||
Ok(Self::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_block_size_policy() -> crate::Result<()> {
|
||||
let policy = crate::config::BlockSizePolicy::new([1024, 2048, 4096]);
|
||||
let encoded = policy.encode();
|
||||
let decoded = crate::config::BlockSizePolicy::decode(&encoded)?;
|
||||
assert_eq!(policy, decoded);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::keyspace::config::{DecodeConfig, EncodeConfig};
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use lsm_tree::{
|
||||
coding::{Decode, Encode},
|
||||
CompressionType,
|
||||
};
|
||||
|
||||
impl EncodeConfig for crate::config::CompressionPolicy {
|
||||
fn encode(&self) -> crate::Slice {
|
||||
let mut v = vec![];
|
||||
|
||||
// NOTE: Policies are limited to 255 entries
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_u8(self.len() as u8)
|
||||
.expect("cannot fail writing into a vec");
|
||||
|
||||
for item in self.iter() {
|
||||
#[expect(clippy::expect_used)]
|
||||
item.encode_into(&mut v)
|
||||
.expect("cannot fail writing into a vec");
|
||||
}
|
||||
|
||||
v.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodeConfig for crate::config::CompressionPolicy {
|
||||
fn decode(mut bytes: &[u8]) -> crate::Result<Self> {
|
||||
let len = bytes.read_u8()?;
|
||||
|
||||
let mut v = vec![];
|
||||
|
||||
for _ in 0..len {
|
||||
v.push(CompressionType::decode_from(&mut bytes)?);
|
||||
}
|
||||
|
||||
Ok(Self::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "lz4")]
|
||||
fn roundtrip_compression_policy() -> crate::Result<()> {
|
||||
let policy =
|
||||
crate::config::CompressionPolicy::new([CompressionType::None, CompressionType::Lz4]);
|
||||
let encoded = policy.encode();
|
||||
let decoded = crate::config::CompressionPolicy::decode(&encoded)?;
|
||||
assert_eq!(policy, decoded);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::keyspace::config::{DecodeConfig, EncodeConfig};
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
|
||||
impl EncodeConfig for crate::config::FilterPolicy {
|
||||
fn encode(&self) -> crate::Slice {
|
||||
let mut v = vec![];
|
||||
|
||||
// NOTE: Policies are limited to 255 entries
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_u8(self.len() as u8)
|
||||
.expect("cannot fail writing into a vec");
|
||||
|
||||
for item in self.iter() {
|
||||
match item {
|
||||
crate::config::FilterPolicyEntry::None => {
|
||||
v.write_u8(0).expect("cannot fail writing into a vec");
|
||||
}
|
||||
crate::config::FilterPolicyEntry::Bloom(bloom) => {
|
||||
v.write_u8(1).expect("cannot fail writing into a vec");
|
||||
|
||||
match bloom {
|
||||
crate::config::BloomConstructionPolicy::BitsPerKey(bits) => {
|
||||
v.write_u8(0).expect("cannot fail writing into a vec");
|
||||
v.write_f32::<byteorder::LittleEndian>(*bits)
|
||||
.expect("cannot fail writing into a vec");
|
||||
}
|
||||
crate::config::BloomConstructionPolicy::FalsePositiveRate(fpr) => {
|
||||
v.write_u8(1).expect("cannot fail writing into a vec");
|
||||
v.write_f32::<byteorder::LittleEndian>(*fpr)
|
||||
.expect("cannot fail writing into a vec");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodeConfig for crate::config::FilterPolicy {
|
||||
fn decode(mut bytes: &[u8]) -> crate::Result<Self> {
|
||||
let len = bytes.read_u8()?;
|
||||
|
||||
let mut v = vec![];
|
||||
|
||||
for _ in 0..len {
|
||||
let tag = bytes.read_u8()?;
|
||||
|
||||
match tag {
|
||||
0 => {
|
||||
v.push(crate::config::FilterPolicyEntry::None);
|
||||
}
|
||||
1 => {
|
||||
let policy_type = bytes.read_u8()?;
|
||||
|
||||
let policy = match policy_type {
|
||||
0 => {
|
||||
let bits = bytes.read_f32::<byteorder::LittleEndian>()?;
|
||||
|
||||
crate::config::FilterPolicyEntry::Bloom(
|
||||
crate::config::BloomConstructionPolicy::BitsPerKey(bits),
|
||||
)
|
||||
}
|
||||
1 => {
|
||||
let value = bytes.read_f32::<byteorder::LittleEndian>()?;
|
||||
|
||||
crate::config::FilterPolicyEntry::Bloom(
|
||||
crate::config::BloomConstructionPolicy::FalsePositiveRate(value),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown bloom filter policy type: {policy_type}");
|
||||
}
|
||||
};
|
||||
|
||||
v.push(policy);
|
||||
}
|
||||
_ => {
|
||||
panic!("unknown filter policy tag: {tag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_filter_policy() -> crate::Result<()> {
|
||||
let policy = crate::config::FilterPolicy::new([
|
||||
crate::config::FilterPolicyEntry::Bloom(
|
||||
crate::config::BloomConstructionPolicy::BitsPerKey(10.0),
|
||||
),
|
||||
crate::config::FilterPolicyEntry::Bloom(
|
||||
crate::config::BloomConstructionPolicy::FalsePositiveRate(0.01),
|
||||
),
|
||||
crate::config::FilterPolicyEntry::None,
|
||||
]);
|
||||
|
||||
let encoded = policy.encode();
|
||||
let decoded = crate::config::FilterPolicy::decode(&encoded)?;
|
||||
|
||||
assert_eq!(policy, decoded);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
use crate::keyspace::config::{DecodeConfig, EncodeConfig};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
impl EncodeConfig for crate::config::HashRatioPolicy {
|
||||
fn encode(&self) -> crate::Slice {
|
||||
let mut v = vec![];
|
||||
|
||||
// NOTE: Policies are limited to 255 entries
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_u8(self.len() as u8)
|
||||
.expect("cannot fail writing into a vec");
|
||||
|
||||
for item in self.iter() {
|
||||
#[expect(clippy::expect_used)]
|
||||
v.write_f32::<LittleEndian>(*item)
|
||||
.expect("cannot fail writing into a vec");
|
||||
}
|
||||
|
||||
v.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodeConfig for crate::config::HashRatioPolicy {
|
||||
fn decode(mut bytes: &[u8]) -> crate::Result<Self> {
|
||||
let len = bytes.read_u8()?;
|
||||
|
||||
let mut v = vec![];
|
||||
|
||||
for _ in 0..len {
|
||||
v.push(bytes.read_f32::<LittleEndian>()?);
|
||||
}
|
||||
|
||||
Ok(Self::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use test_log::test;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_hash_ratio_policy() -> crate::Result<()> {
|
||||
let policy = crate::config::HashRatioPolicy::new([0.1, 0.2, 0.3]);
|
||||
let encoded = policy.encode();
|
||||
let decoded = crate::config::HashRatioPolicy::decode(&encoded)?;
|
||||
assert_eq!(policy, decoded);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2024-present, fjall-rs
|
||||
// This source code is licensed under both the Apache 2.0 and MIT License
|
||||
// (found in the LICENSE-* files in the repository)
|
||||
|
||||
mod block_size;
|
||||
mod compression;
|
||||
mod filter;
|
||||
mod hash_ratio;
|
||||
mod pinning;
|
||||
mod restart_interval;
|
||||
|
||||
pub trait EncodeConfig {
|
||||
fn encode(&self) -> crate::Slice;
|
||||
}
|
||||
|
||||
pub trait DecodeConfig {
|
||||
fn decode(bytes: &[u8]) -> crate::Result<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user