diff --git a/.gitignore b/.gitignore index 5a32aa3f6..b1c0d9b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ _* /*.txt /*.csv /tmp +**/.test # Logs *.log* diff --git a/Cargo.lock b/Cargo.lock index d4d318c38..ce36f0441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,9 +861,11 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "byteview" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d74937895e761d5984206f82ca8ff7725d0fd8021921011921894b41ab1b9f7" +version = "0.3.6" +dependencies = [ + "serde", + "serde_json", +] [[package]] name = "cast" @@ -1462,18 +1464,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "env_filter" version = "2.0.0" @@ -1567,7 +1557,6 @@ dependencies = [ "lsm-tree", "lz4_flex", "nanoid", - "rand 0.10.2", "tempfile", "test-log", "xxhash-rust", @@ -1789,12 +1778,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - [[package]] name = "hashbrown" version = "0.17.1" @@ -2288,9 +2271,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -2333,18 +2316,16 @@ name = "lsm-tree" version = "0.3.6" dependencies = [ "byteorder-lite", - "bytes", "byteview", "criterion", "crossbeam-skiplist", - "enum_dispatch", "fs_extra", "interval-heap", "log", "lz4_flex", "nanoid", "quick_cache", - "rand 0.9.5", + "rand 0.10.2", "rustc-hash", "self_cell", "sfa", @@ -2468,11 +2449,11 @@ dependencies = [ [[package]] name = "nanoid" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873" dependencies = [ - "rand 0.8.7", + "rand 0.9.5", ] [[package]] @@ -2624,9 +2605,9 @@ dependencies = [ [[package]] name = "pco" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553ccdc7f6999785559af4998c79712a5ab820e26b68bad9146609c19587ec82" +checksum = "386342cad4c6e97f081568e5d910ea7d871314c843aa8fc564f2a6b64cab9456" dependencies = [ "better_io", "dtype_dispatch", @@ -2781,12 +2762,12 @@ dependencies = [ [[package]] name = "quick_cache" -version = "0.6.24" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c6658afe513a3b484e3abfdaa0d03ef3c0bbf017542c178dd55f94eb3051f9" +checksum = "403c1a912fec895cafb223201e368234842acb9220aaf08ab042ae89ba5f135c" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -2826,24 +2807,13 @@ dependencies = [ "ptr_meta", ] -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -2858,16 +2828,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -2883,9 +2843,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] [[package]] name = "rand_core" @@ -4047,7 +4004,7 @@ name = "vecdb_derive" version = "0.3.6" dependencies = [ "quote", - "syn 2.0.119", + "syn 3.0.3", "tempfile", "vecdb", ] diff --git a/Cargo.toml b/Cargo.toml index 0ca1fcbf0..9c186a460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ 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.2" +byteview = { version = "0.3.6", path = "crates/byteview" } 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 } @@ -70,10 +70,10 @@ jiff = { version = "0.2.35", features = ["perf-inline", "tz-system"], default-fe 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 } +lz4_flex = { version = "=0.13.1", default-features = false } owo-colors = "4.3.0" parking_lot = "0.12.5" -pco = "1.0.2" +pco = "1.0.3" quickmatch = { path = "crates/quickmatch", version = "0.3.6" } rawdb = { path = "crates/rawdb", version = "0.3.6" } rayon = "1.12.0" diff --git a/crates/brk_store/src/lib.rs b/crates/brk_store/src/lib.rs index f9b4a49cc..a52413687 100644 --- a/crates/brk_store/src/lib.rs +++ b/crates/brk_store/src/lib.rs @@ -299,14 +299,14 @@ where let mut ingestion = keyspace.start_ingestion()?; // FxHashMap/FxHashSet keep keys unique and disjoint; sorting therefore - // proves the strict ordering required by the prevalidated writer. + // proves the strict ordering required by ingestion. for item in items { match item { Item::Value { key, value } => { - ingestion.write_prevalidated(ByteView::from(key), ByteView::from(value))?; + ingestion.write(ByteView::from(key), ByteView::from(value))?; } Item::Tomb(key) => { - ingestion.write_prevalidated_weak_tombstone(ByteView::from(key))?; + ingestion.write_weak_tombstone(ByteView::from(key))?; } } } @@ -335,22 +335,19 @@ where (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))?; + ingestion.write(ByteView::from(key), ByteView::from(value))?; } Ordering::Greater => { - ingestion.write_prevalidated_weak_tombstone(ByteView::from( - dels.next().unwrap(), - ))?; + ingestion.write_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))?; + ingestion.write(ByteView::from(key), ByteView::from(value))?; } (None, Some(_)) => { - ingestion - .write_prevalidated_weak_tombstone(ByteView::from(dels.next().unwrap()))?; + ingestion.write_weak_tombstone(ByteView::from(dels.next().unwrap()))?; } (None, None) => break, } diff --git a/crates/byteview/Cargo.toml b/crates/byteview/Cargo.toml new file mode 100644 index 000000000..8c65e8889 --- /dev/null +++ b/crates/byteview/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "byteview" +description = "Thin, immutable zero-copy slice type" +license = "MIT OR Apache-2.0" +version.workspace = true +edition.workspace = true +readme = "README.md" +repository.workspace = true +homepage.workspace = true +categories = ["data-structures"] +keywords = ["german-string", "string-view", "byte-slice"] + +[features] +default = [] +serde = ["dep:serde"] + +[dependencies] +serde = { workspace = true, optional = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/crates/byteview/LICENSE-APACHE b/crates/byteview/LICENSE-APACHE new file mode 100644 index 000000000..0e5254213 --- /dev/null +++ b/crates/byteview/LICENSE-APACHE @@ -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 diff --git a/crates/byteview/LICENSE-MIT b/crates/byteview/LICENSE-MIT new file mode 100644 index 000000000..97f5e0faa --- /dev/null +++ b/crates/byteview/LICENSE-MIT @@ -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. diff --git a/crates/byteview/README.md b/crates/byteview/README.md new file mode 100644 index 000000000..f7338dfdc --- /dev/null +++ b/crates/byteview/README.md @@ -0,0 +1,76 @@ +# byteview + +[![CI](https://github.com/fjall-rs/byteview/actions/workflows/test.yml/badge.svg)](https://github.com/fjall-rs/byteview/actions/workflows/test.yml) +[![CI](https://github.com/fjall-rs/byteview/actions/workflows/miri.yml/badge.svg)](https://github.com/fjall-rs/byteview/actions/workflows/miri.yml) +[![docs.rs](https://img.shields.io/docsrs/byteview?color=green)](https://docs.rs/byteview) +[![Crates.io](https://img.shields.io/crates/v/byteview?color=blue)](https://crates.io/crates/byteview) +![MSRV](https://img.shields.io/badge/MSRV-1.87-blue) + +An immutable byte slice that may be inlined, and can be partially cloned without heap allocation. + +Think of it as a specialized `Arc<[u8]>` that can be inlined (skip allocation for small values) and no weak count. + +![Memory layout](./byteview.png) + +`byteview` was originally designed to speed up deserialization in [`lsm-tree`](https://github.com/fjall-rs/lsm-tree), allow inlining of small values and reduce memory usage compared to Arc'd slices. +Values with a known length can be constructed 2-2.5x faster than using `Arc<[u8]>`: + +![Constructor benchmark](ctor_bench.png) + +## Sponsors + + + + + + Orbitinghail + + + +## Memory usage + +Allocating 200M "" (len=0) strings: + +| Struct | Memory Usage | +|-----------------|--------------| +| `Arc<[u8]>` | 9.6 GB | +| `tokio::Bytes` | 6.4 GB | +| `ByteView` | 4.8 GB | + +Allocating 200M "helloworld" (len=10) strings: + +| Struct | Memory Usage | +|-----------------|--------------| +| `Arc<[u8]>` | 12.8 GB | +| `tokio::Bytes` | 12.8 GB | +| `ByteView` | 4.8 GB | + +Allocating 100M "helloworldhelloworld" (len=20) strings: + +| Struct | Memory Usage | +|-----------------|--------------| +| `Arc<[u8]>` | 6.4 GB | +| `tokio::Bytes` | 6.4 GB | +| `ByteView` | 2.4 GB | + +Allocating 50M "helloworldhelloworldhelloworldhelloworld" (len=30) strings: + +| Struct | Memory Usage | +|-----------------|--------------| +| `Arc<[u8]>` | 4.0 GB | +| `tokio::Bytes` | 4.0 GB | +| `ByteView` | 3.6 GB | + +Allocating 500k `"helloworld".repeat(1000)` (len=10'000) strings: + +| Struct | Memory Usage | +|-----------------|--------------| +| `Arc<[u8]>` | 5 GB | +| `tokio::Bytes` | 5 GB | +| `ByteView` | 5 GB | + +## Run fuzz tests + +```bash +cargo +nightly fuzz run fuzz_target_1 +``` diff --git a/crates/byteview/src/builder.rs b/crates/byteview/src/builder.rs new file mode 100644 index 000000000..ddac7d7b8 --- /dev/null +++ b/crates/byteview/src/builder.rs @@ -0,0 +1,33 @@ +use crate::ByteView; + +/// A builder for a [`ByteView`] that allows mutation before freezing it. +pub struct Builder(ByteView); + +impl Builder { + /// Creates a new builder. + #[must_use] + pub const fn new(inner: ByteView) -> Self { + Self(inner) + } + + /// Converts the builder into a [`ByteView`], making it immutable. + #[must_use] + pub fn freeze(mut self) -> ByteView { + self.0.update_prefix(); + self.0 + } +} + +impl std::ops::Deref for Builder { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Builder { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.get_mut_slice() + } +} diff --git a/crates/byteview/src/byteview.rs b/crates/byteview/src/byteview.rs new file mode 100644 index 000000000..5b4aedb3b --- /dev/null +++ b/crates/byteview/src/byteview.rs @@ -0,0 +1,1283 @@ +// 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::{ + mem::ManuallyDrop, + ops::Deref, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + +pub use crate::builder::Builder; + +#[cfg(target_pointer_width = "64")] +const INLINE_SIZE: usize = 20; + +#[cfg(target_pointer_width = "32")] +const INLINE_SIZE: usize = 16; + +const PREFIX_SIZE: usize = 4; + +#[repr(C)] +struct HeapAllocationHeader { + ref_count: AtomicU64, +} + +#[repr(C)] +struct ShortRepr { + len: u32, + data: [u8; INLINE_SIZE], +} + +#[repr(C)] +struct LongRepr { + len: u32, + prefix: [u8; PREFIX_SIZE], + heap: *const u8, + original_len: u32, + offset: u32, +} + +#[repr(C)] +pub union Trailer { + short: ManuallyDrop, + long: ManuallyDrop, +} + +impl Default for Trailer { + fn default() -> Self { + Self { + short: ManuallyDrop::new(ShortRepr { + len: 0, + data: [0; INLINE_SIZE], + }), + } + } +} + +/// An immutable byte slice +/// +/// Will be inlined (no pointer dereference or heap allocation) +/// if it is 20 characters or shorter (on a 64-bit system). +/// +/// A single heap allocation will be shared between multiple slices. +/// Even subslices of that heap allocation can be cloned without additional heap allocation. +/// +/// [`ByteView`] does not guarantee any sort of alignment for zero-copy (de)serialization. +/// +/// The design is very similar to: +/// +/// - [Polars' strings]() +/// - [CedarDB's German strings]() +/// - [Umbra's string]() +/// - [Velox' String View](https://facebookincubator.github.io/velox/develop/vectors.html) +/// - [Apache Arrow's String View](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow14BinaryViewType6c_typeE) +#[repr(C)] +#[derive(Default)] +pub struct ByteView { + trailer: Trailer, +} + +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for ByteView {} +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for ByteView {} + +impl Clone for ByteView { + fn clone(&self) -> Self { + self.slice(..) + } +} + +impl Drop for ByteView { + fn drop(&mut self) { + if self.is_inline() { + return; + } + + let heap_region = self.get_heap_region(); + + if heap_region.ref_count.fetch_sub(1, Ordering::AcqRel) != 1 { + return; + } + + unsafe { + let header_size = std::mem::size_of::(); + let alignment = std::mem::align_of::(); + let total_size = header_size + self.trailer.long.original_len as usize; + let layout = std::alloc::Layout::from_size_align(total_size, alignment).unwrap(); + + let ptr = self.trailer.long.heap.cast_mut(); + std::alloc::dealloc(ptr, layout); + } + } +} + +impl Eq for ByteView {} + +impl std::cmp::PartialEq for ByteView { + fn eq(&self, other: &Self) -> bool { + unsafe { + let src_ptr = (self as *const Self).cast::(); + let other_ptr: *const u8 = (other as *const Self).cast::(); + + let a = *src_ptr.cast::(); + let b = *other_ptr.cast::(); + + if a != b { + return false; + } + } + + // NOTE: At this point we know + // both strings must have the same prefix and same length + // + // If we are inlined, the other string must be inlined too, + // so checking the short slice is enough + if self.is_inline() { + self.get_short_slice() == other.get_short_slice() + } else { + self.get_long_slice() == other.get_long_slice() + } + } +} + +impl std::cmp::Ord for ByteView { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.prefix() + .cmp(other.prefix()) + .then_with(|| self.deref().cmp(&**other)) + } +} + +impl std::cmp::PartialOrd for ByteView { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::fmt::Debug for ByteView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &**self) + } +} + +impl Deref for ByteView { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + if self.is_inline() { + self.get_short_slice() + } else { + self.get_long_slice() + } + } +} + +impl std::hash::Hash for ByteView { + fn hash(&self, state: &mut H) { + self.deref().hash(state); + } +} + +/// RAII guard for [`ByteView::get_mut`], so the prefix gets +/// updated properly when the mutation is done +pub struct Mutator<'a>(pub(crate) &'a mut ByteView); + +impl std::ops::Deref for Mutator<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl std::ops::DerefMut for Mutator<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.get_mut_slice() + } +} + +impl Drop for Mutator<'_> { + fn drop(&mut self) { + self.0.update_prefix(); + } +} + +impl ByteView { + #[doc(hidden)] + #[must_use] + pub unsafe fn builder_unzeroed(len: usize) -> Builder { + // SAFETY: The caller is responsible for initializing every byte before + // the returned builder is frozen. + unsafe { Builder::new(Self::with_size_unzeroed(len)) } + } + + #[doc(hidden)] + #[must_use] + pub fn builder(len: usize) -> Builder { + Builder::new(Self::with_size(len)) + } + + fn prefix(&self) -> &[u8] { + let len = PREFIX_SIZE.min(self.len()); + + // SAFETY: Both trailer layouts have the prefix stored at the same position + unsafe { self.trailer.short.data.get_unchecked(..len) } + } + + fn is_inline(&self) -> bool { + self.len() <= INLINE_SIZE + } + + pub(crate) fn update_prefix(&mut self) { + if !self.is_inline() { + unsafe { + let slice_ptr: &[u8] = &*self; + let slice_ptr = slice_ptr.as_ptr(); + + // Zero out prefix + (*self.trailer.long).prefix[0] = 0; + (*self.trailer.long).prefix[1] = 0; + (*self.trailer.long).prefix[2] = 0; + (*self.trailer.long).prefix[3] = 0; + + let prefix = (*self.trailer.long).prefix.as_mut_ptr(); + std::ptr::copy_nonoverlapping(slice_ptr, prefix, self.len().min(4)); + } + } + } + + /// Returns a mutable reference into the given byteview, if there are no other pointers to the same allocation. + pub fn get_mut(&mut self) -> Option> { + if self.ref_count() == 1 { + Some(Mutator(self)) + } else { + None + } + } + + /// Creates a byteview and populates it with `len` bytes + /// from the given reader. + /// + /// # Errors + /// + /// Returns an error if an I/O error occurred. + pub fn from_reader(reader: &mut R, len: usize) -> std::io::Result { + // NOTE: We can use _unzeroed to skip zeroing of the heap allocated slice + // because we receive the `len` parameter + // If the reader does not give us exactly `len` bytes, `read_exact` fails anyway + let mut s = unsafe { Self::with_size_unzeroed(len) }; + { + let mut builder = Mutator(&mut s); + reader.read_exact(&mut builder)?; + } + Ok(s) + } + + /// Fuses two byte slices into a single byteview. + #[must_use] + pub fn fused(left: &[u8], right: &[u8]) -> Self { + let len = left.len() + right.len(); + let mut builder = unsafe { Self::builder_unzeroed(len) }; + builder[0..left.len()].copy_from_slice(left); + builder[left.len()..].copy_from_slice(right); + builder.freeze() + } + + /// Creates a new zeroed, fixed-length byteview. + /// + /// Use [`ByteView::get_mut`] to mutate the content. + /// + /// # Panics + /// + /// Panics if the length does not fit in a u32 (4 GiB). + #[must_use] + pub fn with_size(slice_len: usize) -> Self { + Self::with_size_zeroed(slice_len) + } + + /// Creates a new zeroed, fixed-length byteview. + /// + /// # Panics + /// + /// Panics if the length does not fit in a u32 (4 GiB). + fn with_size_zeroed(slice_len: usize) -> Self { + let view = if slice_len <= INLINE_SIZE { + Self { + trailer: Trailer { + short: ManuallyDrop::new(ShortRepr { + // SAFETY: We know slice_len is INLINE_SIZE or less, so it must be + // a valid u32 + #[allow(clippy::cast_possible_truncation)] + len: slice_len as u32, + data: [0; INLINE_SIZE], + }), + }, + } + } else { + let Ok(len) = u32::try_from(slice_len) else { + panic!("byte slice too long"); + }; + + unsafe { + const HEADER_SIZE: usize = std::mem::size_of::(); + const ALIGNMENT: usize = std::mem::align_of::(); + + let total_size = HEADER_SIZE + slice_len; + let layout = std::alloc::Layout::from_size_align(total_size, ALIGNMENT).unwrap(); + + // IMPORTANT: Zero-allocate the region + let heap_ptr = std::alloc::alloc_zeroed(layout); + if heap_ptr.is_null() { + std::alloc::handle_alloc_error(layout); + } + + // Set ref count + let heap_region = heap_ptr as *const HeapAllocationHeader; + let heap_region = &*heap_region; + heap_region.ref_count.store(1, Ordering::Release); + + Self { + trailer: Trailer { + long: ManuallyDrop::new(LongRepr { + len, + prefix: [0; PREFIX_SIZE], + heap: heap_ptr, + original_len: len, + offset: 0, + }), + }, + } + } + }; + + debug_assert_eq!(1, view.ref_count()); + + view + } + + /// Creates a new fixed-length byteview, **with uninitialized contents**. + /// + /// # Panics + /// + /// Panics if the length does not fit in a u32 (4 GiB). + #[doc(hidden)] + #[must_use] + pub unsafe fn with_size_unzeroed(slice_len: usize) -> Self { + let view = if slice_len <= INLINE_SIZE { + Self { + trailer: Trailer { + short: ManuallyDrop::new(ShortRepr { + // SAFETY: We know slice_len is INLINE_SIZE or less, so it must be + // a valid u32 + #[allow(clippy::cast_possible_truncation)] + len: slice_len as u32, + data: [0; INLINE_SIZE], + }), + }, + } + } else { + let Ok(len) = u32::try_from(slice_len) else { + panic!("byte slice too long"); + }; + + unsafe { + const HEADER_SIZE: usize = std::mem::size_of::(); + const ALIGNMENT: usize = std::mem::align_of::(); + + let total_size = HEADER_SIZE + slice_len; + let layout = std::alloc::Layout::from_size_align(total_size, ALIGNMENT).unwrap(); + + let heap_ptr = std::alloc::alloc(layout); + if heap_ptr.is_null() { + std::alloc::handle_alloc_error(layout); + } + + // Set ref count + let heap_region = heap_ptr as *const HeapAllocationHeader; + let heap_region = &*heap_region; + heap_region.ref_count.store(1, Ordering::Release); + + Self { + trailer: Trailer { + long: ManuallyDrop::new(LongRepr { + len, + prefix: [0; PREFIX_SIZE], + heap: heap_ptr, + original_len: len, + offset: 0, + }), + }, + } + } + }; + + debug_assert_eq!(1, view.ref_count()); + + view + } + + /// Creates a new byteview from an existing byte slice. + /// + /// Will heap-allocate the slice if it has at least length 21. + /// + /// # Panics + /// + /// Panics if the length does not fit in a u32 (4 GiB). + #[must_use] + pub fn new(slice: &[u8]) -> Self { + let slice_len = slice.len(); + + let mut view = unsafe { Self::with_size_unzeroed(slice_len) }; + + if view.is_inline() { + // SAFETY: We check for inlinability + // so we know the the input slice fits our buffer + unsafe { + let data_ptr = std::ptr::addr_of_mut!((*view.trailer.short).data).cast(); + std::ptr::copy_nonoverlapping(slice.as_ptr(), data_ptr, slice_len); + } + } else { + let long_repr = unsafe { &mut *view.trailer.long }; + + // Copy prefix + // SAFETY: We know that there are at least 4 bytes in the input slice + #[allow(clippy::indexing_slicing)] + long_repr.prefix.copy_from_slice(&slice[0..PREFIX_SIZE]); + + // Copy byte slice into heap allocation + view.get_mut_slice().copy_from_slice(slice); + } + + debug_assert_eq!(1, view.ref_count()); + + view + } + + unsafe fn data_ptr(&self) -> *const u8 { + const HEADER_SIZE: usize = std::mem::size_of::(); + + debug_assert!(!self.is_inline()); + + // SAFETY: The non-inline representation is active, and its allocation + // contains the header followed by `original_len` data bytes. + unsafe { + self.trailer + .long + .heap + .add(HEADER_SIZE) + .add(self.trailer.long.offset as usize) + } + } + + unsafe fn data_ptr_mut(&mut self) -> *mut u8 { + const HEADER_SIZE: usize = std::mem::size_of::(); + + debug_assert!(!self.is_inline()); + + // SAFETY: The non-inline representation is active, and its allocation + // contains the header followed by `original_len` data bytes. + unsafe { + self.trailer + .long + .heap + .add(HEADER_SIZE) + .add(self.trailer.long.offset as usize) + .cast_mut() + } + } + + fn get_heap_region(&self) -> &HeapAllocationHeader { + debug_assert!( + !self.is_inline(), + "inline slice does not have a heap allocation" + ); + + unsafe { + let ptr = self.trailer.long.heap; + let heap_region: *const HeapAllocationHeader = ptr.cast::(); + &*heap_region + } + } + + /// Returns the ref_count of the underlying heap allocation. + #[doc(hidden)] + #[must_use] + pub fn ref_count(&self) -> u64 { + if self.is_inline() { + 1 + } else { + self.get_heap_region().ref_count.load(Ordering::Acquire) + } + } + + /// Clones the contents of this slice into an independently tracked slice. + #[must_use] + pub fn to_detached(&self) -> Self { + Self::new(self) + } + + /// Clones the given range of the existing byteview without heap allocation. + /// + /// # Examples + /// + /// ``` + /// # use byteview::ByteView; + /// let slice = ByteView::from("helloworld_thisisalongstring"); + /// let copy = slice.slice(11..); + /// assert_eq!(b"thisisalongstring", &*copy); + /// ``` + /// + /// # Panics + /// + /// Panics if the slice is out of bounds. + #[must_use] + pub fn slice(&self, range: impl std::ops::RangeBounds) -> Self { + use core::ops::Bound; + + // Credits: This is essentially taken from + // https://github.com/tokio-rs/bytes/blob/291df5acc94b82a48765e67eeb1c1a2074539e68/src/bytes.rs#L264 + + let self_len = self.len(); + + let begin = match range.start_bound() { + Bound::Included(&n) => n, + Bound::Excluded(&n) => n.checked_add(1).expect("out of range"), + Bound::Unbounded => 0, + }; + + let end = match range.end_bound() { + Bound::Included(&n) => n.checked_add(1).expect("out of range"), + Bound::Excluded(&n) => n, + Bound::Unbounded => self_len, + }; + + assert!( + begin <= end, + "range start must not be greater than end: {begin:?} <= {end:?}", + ); + assert!( + end <= self_len, + "range end out of bounds: {end:?} <= {self_len:?}", + ); + + let new_len = end - begin; + let len = u32::try_from(new_len).unwrap(); + + // Target and destination slices are inlined + // so we just need to memcpy the struct, and replace + // the inline slice with the requested range + if new_len <= INLINE_SIZE { + let mut child = Self { + trailer: Trailer { + short: ManuallyDrop::new(ShortRepr { + len, + data: [0; INLINE_SIZE], + }), + }, + }; + + let slice = &self[begin..end]; + debug_assert_eq!(slice.len(), new_len); + + let data_ptr = unsafe { &mut (*child.trailer.short).data }; + + unsafe { + std::ptr::copy_nonoverlapping(slice.as_ptr(), data_ptr.as_mut_ptr(), new_len); + } + + child + } else { + // IMPORTANT: Increase ref count + let heap_region = self.get_heap_region(); + heap_region.ref_count.fetch_add(1, Ordering::Release); + + let mut child = Self { + // SAFETY: self.data must be defined + // we cannot get a range larger than our own slice + // so we cannot be inlined while the requested slice is not inlinable + trailer: Trailer { + long: ManuallyDrop::new(LongRepr { + len, + prefix: [0; PREFIX_SIZE], + heap: unsafe { self.trailer.long.heap }, + offset: unsafe { self.trailer.long.offset } + begin as u32, + original_len: unsafe { self.trailer.long.original_len }, + }), + }, + }; + + let prefix = &self[begin..(begin + 4)]; + debug_assert_eq!(prefix.len(), 4); + + unsafe { + (*child.trailer.long).prefix.copy_from_slice(prefix); + } + + child + } + } + + /// Returns `true` if `needle` is a prefix of the slice or equal to the slice. + pub fn starts_with>(&self, needle: T) -> bool { + let needle = needle.as_ref(); + + unsafe { + let len = PREFIX_SIZE.min(needle.len()); + let needle_prefix: &[u8] = needle.get_unchecked(..len); + + if !self.prefix().starts_with(needle_prefix) { + return false; + } + } + + self.deref().starts_with(needle) + } + + /// Returns `true` if the slice is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the amount of bytes in the slice. + #[must_use] + pub fn len(&self) -> usize { + unsafe { self.trailer.short.len as usize } + } + + pub(crate) fn get_mut_slice(&mut self) -> &mut [u8] { + let len = self.len(); + + if self.is_inline() { + unsafe { std::slice::from_raw_parts_mut((*self.trailer.short).data.as_mut_ptr(), len) } + } else { + unsafe { std::slice::from_raw_parts_mut(self.data_ptr_mut(), len) } + } + } + + fn get_short_slice(&self) -> &[u8] { + let len = self.len(); + + debug_assert!( + len <= INLINE_SIZE, + "cannot get short slice - slice is not inlined", + ); + + // SAFETY: Shall only be called if slice is inlined + unsafe { std::slice::from_raw_parts((*self.trailer.short).data.as_ptr(), len) } + } + + fn get_long_slice(&self) -> &[u8] { + let len = self.len(); + + debug_assert!( + len > INLINE_SIZE, + "cannot get long slice - slice is inlined" + ); + + // SAFETY: Shall only be called if slice is heap allocated + unsafe { std::slice::from_raw_parts(self.data_ptr(), len) } + } +} + +impl std::borrow::Borrow<[u8]> for ByteView { + fn borrow(&self) -> &[u8] { + self + } +} + +impl AsRef<[u8]> for ByteView { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl FromIterator for ByteView { + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Self::from(iter.into_iter().collect::>()) + } +} + +impl From<&[u8]> for ByteView { + fn from(value: &[u8]) -> Self { + Self::new(value) + } +} + +impl From> for ByteView { + fn from(value: Arc<[u8]>) -> Self { + Self::new(&value) + } +} + +impl From> for ByteView { + fn from(value: Vec) -> Self { + Self::new(&value) + } +} + +impl From<&str> for ByteView { + fn from(value: &str) -> Self { + Self::from(value.as_bytes()) + } +} + +impl From for ByteView { + fn from(value: String) -> Self { + Self::from(value.as_bytes()) + } +} + +impl From> for ByteView { + fn from(value: Arc) -> Self { + Self::from(&*value) + } +} + +impl From<[u8; N]> for ByteView { + fn from(value: [u8; N]) -> Self { + Self::from(value.as_slice()) + } +} + +impl From<&[u8; N]> for ByteView { + fn from(value: &[u8; N]) -> Self { + Self::from(value.as_slice()) + } +} + +#[cfg(feature = "serde")] +mod serde { + use super::ByteView; + use serde::de::{self, Visitor}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt; + + impl Serialize for ByteView { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(self) + } + } + + impl<'de> Deserialize<'de> for ByteView { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ByteViewVisitor; + + impl<'de> Visitor<'de> for ByteViewVisitor { + type Value = ByteView; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + Ok(ByteView::new(v)) + } + + fn visit_seq(self, seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let bytes: Vec = + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))?; + + Ok(ByteView::new(&bytes)) + } + } + + deserializer.deserialize_bytes(ByteViewVisitor) + } + } +} + +#[cfg(test)] +mod tests { + use super::{ByteView, HeapAllocationHeader}; + use std::io::Cursor; + + #[test] + #[cfg(target_pointer_width = "64")] + fn memsize() { + use crate::byteview::{LongRepr, ShortRepr, Trailer}; + + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() + ); + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() + ); + + assert_eq!(24, std::mem::size_of::()); + assert_eq!( + 32, + std::mem::size_of::() + std::mem::size_of::() + ); + } + + #[test] + fn sliced_clone() { + let s = ByteView::from([ + 1, 255, 255, 255, 251, 255, 255, 255, 255, 255, 1, 21, 255, 255, 255, 255, 5, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 4, 3, 255, + 255, 0, 0, 255, 0, 0, 0, 254, 2, 0, 0, 0, 5, 2, 42, 0, 0, 0, 1, 0, 0, 0, 44, 0, 0, 0, + 2, 0, 0, 0, + ]); + let slice = s.slice(12..(12 + 21)); + + #[allow(clippy::redundant_clone)] + let cloned = slice.clone(); + + assert_eq!(slice.prefix(), cloned.prefix()); + assert_eq!(slice, cloned); + } + + #[test] + fn sized_slice_ref() { + let b = b"hello"; + let _bytes = ByteView::from(b); + } + + #[test] + fn fuse_empty() { + let bytes = ByteView::fused(&[], &[]); + assert_eq!(&*bytes, &[] as &[u8]); + } + + #[test] + fn fuse_one() { + let bytes = ByteView::fused(b"abc", &[]); + assert_eq!(&*bytes, b"abc"); + } + + #[test] + fn fuse_two() { + let bytes = ByteView::fused(b"abc", b"def"); + assert_eq!(&*bytes, b"abcdef"); + } + + #[test] + fn empty_slice() { + let bytes = ByteView::with_size_zeroed(0); + assert_eq!(&*bytes, &[] as &[u8]); + } + + #[test] + fn dealloc_order() { + let bytes = ByteView::new(&(0..32).collect::>()); + let bytes_slice = bytes.slice(..31); + drop(bytes); + drop(bytes_slice); + } + + #[test] + fn dealloc_order_2() { + let bytes = ByteView::new(&(0..32).collect::>()); + let bytes_slice = bytes.slice(..31); + let bytes_slice_2 = bytes.slice(..5); + let bytes_slice_3 = bytes.slice(..6); + + drop(bytes); + drop(bytes_slice); + drop(bytes_slice_2); + drop(bytes_slice_3); + } + + #[test] + fn from_reader_1() -> std::io::Result<()> { + let str = b"abcdef"; + let mut cursor = Cursor::new(str); + + let a = ByteView::from_reader(&mut cursor, 6)?; + assert!(&*a == b"abcdef"); + + Ok(()) + } + + #[test] + fn cmp_misc_1() { + let a = ByteView::from("abcdef"); + let b = ByteView::from("abcdefhelloworldhelloworld"); + assert!(a < b); + } + + #[test] + fn get_mut() { + let mut slice = ByteView::with_size(4); + assert_eq!(4, slice.len()); + assert_eq!([0, 0, 0, 0], &*slice); + + { + let mut mutator = slice.get_mut().unwrap(); + mutator[0] = 1; + mutator[1] = 2; + mutator[2] = 3; + mutator[3] = 4; + } + + assert_eq!(4, slice.len()); + assert_eq!([1, 2, 3, 4], &*slice); + assert_eq!([1, 2, 3, 4], slice.prefix()); + } + + #[test] + fn get_mut_long() { + let mut slice = ByteView::with_size(30); + assert_eq!(30, slice.len()); + assert_eq!([0; 30], &*slice); + + { + let mut mutator = slice.get_mut().unwrap(); + mutator[0] = 1; + mutator[1] = 2; + mutator[2] = 3; + mutator[3] = 4; + } + + assert_eq!(30, slice.len()); + assert_eq!( + [ + 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ], + &*slice + ); + assert_eq!([1, 2, 3, 4], slice.prefix()); + } + + #[test] + fn nostr() { + let slice = ByteView::from(""); + assert_eq!(0, slice.len()); + assert_eq!(&*slice, b""); + assert_eq!(1, slice.ref_count()); + assert!(slice.is_inline()); + } + + #[test] + fn default_str() { + let slice = ByteView::default(); + assert_eq!(0, slice.len()); + assert_eq!(&*slice, b""); + assert_eq!(1, slice.ref_count()); + assert!(slice.is_inline()); + } + + #[test] + fn short_str() { + let slice = ByteView::from("abcdef"); + assert_eq!(6, slice.len()); + assert_eq!(&*slice, b"abcdef"); + assert_eq!(1, slice.ref_count()); + assert_eq!(&slice.prefix(), b"abcd"); + assert!(slice.is_inline()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_str() { + let slice = ByteView::from("abcdefabcdef"); + assert_eq!(12, slice.len()); + assert_eq!(&*slice, b"abcdefabcdef"); + assert_eq!(1, slice.ref_count()); + assert_eq!(&slice.prefix(), b"abcd"); + assert!(slice.is_inline()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_long_str() { + let slice = ByteView::from("abcdefabcdefabcdabcd"); + assert_eq!(20, slice.len()); + assert_eq!(&*slice, b"abcdefabcdefabcdabcd"); + assert_eq!(1, slice.ref_count()); + assert_eq!(&slice.prefix(), b"abcd"); + assert!(slice.is_inline()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_str_clone() { + let slice = ByteView::from("abcdefabcdefabcdefab"); + let copy = slice.clone(); + assert_eq!(slice, copy); + assert_eq!(copy.prefix(), slice.prefix()); + + assert_eq!(1, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + } + + #[test] + fn long_str() { + let slice = ByteView::from("abcdefabcdefabcdefababcd"); + assert_eq!(24, slice.len()); + assert_eq!(&*slice, b"abcdefabcdefabcdefababcd"); + assert_eq!(1, slice.ref_count()); + assert_eq!(&slice.prefix(), b"abcd"); + assert!(!slice.is_inline()); + } + + #[test] + fn long_str_clone() { + let slice = ByteView::from("abcdefabcdefabcdefababcd"); + let copy = slice.clone(); + assert_eq!(slice, copy); + assert_eq!(copy.prefix(), slice.prefix()); + + assert_eq!(2, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + } + + #[test] + fn long_str_slice_full() { + let slice = ByteView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(..); + assert_eq!(copy, slice); + + assert_eq!(2, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice() { + let slice = ByteView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!(b"thisisalongstring", &*copy); + assert_eq!(©.prefix(), b"this"); + + assert_eq!(1, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice_twice() { + let slice = ByteView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!(b"thisisalongstring", &*copy); + + let copycopy = copy.slice(..); + assert_eq!(copy, copycopy); + + assert_eq!(1, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + + drop(slice); + assert_eq!(1, copycopy.ref_count()); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice_downgrade() { + let slice = ByteView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!(b"thisisalongstring", &*copy); + + let copycopy = copy.slice(0..4); + assert_eq!(b"this", &*copycopy); + + { + let copycopy = copy.slice(0..=4); + assert_eq!(b"thisi", &*copycopy); + assert_eq!(b't', *copycopy.first().unwrap()); + } + + assert_eq!(1, slice.ref_count()); + + drop(copy); + assert_eq!(1, slice.ref_count()); + + drop(copycopy); + assert_eq!(1, slice.ref_count()); + } + + #[test] + fn short_str_clone() { + let slice = ByteView::from("abcdef"); + let copy = slice.clone(); + assert_eq!(slice, copy); + + assert_eq!(1, slice.ref_count()); + + drop(slice); + assert_eq!(&*copy, b"abcdef"); + + assert_eq!(1, copy.ref_count()); + } + + #[test] + fn short_str_slice_full() { + let slice = ByteView::from("abcdef"); + let copy = slice.slice(..); + assert_eq!(slice, copy); + + assert_eq!(1, slice.ref_count()); + + drop(slice); + assert_eq!(&*copy, b"abcdef"); + + assert_eq!(1, copy.ref_count()); + } + + #[test] + fn short_str_slice_part() { + let slice = ByteView::from("abcdef"); + let copy = slice.slice(3..); + + assert_eq!(1, slice.ref_count()); + + drop(slice); + assert_eq!(&*copy, b"def"); + + assert_eq!(1, copy.ref_count()); + } + + #[test] + fn short_str_slice_empty() { + let slice = ByteView::from("abcdef"); + let copy = slice.slice(0..0); + + assert_eq!(1, slice.ref_count()); + + drop(slice); + assert_eq!(&*copy, b""); + + assert_eq!(1, copy.ref_count()); + } + + #[test] + fn tiny_str_starts_with() { + let a = ByteView::from("abc"); + assert!(a.starts_with(b"ab")); + assert!(!a.starts_with(b"b")); + } + + #[test] + fn long_str_starts_with() { + let a = ByteView::from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"); + assert!(a.starts_with(b"abcdef")); + assert!(!a.starts_with(b"def")); + } + + #[test] + fn tiny_str_cmp() { + let a = ByteView::from("abc"); + let b = ByteView::from("def"); + assert!(a < b); + } + + #[test] + fn tiny_str_eq() { + let a = ByteView::from("abc"); + let b = ByteView::from("def"); + assert!(a != b); + } + + #[test] + fn long_str_eq() { + let a = ByteView::from("abcdefabcdefabcdefabcdef"); + let b = ByteView::from("xycdefabcdefabcdefabcdef"); + assert!(a != b); + } + + #[test] + fn long_str_cmp() { + let a = ByteView::from("abcdefabcdefabcdefabcdef"); + let b = ByteView::from("xycdefabcdefabcdefabcdef"); + assert!(a < b); + } + + #[test] + fn long_str_eq_2() { + let a = ByteView::from("abcdefabcdefabcdefabcdef"); + let b = ByteView::from("abcdefabcdefabcdefabcdef"); + assert!(a == b); + } + + #[test] + fn long_str_cmp_2() { + let a = ByteView::from("abcdefabcdefabcdefabcdef"); + let b = ByteView::from("abcdefabcdefabcdefabcdeg"); + assert!(a < b); + } + + #[test] + fn long_str_cmp_3() { + let a = ByteView::from("abcdefabcdefabcdefabcde"); + let b = ByteView::from("abcdefabcdefabcdefabcdef"); + assert!(a < b); + } + + #[test] + fn cmp_fuzz_1() { + let a = ByteView::from([0]); + let b = ByteView::from([]); + + assert!(a > b); + assert!(a != b); + } + + #[test] + fn cmp_fuzz_2() { + let a = ByteView::from([0, 0]); + let b = ByteView::from([0]); + + assert!(a > b); + assert!(a != b); + } + + #[test] + fn cmp_fuzz_3() { + let a = ByteView::from([255, 255, 12, 255, 0]); + let b = ByteView::from([255, 255, 12, 255]); + + assert!(a > b); + assert!(a != b); + } + + #[test] + fn cmp_fuzz_4() { + let a = ByteView::from([ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ]); + let b = ByteView::from([ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, + ]); + + assert!(a > b); + assert!(a != b); + } +} diff --git a/crates/byteview/src/lib.rs b/crates/byteview/src/lib.rs new file mode 100644 index 000000000..4e2088911 --- /dev/null +++ b/crates/byteview/src/lib.rs @@ -0,0 +1,57 @@ +// 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) + +//! An immutable byte slice that may be inlined, and can be partially cloned without heap allocation. +//! +//! The length is limited to 2^32 bytes (4 GiB). +//! +//! ``` +//! # use byteview::ByteView; +//! let slice = ByteView::from("helloworld_thisisaverylongstring"); +//! +//! // No heap allocation - increases the ref count like an Arc<[u8]> +//! let full_copy = slice.clone(); +//! drop(full_copy); +//! +//! // No heap allocation - increases the ref count like an Arc<[u8]>, but we only get a subslice +//! let copy = slice.slice(11..); +//! assert_eq!(b"thisisaverylongstring", &*copy); +//! +//! // No heap allocation - if the slice is small enough, it will be inlined into the struct... +//! let copycopy = copy.slice(0..4); +//! assert_eq!(b"this", &*copycopy); +//! +//! // ...so no ref count incrementing is done +//! assert_eq!(2, slice.ref_count()); +//! +//! drop(copy); +//! assert_eq!(1, slice.ref_count()); +//! +//! drop(copycopy); +//! assert_eq!(1, slice.ref_count()); +//! +//! // Our original slice will be automatically freed if all slices vanish +//! drop(slice); +//! ``` + +#![deny(clippy::all, missing_docs, clippy::cargo)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::indexing_slicing)] +#![warn( + clippy::pedantic, + clippy::nursery, + clippy::expect_used, + clippy::unwrap_used, + clippy::indexing_slicing, + clippy::needless_lifetimes +)] + +mod builder; +mod byteview; +mod strview; + +pub use {byteview::ByteView, strview::StrView}; + +#[doc(hidden)] +pub use byteview::{Builder, Mutator}; diff --git a/crates/byteview/src/strview.rs b/crates/byteview/src/strview.rs new file mode 100644 index 000000000..d3652298a --- /dev/null +++ b/crates/byteview/src/strview.rs @@ -0,0 +1,429 @@ +// 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::ByteView; +use std::{ops::Deref, sync::Arc}; + +/// An immutable, UTF-8–encoded string slice +/// +/// Will be inlined (no pointer dereference or heap allocation) +/// if it is 20 characters or shorter (on a 64-bit system). +/// +/// A single heap allocation will be shared between multiple strings. +/// Even substrings of that heap allocation can be cloned without additional heap allocation. +/// +/// Uses [`ByteView`] internally, but derefs as [`&str`]. +#[repr(C)] +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct StrView(ByteView); + +impl std::fmt::Display for StrView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &**self) + } +} + +impl std::fmt::Debug for StrView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &**self) + } +} + +impl Deref for StrView { + type Target = str; + + fn deref(&self) -> &Self::Target { + // SAFETY: Constructor takes a &str + unsafe { std::str::from_utf8_unchecked(&self.0) } + } +} + +impl std::hash::Hash for StrView { + fn hash(&self, state: &mut H) { + self.deref().hash(state); + } +} + +impl StrView { + /// Creates a new string from an existing byte string. + /// + /// Will heap-allocate the string if it has at least length 13. + /// + /// # Panics + /// + /// Panics if the length does not fit in a u32 (4 GiB). + #[must_use] + pub fn new(s: &str) -> Self { + Self(ByteView::new(s.as_bytes())) + } + + #[doc(hidden)] + #[must_use] + #[allow(clippy::missing_const_for_fn)] + pub unsafe fn from_raw(view: ByteView) -> Self { + Self(view) + } + + /// Clones the contents of this string into an independently tracked string. + #[must_use] + pub fn to_detached(&self) -> Self { + Self::new(self) + } + + /// Clones the given range of the existing string without heap allocation. + #[must_use] + pub fn slice(&self, range: impl std::ops::RangeBounds) -> Self { + Self(self.0.slice(range)) + } + + /// Returns `true` if the string is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the amount of bytes in the string. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if `needle` is a prefix of the string or equal to the string. + #[must_use] + pub fn starts_with(&self, needle: &str) -> bool { + self.0.starts_with(needle.as_bytes()) + } +} + +impl std::borrow::Borrow for StrView { + fn borrow(&self) -> &str { + self + } +} + +impl AsRef for StrView { + fn as_ref(&self) -> &str { + self + } +} + +impl From<&str> for StrView { + fn from(value: &str) -> Self { + Self::new(value) + } +} + +impl From for StrView { + fn from(value: String) -> Self { + Self::new(&value) + } +} + +impl From> for StrView { + fn from(value: Arc) -> Self { + Self::new(&value) + } +} + +impl TryFrom for StrView { + type Error = std::str::Utf8Error; + + fn try_from(value: ByteView) -> Result { + std::str::from_utf8(&value)?; + Ok(Self(value)) + } +} + +impl From for ByteView { + fn from(val: StrView) -> Self { + val.0 + } +} + +#[cfg(feature = "serde")] +mod serde { + use super::StrView; + use serde::de::{self, Visitor}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt; + use std::ops::Deref; + + impl Serialize for StrView { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.deref()) + } + } + + impl<'de> Deserialize<'de> for StrView { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StrViewVisitor; + + impl<'de> Visitor<'de> for StrViewVisitor { + type Value = StrView; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(StrView::new(v)) + } + } + + deserializer.deserialize_str(StrViewVisitor) + } + } +} + +#[cfg(test)] +mod tests { + use super::StrView; + use std::collections::HashMap; + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let a = StrView::from("abcdef"); + let b: StrView = serde_json::from_slice(&serde_json::to_vec(&a).unwrap()).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn strview_hash() { + let a = StrView::from("abcdef"); + + let mut map = HashMap::new(); + map.insert(a, 0); + assert!(map.contains_key("abcdef")); + } + + #[test] + fn cmp_misc_1() { + let a = StrView::from("abcdef"); + let b = StrView::from("abcdefhelloworldhelloworld"); + assert!(a < b); + } + + #[test] + fn nostr() { + let slice = StrView::from(""); + assert_eq!(0, slice.len()); + assert_eq!(&*slice, ""); + } + + #[test] + fn default_str() { + let slice = StrView::default(); + assert_eq!(0, slice.len()); + assert_eq!(&*slice, ""); + } + + #[test] + fn short_str() { + let slice = StrView::from("abcdef"); + assert_eq!(6, slice.len()); + assert_eq!(&*slice, "abcdef"); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_str() { + let slice = StrView::from("abcdefabcdef"); + assert_eq!(12, slice.len()); + assert_eq!(&*slice, "abcdefabcdef"); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_long_str() { + let slice = StrView::from("abcdefabcdefabcdabcd"); + assert_eq!(20, slice.len()); + assert_eq!(&*slice, "abcdefabcdefabcdabcd"); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn medium_str_clone() { + let slice = StrView::from("abcdefabcdefabcdefa"); + + #[allow(clippy::redundant_clone)] + let copy = slice.clone(); + + assert_eq!(slice, copy); + } + + #[test] + fn long_str() { + let slice = StrView::from("abcdefabcdefabcdefababcd"); + assert_eq!(24, slice.len()); + assert_eq!(&*slice, "abcdefabcdefabcdefababcd"); + } + + #[test] + fn long_str_clone() { + let slice = StrView::from("abcdefabcdefabcdefababcd"); + + #[allow(clippy::redundant_clone)] + let copy = slice.clone(); + + assert_eq!(slice, copy); + } + + #[test] + fn long_str_slice_full() { + let slice = StrView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(..); + assert_eq!(copy, slice); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice() { + let slice = StrView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!("thisisalongstring", &*copy); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice_twice() { + let slice = StrView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!("thisisalongstring", &*copy); + + let copycopy = copy.slice(..); + assert_eq!(copy, copycopy); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn long_str_slice_downgrade() { + let slice = StrView::from("helloworld_thisisalongstring"); + + let copy = slice.slice(11..); + assert_eq!("thisisalongstring", &*copy); + + let copycopy = copy.slice(0..4); + assert_eq!("this", &*copycopy); + + { + let copycopy = copy.slice(0..=4); + assert_eq!("thisi", &*copycopy); + assert_eq!('t', copycopy.chars().next().unwrap()); + } + } + + #[test] + fn short_str_clone() { + let slice = StrView::from("abcdef"); + let copy = slice.clone(); + assert_eq!(slice, copy); + + drop(slice); + assert_eq!(&*copy, "abcdef"); + } + + #[test] + fn short_str_slice_full() { + let slice = StrView::from("abcdef"); + let copy = slice.slice(..); + assert_eq!(slice, copy); + + drop(slice); + assert_eq!(&*copy, "abcdef"); + } + + #[test] + fn short_str_slice_part() { + let slice = StrView::from("abcdef"); + let copy = slice.slice(3..); + + drop(slice); + assert_eq!(&*copy, "def"); + } + + #[test] + fn short_str_slice_empty() { + let slice = StrView::from("abcdef"); + let copy = slice.slice(0..0); + + drop(slice); + assert_eq!(&*copy, ""); + } + + #[test] + fn tiny_str_starts_with() { + let a = StrView::from("abc"); + assert!(a.starts_with("ab")); + assert!(!a.starts_with("b")); + } + + #[test] + fn long_str_starts_with() { + let a = StrView::from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"); + assert!(a.starts_with("abcdef")); + assert!(!a.starts_with("def")); + } + + #[test] + fn tiny_str_cmp() { + let a = StrView::from("abc"); + let b = StrView::from("def"); + assert!(a < b); + } + + #[test] + fn tiny_str_eq() { + let a = StrView::from("abc"); + let b = StrView::from("def"); + assert!(a != b); + } + + #[test] + fn long_str_eq() { + let a = StrView::from("abcdefabcdefabcdefabcdef"); + let b = StrView::from("xycdefabcdefabcdefabcdef"); + assert!(a != b); + } + + #[test] + fn long_str_cmp() { + let a = StrView::from("abcdefabcdefabcdefabcdef"); + let b = StrView::from("xycdefabcdefabcdefabcdef"); + assert!(a < b); + } + + #[test] + fn long_str_eq_2() { + let a = StrView::from("abcdefabcdefabcdefabcdef"); + let b = StrView::from("abcdefabcdefabcdefabcdef"); + assert!(a == b); + } + + #[test] + fn long_str_cmp_2() { + let a = StrView::from("abcdefabcdefabcdefabcdef"); + let b = StrView::from("abcdefabcdefabcdefabcdeg"); + assert!(a < b); + } + + #[test] + fn long_str_cmp_3() { + let a = StrView::from("abcdefabcdefabcdefabcde"); + let b = StrView::from("abcdefabcdefabcdefabcdef"); + assert!(a < b); + } +} diff --git a/crates/fjall/.config/nextest.toml b/crates/fjall/.config/nextest.toml deleted file mode 100644 index d7653a80a..000000000 --- a/crates/fjall/.config/nextest.toml +++ /dev/null @@ -1,2 +0,0 @@ -[profile.default] -slow-timeout = { period = "5s", terminate-after = 3 } diff --git a/crates/fjall/.gitignore b/crates/fjall/.gitignore deleted file mode 100644 index ee08ccef7..000000000 --- a/crates/fjall/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# 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 diff --git a/crates/fjall/.rustfmt.toml b/crates/fjall/.rustfmt.toml deleted file mode 100644 index 1c7741475..000000000 --- a/crates/fjall/.rustfmt.toml +++ /dev/null @@ -1,3 +0,0 @@ -reorder_imports = true -# group_imports = "StdExternalCrate" -# imports_granularity = "crate" diff --git a/crates/fjall/.vscode/settings.json b/crates/fjall/.vscode/settings.json deleted file mode 100644 index 875c86731..000000000 --- a/crates/fjall/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.showUnlinkedFileNotification": false -} \ No newline at end of file diff --git a/crates/fjall/CHANGELOG.md b/crates/fjall/CHANGELOG.md deleted file mode 100644 index cbb553a7c..000000000 --- a/crates/fjall/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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` diff --git a/crates/fjall/CONTRIBUTING.md b/crates/fjall/CONTRIBUTING.md deleted file mode 100644 index b2bba5915..000000000 --- a/crates/fjall/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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 diff --git a/crates/fjall/Cargo.toml b/crates/fjall/Cargo.toml index 1c445600c..1c99aea48 100644 --- a/crates/fjall/Cargo.toml +++ b/crates/fjall/Cargo.toml @@ -5,11 +5,8 @@ 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" @@ -18,9 +15,6 @@ 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" } @@ -28,15 +22,11 @@ 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"] } +dashmap = "6.2.1" +xxhash-rust = { version = "0.8.18", 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"] +nanoid = "0.5.0" +test-log = "0.2.21" diff --git a/crates/fjall/README.md b/crates/fjall/README.md deleted file mode 100644 index 0dbeeaa15..000000000 --- a/crates/fjall/README.md +++ /dev/null @@ -1,207 +0,0 @@ -

- -

- -
- -

- - Discord - - - Bluesky - -

- -*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 - - - - - - Orbitinghail - - - -## 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`, `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: . - -## 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. diff --git a/crates/fjall/commit.nu b/crates/fjall/commit.nu deleted file mode 100644 index a0a3001bd..000000000 --- a/crates/fjall/commit.nu +++ /dev/null @@ -1,64 +0,0 @@ -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) -} diff --git a/crates/fjall/compile_examples.mjs b/crates/fjall/compile_examples.mjs deleted file mode 100644 index 5812a12bc..000000000 --- a/crates/fjall/compile_examples.mjs +++ /dev/null @@ -1,60 +0,0 @@ -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`); -} diff --git a/crates/fjall/kawaii.png b/crates/fjall/kawaii.png deleted file mode 100644 index 7e6152a5d..000000000 Binary files a/crates/fjall/kawaii.png and /dev/null differ diff --git a/crates/fjall/logo.png b/crates/fjall/logo.png deleted file mode 100644 index 8693656da..000000000 Binary files a/crates/fjall/logo.png and /dev/null differ diff --git a/crates/fjall/renovate.json b/crates/fjall/renovate.json deleted file mode 100644 index 39a2b6e9a..000000000 --- a/crates/fjall/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ] -} diff --git a/crates/fjall/src/batch/item.rs b/crates/fjall/src/batch/item.rs index f0b64f91d..d4640295a 100644 --- a/crates/fjall/src/batch/item.rs +++ b/crates/fjall/src/batch/item.rs @@ -35,7 +35,6 @@ impl std::fmt::Debug for Item { ValueType::Value => "V", ValueType::Tombstone => "T", ValueType::WeakTombstone => "W", - ValueType::Indirection => "Vb", }, self.value ) diff --git a/crates/fjall/src/batch/mod.rs b/crates/fjall/src/batch/mod.rs index 9ee49757a..ae11723c0 100644 --- a/crates/fjall/src/batch/mod.rs +++ b/crates/fjall/src/batch/mod.rs @@ -148,7 +148,6 @@ impl WriteBatch { 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; diff --git a/crates/fjall/src/builder.rs b/crates/fjall/src/builder.rs index 6fc7a7cfa..f47e70841 100644 --- a/crates/fjall/src/builder.rs +++ b/crates/fjall/src/builder.rs @@ -2,21 +2,19 @@ // 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 crate::{Config, Database, db_config::CompactionFilterAssigner}; use lsm_tree::{Cache, CompressionType, DescriptorTable}; -use std::{marker::PhantomData, path::Path, sync::Arc}; +use std::{path::Path, sync::Arc}; /// Database builder -pub struct Builder { +pub struct Builder { inner: Config, - _phantom: PhantomData, } -impl Builder { +impl Builder { pub(crate) fn new(path: &Path) -> Self { Self { inner: Config::new(path), - _phantom: PhantomData, } } @@ -31,8 +29,8 @@ impl Builder { /// # Errors /// /// Errors if an I/O error occurred, or if the database can not be opened. - pub fn open(self) -> crate::Result { - O::open(self.inner) + pub fn open(self) -> crate::Result { + Database::open(self.inner) } /// Sets the cache capacity in bytes. diff --git a/crates/fjall/src/compaction/mod.rs b/crates/fjall/src/compaction/mod.rs index 8b716bbf8..bac568c28 100644 --- a/crates/fjall/src/compaction/mod.rs +++ b/crates/fjall/src/compaction/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod worker; -pub use lsm_tree::compaction::{Fifo, Leveled, Levelled}; +pub use lsm_tree::compaction::{Leveled, Levelled}; /// Compaction filter utilities pub mod filter { diff --git a/crates/fjall/src/compaction/worker.rs b/crates/fjall/src/compaction/worker.rs index 33b121cfa..92c66385a 100644 --- a/crates/fjall/src/compaction/worker.rs +++ b/crates/fjall/src/compaction/worker.rs @@ -2,7 +2,7 @@ // 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 crate::{Keyspace, snapshot_tracker::SnapshotTracker, stats::Stats}; use lsm_tree::AbstractTree; use std::time::Instant; @@ -23,7 +23,7 @@ pub fn run( keyspace.name, ); - let strategy = keyspace.config.compaction_strategy.clone(); + let strategy = std::sync::Arc::new(crate::compaction::Leveled::default()); stats.active_compaction_count.fetch_add(1, Relaxed); @@ -33,7 +33,7 @@ pub fn run( if let Err(e) = keyspace .tree - .compact(strategy.clone(), snapshot_tracker.get_seqno_safe_to_gc()) + .compact(strategy, snapshot_tracker.get_seqno_safe_to_gc()) { log::error!("Compaction failed: {e:?}"); stats.active_compaction_count.fetch_sub(1, Relaxed); diff --git a/crates/fjall/src/db.rs b/crates/fjall/src/db.rs index ecc1b8814..771abcdd7 100644 --- a/crates/fjall/src/db.rs +++ b/crates/fjall/src/db.rs @@ -3,12 +3,13 @@ // (found in the LICENSE-* files in the repository) use crate::{ + HashMap, Keyspace, KeyspaceCreateOptions, batch::WriteBatch, db_config::Config, - file::{fsync_directory, KEYSPACES_FOLDER, LOCK_FILE, VERSION_MARKER}, + file::{KEYSPACES_FOLDER, LOCK_FILE, VERSION_MARKER, fsync_directory}, flush::manager::FlushManager, - journal::{manager::JournalManager, writer::PersistMode, Journal}, - keyspace::{name::is_valid_keyspace_name, KeyspaceKey}, + journal::{Journal, manager::JournalManager, writer::PersistMode}, + keyspace::{KeyspaceKey, name::is_valid_keyspace_name}, locked_file::LockedFileGuard, meta_keyspace::MetaKeyspace, poison::{PoisonDart, PoisonSignal}, @@ -17,17 +18,15 @@ use crate::{ 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}, + sync::{Arc, RwLock, atomic::AtomicUsize}, }; pub type Keyspaces = HashMap; @@ -106,9 +105,6 @@ impl Drop for DatabaseInner { ); } } - - #[cfg(feature = "__internal_whitebox")] - crate::drop::decrement_drop_counter(); } } @@ -131,15 +127,6 @@ impl std::ops::Deref for Database { } } -impl Openable for Database { - fn open(config: Config) -> crate::Result - where - Self: Sized, - { - Self::open(config) - } -} - impl Database { /// Opens a cross-keyspace snapshot. /// @@ -152,7 +139,7 @@ impl Database { } /// Creates a new database builder to create or open a database at `path`. - pub fn builder(path: impl AsRef) -> crate::DatabaseBuilder { + pub fn builder(path: impl AsRef) -> crate::DatabaseBuilder { crate::DatabaseBuilder::new(path.as_ref()) } @@ -391,9 +378,6 @@ impl Database { let db = Self::create_or_recover(config)?; // db.start_background_threads()?; - #[cfg(feature = "__internal_whitebox")] - crate::drop::increment_drop_counter(); - Ok(db) } @@ -477,9 +461,6 @@ impl Database { self.meta_keyspace .create_keyspace(keyspace_id, &name, handle.clone(), keyspaces)?; - #[cfg(feature = "__internal_whitebox")] - crate::drop::increment_drop_counter(); - handle }) } @@ -549,12 +530,12 @@ impl Database { "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 { + if version as u8 > 5 { log::error!( "It looks like you are trying to open a database from the future. Are you a time traveller?" ); } - if version != FormatVersion::V4 { + if version != FormatVersion::V5 { return Err(crate::Error::InvalidVersion(Some(version))); } } else { @@ -718,9 +699,6 @@ impl Database { lsm_tree::ValueType::WeakTombstone => { tree.remove_weak(item.key, batch.seqno); } - lsm_tree::ValueType::Indirection => { - unreachable!() - } } } @@ -837,7 +815,7 @@ impl Database { // 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)?; + FormatVersion::V5.write_file_header(&mut marker)?; marker.sync_all()?; // IMPORTANT: fsync folders on Unix diff --git a/crates/fjall/src/db_test.rs b/crates/fjall/src/db_test.rs index 794111983..7429f9efb 100644 --- a/crates/fjall/src/db_test.rs +++ b/crates/fjall/src/db_test.rs @@ -1,4 +1,4 @@ -use crate::{Database, KeyspaceCreateOptions, KvSeparationOptions}; +use crate::{Database, KeyspaceCreateOptions}; use test_log::test; #[test_log::test] @@ -39,83 +39,6 @@ fn clear_recover_sealed() -> crate::Result<()> { 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()?; @@ -184,31 +107,6 @@ fn recover_sealed_order() -> crate::Result<()> { 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<()> { @@ -221,9 +119,7 @@ fn recover_sealed_pair_1() -> crate::Result<()> { 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())) + KeyspaceCreateOptions::default().max_memtable_size(1_000) })?; assert_eq!(i, tree.len()?.try_into().unwrap()); diff --git a/crates/fjall/src/drop.rs b/crates/fjall/src/drop.rs deleted file mode 100644 index d5473cf94..000000000 --- a/crates/fjall/src/drop.rs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 = 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) -} diff --git a/crates/fjall/src/error.rs b/crates/fjall/src/error.rs index b99a2c45e..39a6b1520 100644 --- a/crates/fjall/src/error.rs +++ b/crates/fjall/src/error.rs @@ -3,7 +3,7 @@ // (found in the LICENSE-* files in the repository) use crate::{ - journal::error::RecoveryError as JournalRecoveryError, version::FormatVersion, CompressionType, + CompressionType, journal::error::RecoveryError as JournalRecoveryError, version::FormatVersion, }; /// Errors that may occur in the storage engine diff --git a/crates/fjall/src/ingestion.rs b/crates/fjall/src/ingestion.rs index 6e658514a..a2b1a7bdd 100644 --- a/crates/fjall/src/ingestion.rs +++ b/crates/fjall/src/ingestion.rs @@ -2,17 +2,17 @@ // 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}; +use crate::{Keyspace, worker_pool::WorkerMessage}; +use lsm_tree::{Ingestion as TreeIngestion, UserKey, UserValue}; pub struct Ingestion<'a> { keyspace: &'a Keyspace, - inner: AnyIngestion<'a>, + inner: TreeIngestion<'a>, } impl<'a> Ingestion<'a> { pub fn new(keyspace: &'a Keyspace) -> crate::Result { - let inner = keyspace.tree.ingestion()?; + let inner = TreeIngestion::new(&keyspace.tree)?; Ok(Self { keyspace, inner }) } @@ -20,37 +20,20 @@ impl<'a> Ingestion<'a> { &mut self, key: K, value: V, - ) -> crate::Result<()> { - self.inner.write(key, value).map_err(Into::into) - } - - #[doc(hidden)] - pub fn write_prevalidated, V: Into>( - &mut self, - key: K, - value: V, ) -> crate::Result<()> { self.inner - .write_prevalidated(key, value) + .write(key.into(), value.into()) .map_err(Into::into) } pub fn write_tombstone>(&mut self, key: K) -> crate::Result<()> { - self.inner.write_tombstone(key).map_err(Into::into) + self.inner.write_tombstone(key.into()).map_err(Into::into) } #[doc(hidden)] pub fn write_weak_tombstone>(&mut self, key: K) -> crate::Result<()> { - self.inner.write_weak_tombstone(key).map_err(Into::into) - } - - #[doc(hidden)] - pub fn write_prevalidated_weak_tombstone>( - &mut self, - key: K, - ) -> crate::Result<()> { self.inner - .write_prevalidated_weak_tombstone(key) + .write_weak_tombstone(key.into()) .map_err(Into::into) } @@ -72,7 +55,7 @@ impl<'a> Ingestion<'a> { // insert seqno=1 let _journal_lock = self.keyspace.supervisor.journal.get_writer(); - self.finish_inner() + self.finish_inner(false) } /// Finishes the ingestion without taking the global journal writer lock. @@ -81,12 +64,17 @@ impl<'a> Ingestion<'a> { /// the same database. Exclusive ingestions into independent keyspaces may /// still finish concurrently. pub fn finish_exclusive(self) -> crate::Result<()> { - self.finish_inner() + self.finish_inner(true) } - fn finish_inner(self) -> crate::Result<()> { - self.inner - .finish() + fn finish_inner(self, exclusive: bool) -> crate::Result<()> { + let result = if exclusive { + self.inner.finish_exclusive() + } else { + self.inner.finish() + }; + + result .inspect(|()| { self.keyspace .worker_messager diff --git a/crates/fjall/src/iter.rs b/crates/fjall/src/iter.rs index 7d61102b5..3312b287d 100644 --- a/crates/fjall/src/iter.rs +++ b/crates/fjall/src/iter.rs @@ -2,7 +2,7 @@ // 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}; +use crate::{Guard, snapshot_nonce::SnapshotNonce}; type InnerIter = Box + Send + 'static>; diff --git a/crates/fjall/src/journal/batch_reader.rs b/crates/fjall/src/journal/batch_reader.rs index 4b858d8f4..4e606ad2b 100644 --- a/crates/fjall/src/journal/batch_reader.rs +++ b/crates/fjall/src/journal/batch_reader.rs @@ -3,7 +3,7 @@ // (found in the LICENSE-* files in the repository) use super::reader::JournalReader; -use crate::{journal::entry::Entry, keyspace::InternalKeyspaceId, JournalRecoveryError}; +use crate::{JournalRecoveryError, journal::entry::Entry, keyspace::InternalKeyspaceId}; use lsm_tree::{SeqNo, UserKey, UserValue, ValueType}; use std::{fs::OpenOptions, hash::Hasher}; @@ -63,7 +63,9 @@ impl JournalBatchReader { 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"); + 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)?; @@ -125,7 +127,9 @@ impl Iterator for JournalBatchReader { 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}"); + log::error!( + "Invalid batch: checksum check failed, expected: {expected_checksum}, got: {got_checksum}" + ); return Some(Err(JournalRecovery(JournalRecoveryError::ChecksumMismatch))); } diff --git a/crates/fjall/src/journal/entry.rs b/crates/fjall/src/journal/entry.rs index 4b7066c5b..ee45fe344 100644 --- a/crates/fjall/src/journal/entry.rs +++ b/crates/fjall/src/journal/entry.rs @@ -2,11 +2,11 @@ // 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 crate::{Slice, file::MAGIC_BYTES, keyspace::InternalKeyspaceId}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use lsm_tree::{ - coding::{Decode, Encode}, CompressionType, SeqNo, UserKey, UserValue, ValueType, + coding::{Decode, Encode}, }; use std::io::{Read, Write}; diff --git a/crates/fjall/src/journal/manager.rs b/crates/fjall/src/journal/manager.rs index 3fad1b533..0dad2385c 100644 --- a/crates/fjall/src/journal/manager.rs +++ b/crates/fjall/src/journal/manager.rs @@ -49,17 +49,11 @@ pub struct JournalManager { 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, diff --git a/crates/fjall/src/journal/mod.rs b/crates/fjall/src/journal/mod.rs index 7b656d079..7151538ef 100644 --- a/crates/fjall/src/journal/mod.rs +++ b/crates/fjall/src/journal/mod.rs @@ -18,7 +18,7 @@ use crate::file::fsync_directory; use batch_reader::JournalBatchReader; use lsm_tree::CompressionType; use reader::JournalReader; -use recovery::{recover_journals, RecoveryResult}; +use recovery::{RecoveryResult, recover_journals}; use std::{ path::{Path, PathBuf}, sync::{Mutex, MutexGuard}, @@ -53,9 +53,6 @@ impl Drop for Journal { log::error!("Flush error on drop: {e:?}"); } } - - #[cfg(feature = "__internal_whitebox")] - crate::drop::decrement_drop_counter(); } } @@ -92,9 +89,6 @@ impl Journal { // IMPORTANT: fsync folder on Unix fsync_directory(folder)?; - #[cfg(feature = "__internal_whitebox")] - crate::drop::increment_drop_counter(); - Ok(Self { writer: Mutex::new(writer), }) diff --git a/crates/fjall/src/journal/writer.rs b/crates/fjall/src/journal/writer.rs index 9cac11c52..bcff09cd6 100644 --- a/crates/fjall/src/journal/writer.rs +++ b/crates/fjall/src/journal/writer.rs @@ -2,7 +2,7 @@ // 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 super::entry::{Entry, serialize_marker_item}; use crate::{ batch::item::Item as BatchItem, file::fsync_directory, journal::recovery::JournalId, keyspace::InternalKeyspaceId, diff --git a/crates/fjall/src/keyspace/config/compression.rs b/crates/fjall/src/keyspace/config/compression.rs index c19149cca..3ec8d4b71 100644 --- a/crates/fjall/src/keyspace/config/compression.rs +++ b/crates/fjall/src/keyspace/config/compression.rs @@ -5,8 +5,8 @@ use crate::keyspace::config::{DecodeConfig, EncodeConfig}; use byteorder::{ReadBytesExt, WriteBytesExt}; use lsm_tree::{ - coding::{Decode, Encode}, CompressionType, + coding::{Decode, Encode}, }; impl EncodeConfig for crate::config::CompressionPolicy { diff --git a/crates/fjall/src/keyspace/mod.rs b/crates/fjall/src/keyspace/mod.rs index c2573d336..1d572d7b6 100644 --- a/crates/fjall/src/keyspace/mod.rs +++ b/crates/fjall/src/keyspace/mod.rs @@ -11,6 +11,7 @@ mod write_delay; mod test; use crate::{ + Database, Guard, Iter, file::{KEYSPACES_FOLDER, LSM_CURRENT_VERSION_MARKER}, flush::Task as FlushTask, ingestion::Ingestion, @@ -18,14 +19,13 @@ use crate::{ poison::PoisonSignal, supervisor::Supervisor, worker_pool::WorkerMessage, - Database, Guard, Iter, }; -use lsm_tree::{AbstractTree, AnyTree, SeqNo, UserKey, UserValue}; +use lsm_tree::{AbstractTree, SeqNo, Tree, UserKey, UserValue}; use options::CreateOptions; use std::{ ops::RangeBounds, path::Path, - sync::{atomic::AtomicBool, Arc, MutexGuard}, + sync::{Arc, MutexGuard, atomic::AtomicBool}, time::Duration, }; use write_delay::perform_write_stall; @@ -50,7 +50,6 @@ pub fn apply_to_base_config( .index_block_pinning_policy(our_config.index_block_pinning_policy.clone()) .data_block_hash_ratio_policy(our_config.data_block_hash_ratio_policy.clone()) .expect_point_read_hits(our_config.expect_point_read_hits) - .with_kv_separation(our_config.kv_separation_opts.clone()) .index_block_partitioning_policy(our_config.index_block_partitioning_policy.clone()) .filter_block_partitioning_policy(our_config.filter_block_partitioning_policy.clone()) .filter_policy(our_config.filter_policy.clone()) @@ -78,7 +77,7 @@ pub struct KeyspaceInner { /// LSM-tree wrapper #[doc(hidden)] - pub tree: AnyTree, + pub tree: Tree, pub(crate) supervisor: Supervisor, @@ -132,9 +131,6 @@ impl Drop for KeyspaceInner { } } } - - #[cfg(feature = "__internal_whitebox")] - crate::drop::decrement_drop_counter(); } } @@ -183,14 +179,6 @@ impl std::hash::Hash for Keyspace { } impl Keyspace { - #[inline] - fn standard_tree(&self) -> &lsm_tree::Tree { - let lsm_tree::AnyTree::Standard(tree) = &self.tree else { - panic!("standard keyspace operation used with a blob keyspace"); - }; - tree - } - #[doc(hidden)] #[must_use] pub fn id(&self) -> InternalKeyspaceId { @@ -271,14 +259,6 @@ impl Keyspace { Ok(()) } - /// Returns the number of blob bytes on disk that are not referenced. - /// - /// These will be reclaimed over time by blob garbage collection automatically. - #[must_use] - pub fn fragmented_blob_bytes(&self) -> u64 { - self.tree.stale_blob_bytes() - } - #[doc(hidden)] #[must_use] pub fn sealed_memtable_count(&self) -> usize { @@ -304,7 +284,7 @@ impl Keyspace { pub(crate) fn from_database( keyspace_id: InternalKeyspaceId, db: &Database, - tree: AnyTree, + tree: Tree, name: KeyspaceKey, config: CreateOptions, ) -> Self { @@ -362,17 +342,6 @@ impl Keyspace { }))) } - /// Returns the metrics struct of the underlying LSM-tree. - /// - /// # Note - /// - /// This function is experimental and metric names may change in future releases. - #[cfg(feature = "metrics")] - #[doc(hidden)] - pub fn metrics(&self) -> &lsm_tree::Metrics { - &**self.tree.metrics() - } - /// Returns the underlying LSM-tree's path. #[must_use] pub fn path(&self) -> &Path { @@ -432,7 +401,7 @@ impl Keyspace { ) -> impl DoubleEndedIterator> + Send + 'static { let nonce = self.supervisor.snapshot_tracker.open(); let range = ..; - self.standard_tree() + self.tree .create_range::<&[u8], _>(&range, nonce.instant, None) .map(move |item| { let _keep_snapshot_alive = &nonce; @@ -472,7 +441,7 @@ impl Keyspace { range: R, ) -> impl DoubleEndedIterator> + Send + 'static { let nonce = self.supervisor.snapshot_tracker.open(); - self.standard_tree() + self.tree .create_range(&range, nonce.instant, None) .map(move |item| { let _keep_snapshot_alive = &nonce; @@ -512,7 +481,7 @@ impl Keyspace { prefix: K, ) -> impl DoubleEndedIterator> + Send + 'static { let nonce = self.supervisor.snapshot_tracker.open(); - self.standard_tree() + self.tree .create_prefix(prefix, nonce.instant, None) .map(move |item| { let _keep_snapshot_alive = &nonce; @@ -677,7 +646,7 @@ impl Keyspace { &self, key: K, ) -> crate::Result> { - Ok(self.standard_tree().get(key, SeqNo::MAX)?) + Ok(self.tree.get(key, SeqNo::MAX)?) } /// Retrieves the size of an item from the keyspace. @@ -753,12 +722,6 @@ impl Keyspace { self.tree.last_key_value(SeqNo::MAX, None).map(Guard) } - /// Returns `true` if the underlying LSM-tree is key-value-separated. - #[must_use] - pub fn is_kv_separated(&self) -> bool { - matches!(self.tree, crate::AnyTree::Blob(_)) - } - // NOTE: Used in tests #[doc(hidden)] pub fn rotate_memtable_and_wait(&self) -> crate::Result<()> { @@ -908,13 +871,6 @@ impl Keyspace { self.tree.table_count() } - /// Number of blob files in the LSM-tree. - #[doc(hidden)] - #[must_use] - pub fn blob_file_count(&self) -> usize { - self.tree.blob_file_count() - } - /// Performs major compaction, blocking the caller until it's done. /// /// # Errors diff --git a/crates/fjall/src/keyspace/options.rs b/crates/fjall/src/keyspace/options.rs index 1b8455aa3..e0ceabc6c 100644 --- a/crates/fjall/src/keyspace/options.rs +++ b/crates/fjall/src/keyspace/options.rs @@ -8,13 +8,11 @@ use crate::{ FilterPolicyEntry, HashRatioPolicy, PartitioningPolicy, PinningPolicy, RestartIntervalPolicy, }, - keyspace::{config::DecodeConfig, InternalKeyspaceId}, - meta_keyspace::{encode_config_key, MetaKeyspace}, + keyspace::{InternalKeyspaceId, config::DecodeConfig}, + meta_keyspace::{MetaKeyspace, encode_config_key}, }; use byteorder::ReadBytesExt; -use lsm_tree::{ - compaction::Factory as CompactionFilterFactory, CompressionType, KvPair, KvSeparationOptions, -}; +use lsm_tree::{CompressionType, KvPair, compaction::Factory as CompactionFilterFactory}; use std::sync::Arc; /// Options to configure a keyspace @@ -72,12 +70,6 @@ pub struct CreateOptions { pub(crate) manual_journal_persist: bool, - #[doc(hidden)] - pub compaction_strategy: Arc, - - #[doc(hidden)] - pub kv_separation_opts: Option, - pub(crate) compaction_filter_factory: Option>, } @@ -120,12 +112,6 @@ impl Default for CreateOptions { index_block_compression_policy: CompressionPolicy::all(CompressionType::None), - compaction_strategy: Arc::new( - crate::compaction::Leveled::default() - ), - - kv_separation_opts: None, - compaction_filter_factory: None, } } @@ -153,8 +139,6 @@ impl CreateOptions { keyspace_id: InternalKeyspaceId, meta_keyspace: &MetaKeyspace, ) -> crate::Result { - let blob = meta_keyspace.get_kv_for_config(keyspace_id, "blob")?; - let data_block_compression_policy = meta_keyspace .get_kv_for_config(keyspace_id, "data_block_compression_policy")? .expect("should exist"); @@ -221,117 +205,6 @@ impl CreateOptions { .expect("should exist"); let filter_policy = FilterPolicy::decode(&filter_policy)?; - let blob_opts = blob.map(|_| { - use byteorder::LE; - use lsm_tree::coding::Decode; - - let blob_age_cutoff = meta_keyspace - .get_kv_for_config(keyspace_id, "blob_age_cutoff")? - .expect("blob_age_cutoff should be defined"); - let blob_age_cutoff = (&mut &blob_age_cutoff[..]).read_f32::()?; - - let blob_compression = meta_keyspace - .get_kv_for_config(keyspace_id, "blob_compression")? - .expect("blob_compression should be defined"); - let blob_compression = CompressionType::decode_from(&mut &blob_compression[..])?; - - let file_target_size = meta_keyspace - .get_kv_for_config(keyspace_id, "blob_file_target_size")? - .expect("blob_file_target_size should be defined"); - let file_target_size = (&mut &file_target_size[..]).read_u64::()?; - - let separation_threshold = meta_keyspace - .get_kv_for_config(keyspace_id, "blob_separation_threshold")? - .expect("blob_separation_threshold should be defined"); - let separation_threshold = (&mut &separation_threshold[..]).read_u32::()?; - - let staleness_threshold = meta_keyspace - .get_kv_for_config(keyspace_id, "blob_staleness_threshold")? - .expect("blob_staleness_threshold should be defined"); - let staleness_threshold = (&mut &staleness_threshold[..]).read_f32::()?; - - Ok::<_, crate::Error>( - KvSeparationOptions::default() - .compression(blob_compression) - .file_target_size(file_target_size) - .separation_threshold(separation_threshold) - .staleness_threshold(staleness_threshold) - .age_cutoff(blob_age_cutoff), - ) - }); - - let compaction_strategy_name = meta_keyspace - .get_kv_for_config(keyspace_id, "compaction_strategy")? - .expect("compaction_strategy should be defined"); - - let compaction_strategy_name = std::str::from_utf8(&compaction_strategy_name) - .expect("compaction_strategy should be UTF-8"); - - let compaction_strategy = match compaction_strategy_name { - lsm_tree::compaction::LEVELED_COMPACTION_NAME => { - use byteorder::LE; - - let l0_threshold = meta_keyspace - .get_kv_for_config(keyspace_id, "leveled_l0_threshold")? - .expect("leveled_l0_threshold should be defined"); - let l0_threshold = (&mut &l0_threshold[..]).read_u8()?; - - let target_size = meta_keyspace - .get_kv_for_config(keyspace_id, "leveled_target_size")? - .expect("leveled_target_size should be defined"); - let target_size = (&mut &target_size[..]).read_u64::()?; - - let level_ratio_policy_bytes = meta_keyspace - .get_kv_for_config(keyspace_id, "leveled_level_ratio_policy")? - .expect("leveled_level_ratio_policy should be defined"); - let level_ratio_policy_bytes = &mut &level_ratio_policy_bytes[..]; - - let level_ratio_policy_len = level_ratio_policy_bytes.read_u8()?; - - let mut ratios = vec![]; - - for _ in 0..level_ratio_policy_len { - ratios.push(level_ratio_policy_bytes.read_f32::()?); - } - - Arc::new( - crate::compaction::Leveled::default() - .with_l0_threshold(l0_threshold) - .with_table_target_size(target_size) - .with_level_ratio_policy(ratios), - ) as Arc - } - lsm_tree::compaction::FIFO_COMPACTION_NAME => { - use byteorder::LE; - - let fifo_limit = meta_keyspace - .get_kv_for_config(keyspace_id, "fifo_limit")? - .expect("fifo_limit should be defined"); - let fifo_limit = (&mut &fifo_limit[..]).read_u64::()?; - - let has_ttl = meta_keyspace - .get_kv_for_config(keyspace_id, "fifo_ttl")? - .expect("fifo_ttl should be defined") - == [1]; - - let ttl_seconds = if has_ttl { - let fifo_ttl_seconds = meta_keyspace - .get_kv_for_config(keyspace_id, "fifo_ttl_seconds")? - .expect("fifo_ttl_seconds should be defined"); - let fifo_ttl_seconds = (&mut &fifo_ttl_seconds[..]).read_u64::()?; - - Some(fifo_ttl_seconds) - } else { - None - }; - - Arc::new(crate::compaction::Fifo::new(fifo_limit, ttl_seconds)) - } - name => { - panic!("Invalid/unsupported compaction strategy: {name:?}"); - } - }; - let manual_journal_persist = meta_keyspace .get_kv_for_config(keyspace_id, "manual_journal_persist")? .expect("should exist") @@ -368,10 +241,6 @@ impl CreateOptions { max_memtable_size, - compaction_strategy, - - kv_separation_opts: blob_opts.transpose()?, - compaction_filter_factory: None, }) } @@ -380,11 +249,7 @@ impl CreateOptions { pub(crate) fn encode_kvs(&self, keyspace_id: InternalKeyspaceId) -> Vec { use crate::keyspace::config::EncodeConfig; - let mut kvs = vec![ - { - let key = encode_config_key(keyspace_id, "compaction_strategy"); - (key, self.compaction_strategy.get_name().into()) - }, + let kvs = vec![ policy!( keyspace_id, "data_block_compression_policy", @@ -466,68 +331,9 @@ impl CreateOptions { }, ]; - match self.compaction_strategy.get_name() { - "LeveledCompaction" | "FifoCompaction" => { - kvs.extend( - self.compaction_strategy - .get_config() - .into_iter() - .map(|(k, v)| { - // TODO: this is a bit stupid right now because we depend on behaviour in lsm-tree - // we should probably make lsm-tree just return a String for the key - let k = std::str::from_utf8(&k) - .expect("compaction strategy should return UTF-8 key"); - - (encode_config_key(keyspace_id, k), v) - }), - ); - } - name => { - panic!("Invalid/unsupported compaction strategy: {name:?}"); - } - } - - if let Some(blob_opts) = &self.kv_separation_opts { - kvs.extend([ - { - let key = encode_config_key(keyspace_id, "blob"); - (key, [1u8].into()) - }, - { - let key = encode_config_key(keyspace_id, "blob_age_cutoff"); - (key, blob_opts.age_cutoff.to_le_bytes().into()) - }, - { - use lsm_tree::coding::Encode; - - let key = encode_config_key(keyspace_id, "blob_compression"); - (key, blob_opts.compression.encode_into_vec().into()) - }, - { - let key = encode_config_key(keyspace_id, "blob_file_target_size"); - (key, blob_opts.file_target_size.to_le_bytes().into()) - }, - { - let key = encode_config_key(keyspace_id, "blob_separation_threshold"); - (key, blob_opts.separation_threshold.to_le_bytes().into()) - }, - { - let key = encode_config_key(keyspace_id, "blob_staleness_threshold"); - (key, blob_opts.staleness_threshold.to_le_bytes().into()) - }, - ]); - } - kvs } - /// Toggles key-value separation. - #[must_use] - pub fn with_kv_separation(mut self, opts: Option) -> Self { - self.kv_separation_opts = opts; - self - } - /// Sets the restart interval inside data blocks. /// /// A higher restart interval saves space while increasing lookup times @@ -632,18 +438,6 @@ impl CreateOptions { self } - /// Sets the compaction strategy. - /// - /// Default = Leveled - #[must_use] - pub fn compaction_strategy( - mut self, - compaction_strategy: Arc, - ) -> Self { - self.compaction_strategy = compaction_strategy; - self - } - /// If `false`, writes will flush data to the operating system. /// /// Default = false @@ -704,23 +498,12 @@ mod tests { c.data_block_compression_policy, CompressionPolicy::disabled(), ); - assert_eq!(c.kv_separation_opts, None); - - c = c.with_kv_separation(KvSeparationOptions::default()); - assert_eq!( - c.kv_separation_opts.as_ref().unwrap().compression, - CompressionType::None, - ); c = c.data_block_compression_policy(CompressionPolicy::disabled()); assert_eq!( c.data_block_compression_policy, CompressionPolicy::disabled(), ); - assert_eq!( - c.kv_separation_opts.unwrap().compression, - CompressionType::None, - ); } #[test] @@ -734,16 +517,11 @@ mod tests { c.data_block_compression_policy, CompressionPolicy::new([Uncompressed, Uncompressed, Lz4]), ); - assert_eq!(c.kv_separation_opts, None); - - c = c.with_kv_separation(Some(KvSeparationOptions::default())); - assert_eq!(c.kv_separation_opts.as_ref().unwrap().compression, Lz4); c = c.data_block_compression_policy(CompressionPolicy::disabled()); assert_eq!( c.data_block_compression_policy, CompressionPolicy::disabled(), ); - assert_eq!(c.kv_separation_opts.as_ref().unwrap().compression, Lz4); } } diff --git a/crates/fjall/src/lib.rs b/crates/fjall/src/lib.rs index c241516c4..125fde69c 100644 --- a/crates/fjall/src/lib.rs +++ b/crates/fjall/src/lib.rs @@ -11,7 +11,6 @@ //! - 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) //! - Automatic background maintenance //! //! It is not: @@ -98,10 +97,6 @@ pub mod compaction; mod db_config; -#[cfg(feature = "__internal_whitebox")] -#[doc(hidden)] -pub mod drop; - mod db; #[cfg(test)] @@ -126,7 +121,6 @@ mod snapshot_nonce; mod snapshot_tracker; mod stats; mod supervisor; -mod tx; mod version; mod worker_pool; mod write_buffer_manager; @@ -152,27 +146,16 @@ pub use { guard::Guard, iter::Iter, journal::{error::RecoveryError as JournalRecoveryError, writer::PersistMode}, - keyspace::{options::CreateOptions as KeyspaceCreateOptions, Keyspace}, + keyspace::{Keyspace, options::CreateOptions as KeyspaceCreateOptions}, readable::Readable, snapshot::Snapshot, version::FormatVersion, }; -pub use tx::single_writer::{ - SingleWriterTxKeyspace, TxDatabase as SingleWriterTxDatabase, - WriteTransaction as SingleWriterWriteTx, -}; - -pub use tx::optimistic::{ - Conflict, OptimisticTxDatabase, OptimisticTxKeyspace, WriteTransaction as OptimisticWriteTx, -}; - #[doc(hidden)] -pub use lsm_tree::{AbstractTree, AnyTree, Error as LsmError, TreeType}; +pub use lsm_tree::{AbstractTree, Error as LsmError}; -pub use lsm_tree::{ - CompressionType, KvPair, KvSeparationOptions, SeqNo, Slice, UserKey, UserValue, -}; +pub use lsm_tree::{CompressionType, KvPair, SeqNo, Slice, UserKey, UserValue}; /// Utility functions pub mod util { diff --git a/crates/fjall/src/locked_file.rs b/crates/fjall/src/locked_file.rs index 34bcafafe..fa018f8b3 100644 --- a/crates/fjall/src/locked_file.rs +++ b/crates/fjall/src/locked_file.rs @@ -61,7 +61,9 @@ impl LockedFileGuard { if let Err(e) = file.try_lock() { match e { std::fs::TryLockError::Error(e) => { - log::error!("Failed to acquire database lock - if this is expected, you can try opening again (maybe wait a little)"); + log::error!( + "Failed to acquire database lock - if this is expected, you can try opening again (maybe wait a little)" + ); return Err(crate::Error::Io(e)); } std::fs::TryLockError::WouldBlock => { diff --git a/crates/fjall/src/meta_keyspace.rs b/crates/fjall/src/meta_keyspace.rs index 0a3e1cb2e..6bb30e366 100644 --- a/crates/fjall/src/meta_keyspace.rs +++ b/crates/fjall/src/meta_keyspace.rs @@ -2,13 +2,13 @@ // 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::Keyspaces, keyspace::InternalKeyspaceId, Keyspace}; +use crate::{Keyspace, db::Keyspaces, keyspace::InternalKeyspaceId}; use byteview::StrView; -use lsm_tree::{AbstractTree, AnyTree, SeqNo, SequenceNumberCounter, UserValue}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, Tree, UserValue}; use std::sync::{Arc, RwLock, RwLockWriteGuard}; pub fn encode_config_key(keyspace_id: InternalKeyspaceId, name: &str) -> crate::UserKey { - use byteorder::{WriteBytesExt, BE}; + use byteorder::{BE, WriteBytesExt}; use std::io::Write; #[expect(unsafe_code)] @@ -46,7 +46,7 @@ pub struct MetaKeyspace { /// /// We never commit data through the WAL but write tables directly using /// lsm-tree's ingestion API. - inner: AnyTree, + inner: Tree, /// Dictionary of all keyspaces #[doc(hidden)] @@ -58,7 +58,7 @@ pub struct MetaKeyspace { impl MetaKeyspace { pub(crate) fn new( - inner: AnyTree, + inner: Tree, keyspaces: Arc>, seqno_generator: SequenceNumberCounter, visible_seqno: SequenceNumberCounter, @@ -224,7 +224,7 @@ impl MetaKeyspace { #[cfg(test)] mod tests { - use crate::{Database, Guard, KeyspaceCreateOptions, Readable, SingleWriterTxDatabase}; + use crate::{Database, Guard, KeyspaceCreateOptions}; use test_log::test; const ITEM_COUNT: usize = 10; @@ -374,83 +374,6 @@ mod tests { Ok(()) } - #[test] - fn tx_keyspace_delete() -> crate::Result<()> { - let folder = tempfile::tempdir()?; - - let path; - - { - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - path = tree.path(); - - assert!(path.try_exists()?); - - for x in 0..ITEM_COUNT as u64 { - let key = x.to_be_bytes(); - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes())?; - } - - for x in 0..ITEM_COUNT as u64 { - let key: [u8; 8] = (x + ITEM_COUNT as u64).to_be_bytes(); - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes())?; - } - - assert_eq!(db.read_tx().len(&tree)?, ITEM_COUNT * 2); - assert_eq!( - db.read_tx().iter(&tree).flat_map(Guard::key).count(), - ITEM_COUNT * 2, - ); - assert_eq!( - db.read_tx().iter(&tree).rev().flat_map(Guard::key).count(), - ITEM_COUNT * 2, - ); - } - - for _ in 0..3 { - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - assert_eq!(db.read_tx().len(&tree)?, ITEM_COUNT * 2); - assert_eq!( - db.read_tx().iter(&tree).flat_map(Guard::key).count(), - ITEM_COUNT * 2, - ); - assert_eq!( - db.read_tx().iter(&tree).rev().flat_map(Guard::key).count(), - ITEM_COUNT * 2, - ); - - assert!(path.try_exists()?); - } - - { - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - { - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - assert!(path.try_exists()?); - - db.inner().delete_keyspace(tree.inner().clone())?; - } - - assert!(!path.try_exists()?); - } - - { - let _db = Database::builder(&folder).open()?; - assert!(!path.try_exists()?); - } - - Ok(()) - } - #[test] fn keyspace_delete_and_reopening_behavior() -> crate::Result<()> { let keyspace_name = "default"; @@ -473,9 +396,10 @@ mod tests { db.delete_keyspace(keyspace)?; assert!(!keyspace_exists(1)?); - assert!(db - .keyspace("default", KeyspaceCreateOptions::default) - .is_ok()); + assert!( + db.keyspace("default", KeyspaceCreateOptions::default) + .is_ok() + ); assert!(keyspace_exists(2)?); Ok(()) diff --git a/crates/fjall/src/poison.rs b/crates/fjall/src/poison.rs index 5744b6c63..7000815a4 100644 --- a/crates/fjall/src/poison.rs +++ b/crates/fjall/src/poison.rs @@ -2,7 +2,7 @@ // 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::AtomicBool, Arc}; +use std::sync::{Arc, atomic::AtomicBool}; #[derive(Clone, Debug, Default)] pub struct PoisonSignal(Arc); diff --git a/crates/fjall/src/recovery.rs b/crates/fjall/src/recovery.rs index 629f87661..b7a49e8e9 100644 --- a/crates/fjall/src/recovery.rs +++ b/crates/fjall/src/recovery.rs @@ -3,15 +3,15 @@ // (found in the LICENSE-* files in the repository) use crate::{ + Database, HashMap, Keyspace, file::{KEYSPACES_FOLDER, LSM_CURRENT_VERSION_MARKER}, journal::{ batch_reader::JournalBatchReader, manager::EvictionWatermark, reader::JournalReader, }, keyspace::{ - apply_to_base_config, options::CreateOptions as KeyspaceCreateOptions, InternalKeyspaceId, + InternalKeyspaceId, apply_to_base_config, options::CreateOptions as KeyspaceCreateOptions, }, meta_keyspace::MetaKeyspace, - Database, HashMap, Keyspace, }; use lsm_tree::AbstractTree; use std::path::PathBuf; @@ -177,9 +177,6 @@ pub fn recover_sealed_memtables( lsm_tree::ValueType::WeakTombstone => { tree.remove_weak(item.key, batch.seqno); } - lsm_tree::ValueType::Indirection => { - unreachable!() - } } } diff --git a/crates/fjall/src/snapshot.rs b/crates/fjall/src/snapshot.rs index 920bb328d..dd594bc80 100644 --- a/crates/fjall/src/snapshot.rs +++ b/crates/fjall/src/snapshot.rs @@ -2,7 +2,7 @@ // 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, Iter, Keyspace, Readable}; +use crate::{Guard, Iter, Keyspace, Readable, snapshot_nonce::SnapshotNonce}; use lsm_tree::{AbstractTree, SeqNo, UserValue}; use std::ops::RangeBounds; diff --git a/crates/fjall/src/snapshot_nonce.rs b/crates/fjall/src/snapshot_nonce.rs index b8e47ad0f..9a7263b2d 100644 --- a/crates/fjall/src/snapshot_nonce.rs +++ b/crates/fjall/src/snapshot_nonce.rs @@ -2,7 +2,7 @@ // 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, SeqNo}; +use crate::{SeqNo, snapshot_tracker::SnapshotTracker}; /// Holds a snapshot instant and automatically frees it from the snapshot tracker when dropped pub struct SnapshotNonce { diff --git a/crates/fjall/src/snapshot_tracker.rs b/crates/fjall/src/snapshot_tracker.rs index 3d68fec2c..c9aa8f6f3 100644 --- a/crates/fjall/src/snapshot_tracker.rs +++ b/crates/fjall/src/snapshot_tracker.rs @@ -2,10 +2,10 @@ // 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, SeqNo}; +use crate::{SeqNo, snapshot_nonce::SnapshotNonce}; use dashmap::DashMap; use lsm_tree::SequenceNumberCounter; -use std::sync::{atomic::AtomicU64, Arc, RwLock}; +use std::sync::{Arc, RwLock, atomic::AtomicU64}; /// Keeps track of open snapshots pub struct SnapshotTrackerInner { diff --git a/crates/fjall/src/supervisor.rs b/crates/fjall/src/supervisor.rs index d29f530a0..277d1b6fe 100644 --- a/crates/fjall/src/supervisor.rs +++ b/crates/fjall/src/supervisor.rs @@ -7,7 +7,7 @@ use lsm_tree::SequenceNumberCounter; use crate::{ db::Keyspaces, flush::manager::FlushManager, - journal::{manager::JournalManager, Journal}, + journal::{Journal, manager::JournalManager}, snapshot_tracker::SnapshotTracker, write_buffer_manager::WriteBufferManager, }; diff --git a/crates/fjall/src/tx/mod.rs b/crates/fjall/src/tx/mod.rs deleted file mode 100644 index 4a0c61dfa..000000000 --- a/crates/fjall/src/tx/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// 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 optimistic; -pub mod single_writer; -pub mod write_tx; diff --git a/crates/fjall/src/tx/optimistic/conflict_manager.rs b/crates/fjall/src/tx/optimistic/conflict_manager.rs deleted file mode 100644 index 992d52fce..000000000 --- a/crates/fjall/src/tx/optimistic/conflict_manager.rs +++ /dev/null @@ -1,197 +0,0 @@ -// 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::InternalKeyspaceId; -use core::ops::Bound; -use lsm_tree::Slice; -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::RangeBounds, - sync::Mutex, -}; - -#[derive(Clone, Debug)] -enum Read { - Single(Slice), - Range { - start: Bound, - end: Bound, - }, - All, -} - -#[derive(Default, Debug)] -pub struct ConflictManager { - reads: Mutex>>, - conflict_keys: Mutex>>, -} - -impl ConflictManager { - fn push_read(&self, keyspace_id: InternalKeyspaceId, read: Read) { - #[expect(clippy::expect_used)] - let mut lock = self.reads.lock().expect("lock is poisoned"); - - if let Some(tbl) = lock.get_mut(&keyspace_id) { - tbl.push(read); - } else { - lock.entry(keyspace_id).or_default().push(read); - } - } - - pub fn mark_read(&self, keyspace_id: InternalKeyspaceId, key: Slice) { - self.push_read(keyspace_id, Read::Single(key)); - } - - pub fn mark_conflict(&self, keyspace_id: InternalKeyspaceId, key: Slice) { - #[expect(clippy::expect_used)] - let mut lock = self.conflict_keys.lock().expect("lock is poisoned"); - - if let Some(tbl) = lock.get_mut(&keyspace_id) { - tbl.insert(key); - } else { - lock.entry(keyspace_id).or_default().insert(key); - } - } - - pub fn mark_range(&self, keyspace_id: InternalKeyspaceId, range: impl RangeBounds) { - let start = match range.start_bound() { - Bound::Included(k) => Bound::Included(k.clone()), - Bound::Excluded(k) => Bound::Excluded(k.clone()), - Bound::Unbounded => Bound::Unbounded, - }; - - let end = match range.end_bound() { - Bound::Included(k) => Bound::Included(k.clone()), - Bound::Excluded(k) => Bound::Excluded(k.clone()), - Bound::Unbounded => Bound::Unbounded, - }; - - let read = if start == Bound::Unbounded && end == Bound::Unbounded { - Read::All - } else { - Read::Range { start, end } - }; - - self.push_read(keyspace_id, read); - } - - #[expect(clippy::too_many_lines, clippy::significant_drop_tightening)] - pub fn has_conflict(&self, other: &Self) -> bool { - #[expect(clippy::expect_used)] - let reads_lock = self.reads.lock().expect("lock is poisoned"); - - #[expect(clippy::expect_used)] - let conflict_keys_lock = other.conflict_keys.lock().expect("lock is poisoned"); - - if reads_lock.is_empty() { - return false; - } - - for (keyspace_name, keys) in &*reads_lock { - if let Some(other_conflict_keys) = conflict_keys_lock.get(keyspace_name) { - for ro in keys { - match ro { - Read::Single(k) => { - if other_conflict_keys.contains(k) { - return true; - } - } - Read::Range { start, end } => match (start, end) { - (Bound::Included(start), Bound::Included(end)) => { - if other_conflict_keys - .range::(( - Bound::Included(start), - Bound::Included(end), - )) - .next() - .is_some() - { - return true; - } - } - (Bound::Included(start), Bound::Excluded(end)) => { - if other_conflict_keys - .range::(( - Bound::Included(start), - Bound::Excluded(end), - )) - .next() - .is_some() - { - return true; - } - } - (Bound::Included(start), Bound::Unbounded) => { - if other_conflict_keys - .range::((Bound::Included(start), Bound::Unbounded)) - .next() - .is_some() - { - return true; - } - } - (Bound::Excluded(start), Bound::Included(end)) => { - if other_conflict_keys - .range::(( - Bound::Excluded(start), - Bound::Included(end), - )) - .next() - .is_some() - { - return true; - } - } - (Bound::Excluded(start), Bound::Excluded(end)) => { - if other_conflict_keys - .range::(( - Bound::Excluded(start), - Bound::Excluded(end), - )) - .next() - .is_some() - { - return true; - } - } - (Bound::Excluded(start), Bound::Unbounded) => { - if other_conflict_keys - .range::((Bound::Excluded(start), Bound::Unbounded)) - .next() - .is_some() - { - return true; - } - } - (Bound::Unbounded, Bound::Included(end)) => { - let range = ..=end; - for write in other_conflict_keys { - if range.contains(&write) { - return true; - } - } - } - (Bound::Unbounded, Bound::Excluded(end)) => { - let range = ..end; - for write in other_conflict_keys { - if range.contains(&write) { - return true; - } - } - } - (Bound::Unbounded, Bound::Unbounded) => unreachable!(), - }, - Read::All => { - if !other_conflict_keys.is_empty() { - return true; - } - } - } - } - } - } - - false - } -} diff --git a/crates/fjall/src/tx/optimistic/keyspace.rs b/crates/fjall/src/tx/optimistic/keyspace.rs deleted file mode 100644 index 3e9d003a3..000000000 --- a/crates/fjall/src/tx/optimistic/keyspace.rs +++ /dev/null @@ -1,502 +0,0 @@ -// 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::{tx::optimistic::OptimisticTxDatabase, Keyspace}; -use crate::{Guard, Readable}; -use lsm_tree::{UserKey, UserValue}; -use std::path::PathBuf; - -/// Handle to a keyspace of a transactional database -#[derive(Clone)] -pub struct OptimisticTxKeyspace { - pub(crate) inner: Keyspace, - pub(crate) db: OptimisticTxDatabase, -} - -impl AsRef for OptimisticTxKeyspace { - fn as_ref(&self) -> &Keyspace { - self.inner() - } -} - -impl OptimisticTxKeyspace { - /// Returns the underlying LSM-tree's path. - #[must_use] - pub fn path(&self) -> PathBuf { - self.inner.path().into() - } - - /// Approximates the number of items in the keyspace. - /// - /// For update- or delete-heavy workloads, this value will - /// diverge from the real value, but is a O(1) operation. - /// - /// For insert-only workloads (e.g. logs, time series) - /// this value is reliable. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// assert_eq!(tree.approximate_len(), 0); - /// - /// tree.insert("1", "abc")?; - /// assert_eq!(tree.approximate_len(), 1); - /// - /// tree.remove("1")?; - /// // Oops! approximate_len will not be reliable here - /// assert_eq!(tree.approximate_len(), 2); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn approximate_len(&self) -> usize { - self.inner.approximate_len() - } - - /// Removes an item and returns its value if it existed. - /// - /// The operation will run wrapped in a transaction. - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let taken = tree.take("a")?.unwrap(); - /// assert_eq!(b"abc", &*taken); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn take>(&self, key: K) -> crate::Result> { - self.fetch_update(key, |_| None) - } - - /// Atomically updates an item and returns the previous value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Note - /// - /// The provided closure can be called multiple times as this function - /// automatically retries on conflict. Since this is an `FnMut`, make sure - /// it is idempotent and will not cause side-effects. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, Slice, KeyspaceCreateOptions}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let prev = tree.fetch_update("a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let prev = tree.fetch_update("a", |_| None)?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn fetch_update, F: FnMut(Option<&UserValue>) -> Option>( - &self, - key: K, - mut f: F, - ) -> crate::Result> { - let key = key.into(); - - loop { - let mut tx = self.db.write_tx()?; - let prev = tx.fetch_update(self.inner(), key.clone(), &mut f)?; - if tx.commit()?.is_ok() { - return Ok(prev); - } - } - } - - /// Atomically updates an item and returns the new value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Note - /// - /// The provided closure can be called multiple times as this function - /// automatically retries on conflict. Since this is an `FnMut`, make sure - /// it is idempotent and will not cause side-effects. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, Slice, KeyspaceCreateOptions}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let updated = tree.update_fetch("a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"def", &*updated); - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let updated = tree.update_fetch("a", |_| None)?; - /// assert!(updated.is_none()); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn update_fetch, F: FnMut(Option<&UserValue>) -> Option>( - &self, - key: K, - mut f: F, - ) -> crate::Result> { - let key = key.into(); - - loop { - let mut tx = self.db.write_tx()?; - let updated = tx.update_fetch(self.inner(), key.clone(), &mut f)?; - if tx.commit()?.is_ok() { - return Ok(updated); - } - } - } - - /// Inserts a key-value pair into the keyspace. - /// - /// Keys may be up to 65536 bytes long, values up to 2^32 bytes. - /// Shorter keys and values result in better performance. - /// - /// If the key already exists, the item will be overwritten. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// assert!(!db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn insert, V: Into>( - &self, - key: K, - value: V, - ) -> crate::Result<()> { - let mut tx = self.db.write_tx()?; - tx.insert(self.inner(), key, value); - - #[expect( - clippy::expect_used, - clippy::missing_panics_doc, - reason = "blind insert should not conflict ever" - )] - tx.commit()?.expect("blind insert should not conflict ever"); - - Ok(()) - } - - /// Removes an item from the keyspace. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// assert!(!db.read_tx().is_empty(&tree)?); - /// - /// tree.remove("a")?; - /// assert!(db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn remove>(&self, key: K) -> crate::Result<()> { - let mut tx = self.db.write_tx()?; - tx.remove(self.inner(), key); - - #[expect( - clippy::expect_used, - clippy::missing_panics_doc, - reason = "blind remove should not conflict ever" - )] - tx.commit()?.expect("blind remove should not conflict ever"); - - Ok(()) - } - - /// Removes an item from the keyspace, leaving behind a weak tombstone. - /// - /// 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. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Experimental - /// - /// This function is currently experimental. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// assert!(!db.read_tx().is_empty(&tree)?); - /// - /// tree.remove_weak("a")?; - /// assert!(db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - #[doc(hidden)] - pub fn remove_weak>(&self, key: K) -> crate::Result<()> { - let mut tx = self.db.write_tx()?; - tx.remove_weak(self.inner(), key); - - #[expect(clippy::expect_used)] - tx.commit()?.expect("blind remove should not conflict ever"); - - Ok(()) - } - - /// Retrieves an item from the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("my_value".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn get>(&self, key: K) -> crate::Result> { - self.inner.get(key) - } - - /// Retrieves the size of an item from the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// let len = tree.size_of("a")?.unwrap_or_default(); - /// assert_eq!("my_value".len() as u32, len); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn size_of>(&self, key: K) -> crate::Result> { - self.inner.size_of(key) - } - - /// Returns the first key-value pair in the keyspace. - /// The key in this pair is the minimum key in the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// tree.insert("b", "my_value")?; - /// - /// assert_eq!(b"a", &*tree.first_key_value().unwrap().key()?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn first_key_value(&self) -> Option { - let read_tx = self.db.read_tx(); - read_tx.first_key_value(self) - } - - /// Returns the last key-value pair in the keyspace. - /// The key in this pair is the maximum key in the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// tree.insert("b", "my_value")?; - /// - /// assert_eq!(b"b", &*tree.last_key_value().unwrap().key()?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn last_key_value(&self) -> Option { - let read_tx = self.db.read_tx(); - read_tx.last_key_value(self) - } - - /// Returns `true` if the keyspace contains the specified key. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// assert!(tree.contains_key("a")?); - /// assert!(!tree.contains_key("b")?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn contains_key>(&self, key: K) -> crate::Result { - self.inner.contains_key(key) - } - - /// Allows access to the inner keyspace handle, allowing to - /// escape from the transactional context. - #[doc(hidden)] - #[must_use] - pub fn inner(&self) -> &Keyspace { - &self.inner - } -} diff --git a/crates/fjall/src/tx/optimistic/mod.rs b/crates/fjall/src/tx/optimistic/mod.rs deleted file mode 100644 index 1dca25162..000000000 --- a/crates/fjall/src/tx/optimistic/mod.rs +++ /dev/null @@ -1,184 +0,0 @@ -// 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 conflict_manager; -mod keyspace; -mod oracle; -mod write_tx; - -use crate::{ - keyspace::KeyspaceKey, - tx::{optimistic::oracle::Oracle, single_writer::Openable}, - Config, Database, KeyspaceCreateOptions, PersistMode, Snapshot, -}; -use std::{ - path::Path, - sync::{Arc, Mutex}, -}; - -pub use keyspace::OptimisticTxKeyspace; -pub use write_tx::{Conflict, WriteTransaction}; - -/// Transactional database -#[derive(Clone)] -pub struct OptimisticTxDatabase { - pub(crate) inner: Database, - pub(super) oracle: Arc, -} - -impl Openable for OptimisticTxDatabase { - fn open(config: Config) -> crate::Result - where - Self: Sized, - { - let inner = Database::create_or_recover(config)?; - // inner.start_background_threads()?; - - Ok(Self { - oracle: Arc::new(Oracle { - write_serialize_lock: Mutex::default(), - snapshot_tracker: inner.supervisor.snapshot_tracker.clone(), - }), - inner, - }) - } -} - -impl OptimisticTxDatabase { - /// Creates a new database builder to create or open a database at `path`. - pub fn builder(path: impl AsRef) -> crate::DatabaseBuilder { - crate::DatabaseBuilder::new(path.as_ref()) - } - - #[doc(hidden)] - #[must_use] - pub fn inner(&self) -> &Database { - &self.inner - } - - /// Starts a new writeable transaction. - /// - /// # Errors - /// - /// Will return `Err` if creation failed. - pub fn write_tx(&self) -> crate::Result { - let snapshot = { - // acquire a lock here to prevent getting a stale snapshot seqno - // this will drain at least part of the commit queue, but ordering - // is platform-dependent since we use std::sync::Mutex - let _guard = self.oracle.write_serialize_lock()?; - - self.inner.supervisor.snapshot_tracker.open() - }; - - let mut write_tx = - WriteTransaction::new(self.inner().clone(), snapshot, self.oracle.clone()); - - if !self.inner.config.manual_journal_persist { - write_tx = write_tx.durability(Some(PersistMode::Buffer)); - } - - Ok(write_tx) - } - - /// Starts a new read-only transaction (a.k.a. [`Snapshot`]). - #[must_use] - pub fn read_tx(&self) -> Snapshot { - self.inner.snapshot() - } - - /// 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, OptimisticTxDatabase, KeyspaceCreateOptions}; - /// # let folder = tempfile::tempdir()?; - /// let db = OptimisticTxDatabase::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<()> { - self.inner.persist(mode) - } - - /// 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 { - let keyspace = self.inner.keyspace(name, create_options)?; - - Ok(OptimisticTxKeyspace { - inner: keyspace, - db: self.clone(), - }) - } - - /// Returns the number of keyspaces. - #[must_use] - pub fn keyspace_count(&self) -> usize { - self.inner.keyspace_count() - } - - /// Gets a list of all keyspace names in the database. - #[must_use] - pub fn list_keyspace_names(&self) -> Vec { - self.inner.list_keyspace_names() - } - - /// Returns `true` if the keyspace with the given name exists. - #[must_use] - pub fn keyspace_exists(&self, name: &str) -> bool { - self.inner.keyspace_exists(name) - } - - /// Returns the current write buffer size (active + sealed memtables). - #[must_use] - pub fn write_buffer_size(&self) -> u64 { - self.inner.write_buffer_size() - } - - /// Returns the number of journal fragments on disk. - #[must_use] - pub fn journal_count(&self) -> usize { - self.inner.journal_count() - } - - /// Returns the disk space usage of the entire database. - /// - /// # Errors - /// - /// Returns error, if an IO error occurred. - pub fn disk_space(&self) -> crate::Result { - self.inner.disk_space() - } -} diff --git a/crates/fjall/src/tx/optimistic/oracle.rs b/crates/fjall/src/tx/optimistic/oracle.rs deleted file mode 100644 index 5b423a6c1..000000000 --- a/crates/fjall/src/tx/optimistic/oracle.rs +++ /dev/null @@ -1,121 +0,0 @@ -// 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::conflict_manager::ConflictManager; -use crate::snapshot_tracker::SnapshotTracker; -use crate::SeqNo; -use std::collections::BTreeMap; -use std::sync::{Mutex, MutexGuard}; - -pub enum CommitOutcome { - Ok, - Aborted(E), - Conflicted, -} - -pub struct Oracle { - pub(super) write_serialize_lock: Mutex>, - pub(super) snapshot_tracker: SnapshotTracker, -} - -impl Oracle { - pub(super) fn with_commit Result<(), E>>( - &self, - instant: SeqNo, - conflict_checker: ConflictManager, - f: F, - ) -> crate::Result> { - let mut committed_txns = self - .write_serialize_lock - .lock() - .map_err(|_| crate::Error::Poisoned)?; - - // If the committed_txn.ts is less than `SeqNo` that implies that the - // committed_txn finished before the current transaction started. - // We don't need to check for conflict in that case. - // This change assumes linearizability. Lack of linearizability could - // cause the read ts of a new txn to be lower than the commit ts of - // a txn before it. - let conflicted = - committed_txns - .range((instant + 1)..) - .any(|(_ts, other_conflict_checker)| { - conflict_checker.has_conflict(other_conflict_checker) - }); - - self.snapshot_tracker.close_raw(instant); - - // TODO: This can be expensive and should probably be done in a background worker, or a after a memtable rotation - let safe_to_gc = self.snapshot_tracker.get_seqno_safe_to_gc(); - committed_txns.retain(|ts, _| *ts > safe_to_gc); - - if conflicted { - return Ok(CommitOutcome::Conflicted); - } - - if let Err(e) = f() { - return Ok(CommitOutcome::Aborted(e)); - } - - committed_txns.insert(self.snapshot_tracker.get(), conflict_checker); - - Ok(CommitOutcome::Ok) - } - - pub(super) fn write_serialize_lock( - &self, - ) -> crate::Result>> { - self.write_serialize_lock - .lock() - .map_err(|_| crate::Error::Poisoned) - } -} - -#[cfg(test)] -mod tests { - use crate::{KeyspaceCreateOptions, OptimisticTxDatabase, OptimisticTxKeyspace, Readable}; - - #[expect(clippy::significant_drop_tightening)] - fn run_tx( - db: &OptimisticTxDatabase, - tree: &OptimisticTxKeyspace, - ) -> Result<(), Box> { - let mut tx1 = db.write_tx()?; - let mut tx2 = db.write_tx()?; - tx1.insert(tree.inner(), "hello", "world"); - - tx1.commit()??; - assert!(tree.contains_key("hello")?); - - _ = tx2.get(tree.inner(), "hello")?; - - tx2.insert(tree.inner(), "hello", "world2"); - assert!(tx2.commit()?.is_err()); // intended to conflict - - Ok(()) - } - - #[expect(clippy::unwrap_used)] - #[test] - fn oracle_committed_txns_does_not_leak() -> crate::Result<()> { - let tmpdir = tempfile::tempdir()?; - let db = OptimisticTxDatabase::builder(tmpdir.path()).open()?; - - let part = db.keyspace("foo", KeyspaceCreateOptions::default)?; - - for _ in 0..10_000 { - run_tx(&db, &part).unwrap(); - } - - assert!(dbg!(db.oracle.write_serialize_lock.lock().unwrap().len()) < 10_000); - - for _ in 0..10_000 { - run_tx(&db, &part).unwrap(); - } - - assert!(dbg!(db.oracle.write_serialize_lock.lock().unwrap().len()) < 10_000); - - Ok(()) - } -} diff --git a/crates/fjall/src/tx/optimistic/write_tx.rs b/crates/fjall/src/tx/optimistic/write_tx.rs deleted file mode 100644 index e4cfc2dfc..000000000 --- a/crates/fjall/src/tx/optimistic/write_tx.rs +++ /dev/null @@ -1,891 +0,0 @@ -// 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, - tx::{ - optimistic::{ - conflict_manager::ConflictManager, - oracle::{CommitOutcome, Oracle}, - }, - write_tx::BaseTransaction, - }, - Database, Guard, Iter, Keyspace, PersistMode, Readable, -}; -use lsm_tree::{Slice, UserKey, UserValue}; -use std::{ - fmt, - ops::{Bound, RangeBounds, RangeFull}, - sync::Arc, -}; - -/// Transaction conflict -/// -/// SSI transactions can conflict which require them to be rerun. -#[derive(Debug)] -pub struct Conflict; - -impl std::error::Error for Conflict {} - -impl fmt::Display for Conflict { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "Transaction conflict".fmt(f) - } -} - -/// A cross-keyspace transaction using optimistic concurrency control -/// -/// Use [`WriteTransaction::commit`] to commit changes to the keyspace(s). -/// -/// Transactions keep a consistent view of the database at the time, -/// meaning old data will not be dropped until it is not referenced by any active transaction. -/// -/// For that reason, you should try to keep transactions short-lived, and make sure they -/// are not held somewhere forever. -/// -/// # Caution -/// -/// The transaction may fail and have to be rerun if it conflicts. -#[clippy::has_significant_drop] -pub struct WriteTransaction { - inner: BaseTransaction, - cm: ConflictManager, - oracle: Arc, -} - -impl Readable for WriteTransaction { - fn get>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - - let res = self.inner.get(keyspace, key.as_ref())?; - - self.cm.mark_read(keyspace.id, key.as_ref().into()); - - Ok(res) - } - - fn contains_key>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result { - let keyspace = keyspace.as_ref(); - - let contains = self.inner.contains_key(keyspace, key.as_ref())?; - - self.cm.mark_read(keyspace.id, key.as_ref().into()); - - Ok(contains) - } - - fn first_key_value(&self, keyspace: impl AsRef) -> Option { - self.iter(&keyspace).next() - } - - fn last_key_value(&self, keyspace: impl AsRef) -> Option { - self.iter(&keyspace).next_back() - } - - fn size_of>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - self.inner.size_of(keyspace, key) - } - - fn iter(&self, keyspace: impl AsRef) -> Iter { - self.cm.mark_range(keyspace.as_ref().id, RangeFull); - self.inner.iter(keyspace) - } - - fn range, R: RangeBounds>( - &self, - keyspace: impl AsRef, - range: R, - ) -> Iter { - let start: Bound = range.start_bound().map(|k| k.as_ref().into()); - let end: Bound = range.end_bound().map(|k| k.as_ref().into()); - - self.cm.mark_range(keyspace.as_ref().id, (start, end)); - - self.inner.range(keyspace, range) - } - - fn prefix>(&self, keyspace: impl AsRef, prefix: K) -> Iter { - self.range(keyspace, lsm_tree::range::prefix_to_range(prefix.as_ref())) - } -} - -impl WriteTransaction { - pub(crate) fn new(db: Database, nonce: SnapshotNonce, oracle: Arc) -> Self { - Self { - inner: BaseTransaction::new(db, nonce), - cm: ConflictManager::default(), - oracle, - } - } - - /// Sets the durability level. - #[must_use] - pub fn durability(mut self, mode: Option) -> Self { - self.inner = self.inner.durability(mode); - self - } - - /// Removes an item and returns its value if it existed. - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx()?; - /// - /// let taken = tx.take(&tree, "a")?.unwrap(); - /// assert_eq!(b"abc", &*taken); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn take>( - &mut self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - self.fetch_update(keyspace, key, |_| None) - } - - /// Atomically updates an item and returns the new value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable, Slice}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx()?; - /// - /// let updated = tx.update_fetch(&tree, "a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"def", &*updated); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx()?; - /// - /// let updated = tx.update_fetch(&tree, "a", |_| None)?; - /// assert!(updated.is_none()); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn update_fetch, F: FnOnce(Option<&UserValue>) -> Option>( - &mut self, - keyspace: impl AsRef, - key: K, - f: F, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - let key: UserKey = key.into(); - - let updated = self.inner.update_fetch(keyspace, key.clone(), f)?; - - self.cm.mark_read(keyspace.id, key.clone()); - self.cm.mark_conflict(keyspace.id, key); - - Ok(updated) - } - - /// Atomically updates an item and returns the previous value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable, Slice}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx()?; - /// - /// let prev = tx.fetch_update(&tree, "a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx()?; - /// - /// let prev = tx.fetch_update(&tree, "a", |_| None)?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn fetch_update, F: FnOnce(Option<&UserValue>) -> Option>( - &mut self, - keyspace: impl AsRef, - key: K, - f: F, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - let key = key.into(); - - let prev = self.inner.fetch_update(keyspace, key.clone(), f)?; - - self.cm.mark_read(keyspace.id, key.clone()); - self.cm.mark_conflict(keyspace.id, key); - - Ok(prev) - } - - /// Inserts a key-value pair into the keyspace. - /// - /// Keys may be up to 65536 bytes long, values up to 2^32 bytes. - /// Shorter keys and values result in better performance. - /// - /// If the key already exists, the item will be overwritten. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx()?; - /// tx.insert(&tree, "a", "new_value"); - /// - /// drop(tx); - /// - /// // Write was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - pub fn insert, V: Into>( - &mut self, - keyspace: impl AsRef, - key: K, - value: V, - ) { - let keyspace = keyspace.as_ref(); - let key: UserKey = key.into(); - - self.inner.insert(keyspace, key.clone(), value); - self.cm.mark_conflict(keyspace.id, key); - } - - /// Removes an item from the keyspace. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx()?; - /// tx.remove(&tree, "a"); - /// - /// // Read-your-own-write - /// let item = tx.get(&tree, "a")?; - /// assert_eq!(None, item); - /// - /// drop(tx); - /// - /// // Deletion was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - pub fn remove>(&mut self, keyspace: impl AsRef, key: K) { - let keyspace = keyspace.as_ref(); - let key: UserKey = key.into(); - - self.inner.remove(keyspace, key.clone()); - self.cm.mark_conflict(keyspace.id, key); - } - - /// Removes an item from the keyspace, leaving behind a weak tombstone. - /// - /// 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. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{OptimisticTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = OptimisticTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx()?; - /// tx.remove_weak(&tree, "a"); - /// - /// // Read-your-own-write - /// let item = tx.get(&tree, "a")?; - /// assert_eq!(None, item); - /// - /// drop(tx); - /// - /// // Deletion was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[doc(hidden)] - pub fn remove_weak>(&mut self, keyspace: impl AsRef, key: K) { - let keyspace = keyspace.as_ref(); - let key: UserKey = key.into(); - - self.inner.remove_weak(keyspace, key.clone()); - self.cm.mark_conflict(keyspace.id, key); - } - - /// Commits the transaction. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn commit(self) -> crate::Result> { - // NOTE: We have no write set, so we are basically - // a read-only transaction, so nothing to do here - if self.inner.memtables.is_empty() { - return Ok(Ok(())); - } - - let oracle = self.oracle.clone(); - - match oracle.with_commit(self.inner.nonce.instant, self.cm, move || { - self.inner.commit() - })? { - CommitOutcome::Ok => Ok(Ok(())), - CommitOutcome::Aborted(e) => Err(e), - CommitOutcome::Conflicted => Ok(Err(Conflict)), - } - } - - /// More explicit alternative to dropping the transaction - /// to roll it back. - pub fn rollback(self) { - self.inner.rollback(); - } -} - -#[cfg(test)] -mod tests { - use crate::{ - Conflict, KeyspaceCreateOptions, OptimisticTxDatabase, OptimisticTxKeyspace, Readable, - }; - use tempfile::TempDir; - use test_log::test; - - struct TestEnv { - db: OptimisticTxDatabase, - tree: OptimisticTxKeyspace, - - #[expect(unused)] - tmpdir: TempDir, - } - - impl TestEnv { - fn seed_hermitage_data(&self) -> crate::Result<()> { - self.tree.insert([1u8], [10u8])?; - self.tree.insert([2u8], [20u8])?; - Ok(()) - } - } - - fn setup() -> Result> { - let tmpdir = tempfile::tempdir()?; - let db = OptimisticTxDatabase::builder(tmpdir.path()).open()?; - - let tree = db.keyspace("foo", KeyspaceCreateOptions::default)?; - - Ok(TestEnv { db, tree, tmpdir }) - } - - // Adapted from https://github.com/al8n/skipdb/issues/10 - #[test] - #[expect(clippy::unwrap_used)] - fn tx_ssi_arthur_1() -> Result<(), Box> { - let env = setup()?; - - let mut tx = env.db.write_tx()?; - tx.insert(env.tree.inner(), "a1", 10u64.to_be_bytes()); - tx.insert(env.tree.inner(), "b1", 100u64.to_be_bytes()); - tx.insert(env.tree.inner(), "b2", 200u64.to_be_bytes()); - tx.commit()??; - - let mut tx1 = env.db.write_tx()?; - let val = tx1 - .range(&env.tree, "a".."b") - .map(|kv| { - let v = kv.value().unwrap(); - - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - u64::from_be_bytes(buf) - }) - .sum::(); - tx1.insert(env.tree.inner(), "b3", 10u64.to_be_bytes()); - assert_eq!(10, val); - - let mut tx2 = env.db.write_tx()?; - let val = tx2 - .range(&env.tree, "b".."c") - .map(|kv| { - let v = kv.value().unwrap(); - - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - u64::from_be_bytes(buf) - }) - .sum::(); - tx2.insert(env.tree.inner(), "a3", 300u64.to_be_bytes()); - assert_eq!(300, val); - tx2.commit()??; - assert!(matches!(tx1.commit()?, Err(Conflict))); - - let tx3 = env.db.write_tx()?; - let val = tx3 - .iter(&env.tree) - .filter_map(|kv| { - let (k, v) = kv.into_inner().unwrap(); - - if k.starts_with(b"a") { - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - Some(u64::from_be_bytes(buf)) - } else { - None - } - }) - .sum::(); - assert_eq!(310, val); - - Ok(()) - } - - // Adapted from https://github.com/al8n/skipdb/issues/10 - #[test] - #[expect(clippy::unwrap_used)] - fn tx_ssi_arthur_2() -> Result<(), Box> { - let env = setup()?; - - let mut tx = env.db.write_tx()?; - tx.insert(env.tree.inner(), "b1", 100u64.to_be_bytes()); - tx.insert(env.tree.inner(), "b2", 200u64.to_be_bytes()); - tx.commit()??; - - let mut tx1 = env.db.write_tx()?; - let val = tx1 - .range(&env.tree, "a".."b") - .map(|kv| { - let v = kv.value().unwrap(); - - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - u64::from_be_bytes(buf) - }) - .sum::(); - tx1.insert(env.tree.inner(), "b3", 0u64.to_be_bytes()); - assert_eq!(0, val); - - let mut tx2 = env.db.write_tx()?; - let val = tx2 - .range(&env.tree, "b".."c") - .map(|kv| { - let v = kv.value().unwrap(); - - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - u64::from_be_bytes(buf) - }) - .sum::(); - tx2.insert(env.tree.inner(), "a3", 300u64.to_be_bytes()); - assert_eq!(300, val); - tx2.commit()??; - assert!(matches!(tx1.commit()?, Err(Conflict))); - - let tx3 = env.db.write_tx()?; - let val = tx3 - .iter(&env.tree) - .filter_map(|kv| { - let (k, v) = kv.into_inner().unwrap(); - - if k.starts_with(b"a") { - let mut buf = [0u8; 8]; - buf.copy_from_slice(&v); - Some(u64::from_be_bytes(buf)) - } else { - None - } - }) - .sum::(); - assert_eq!(300, val); - - Ok(()) - } - - #[test] - fn tx_ssi_basic() -> Result<(), Box> { - let env = setup()?; - - let mut tx1 = env.db.write_tx()?; - let mut tx2 = env.db.write_tx()?; - - tx1.insert(env.tree.inner(), "hello", "world"); - - tx1.commit()??; - assert!(env.tree.contains_key("hello")?); - - assert_eq!(tx2.get(env.tree.inner(), "hello")?, None); - - tx2.insert(env.tree.inner(), "hello", "world2"); - assert!(matches!(tx2.commit()?, Err(Conflict))); - - let mut tx1 = env.db.write_tx()?; - let mut tx2 = env.db.write_tx()?; - - tx1.iter(&env.tree).next(); - tx2.insert(env.tree.inner(), "hello", "world2"); - - tx1.insert(env.tree.inner(), "hello2", "world1"); - tx1.commit()??; - - tx2.commit()??; - - Ok(()) - } - - #[test] - #[expect(clippy::unwrap_used)] - fn tx_ssi_ww() -> Result<(), Box> { - // https://en.wikipedia.org/wiki/Write%E2%80%93write_conflict - let env = setup()?; - - let mut tx1 = env.db.write_tx()?; - let mut tx2 = env.db.write_tx()?; - - tx1.insert(env.tree.inner(), "a", "a"); - tx2.insert(env.tree.inner(), "b", "c"); - tx1.insert(env.tree.inner(), "b", "b"); - tx1.commit()??; - - tx2.insert(env.tree.inner(), "a", "c"); - - tx2.commit()??; - assert_eq!(b"c", &*env.tree.get("a")?.unwrap()); - assert_eq!(b"c", &*env.tree.get("b")?.unwrap()); - - Ok(()) - } - - #[test] - #[expect(clippy::unwrap_used)] - fn tx_ssi_swap() -> Result<(), Box> { - let env = setup()?; - - env.tree.insert("x", "x")?; - env.tree.insert("y", "y")?; - - let mut tx1 = env.db.write_tx()?; - let mut tx2 = env.db.write_tx()?; - - { - let x = tx1.get(env.tree.inner(), "x")?.unwrap(); - tx1.insert(env.tree.inner(), "y", x); - } - - { - let y = tx2.get(env.tree.inner(), "y")?.unwrap(); - tx2.insert(env.tree.inner(), "x", y); - } - - tx1.commit()??; - assert!(matches!(tx2.commit()?, Err(Conflict))); - - Ok(()) - } - - #[test] - fn tx_ssi_write_cycles() -> Result<(), Box> { - let env = setup()?; - env.seed_hermitage_data()?; - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - t1.insert(env.tree.inner(), [1u8], [11u8]); - t2.insert(env.tree.inner(), [1u8], [12u8]); - t1.insert(env.tree.inner(), [2u8], [21u8]); - t1.commit()??; - - assert_eq!(env.tree.get([1u8])?, Some([11u8].into())); - - t2.insert(env.tree.inner(), [2u8], [22u8]); - t2.commit()??; - - assert_eq!(env.tree.get([1u8])?, Some([12u8].into())); - assert_eq!(env.tree.get([2u8])?, Some([22u8].into())); - - Ok(()) - } - - #[test] - fn tx_ssi_aborted_reads() -> Result<(), Box> { - let env = setup()?; - env.seed_hermitage_data()?; - - let mut t1 = env.db.write_tx()?; - let t2 = env.db.write_tx()?; - - t1.insert(env.tree.inner(), [1u8], [101u8]); - - assert_eq!(t2.get(env.tree.inner(), [1u8])?, Some([10u8].into())); - - t1.rollback(); - - assert_eq!(t2.get(env.tree.inner(), [1u8])?, Some([10u8].into())); - - t2.commit()??; - - Ok(()) - } - - #[expect(clippy::unwrap_used)] - #[test] - fn tx_ssi_anti_dependency_cycles() -> Result<(), Box> { - let env = setup()?; - env.seed_hermitage_data()?; - - let mut t1 = env.db.write_tx()?; - { - let mut iter = t1.iter(&env.tree); - assert_eq!( - iter.next().unwrap().into_inner()?, - ([1u8].into(), [10u8].into()), - ); - assert_eq!( - iter.next().unwrap().into_inner()?, - ([2u8].into(), [20u8].into()), - ); - assert!(iter.next().is_none()); - } - - let mut t2 = env.db.write_tx()?; - let new = t2.update_fetch(&env.tree, [2u8], |v| { - v.and_then(|v| v.first().copied()).map(|v| [v + 5].into()) - })?; - assert_eq!(new, Some([25u8].into())); - t2.commit()??; - - let t3 = env.db.write_tx()?; - { - let mut iter = t3.iter(&env.tree); - assert_eq!( - iter.next().unwrap().into_inner()?, - ([1u8].into(), [10u8].into()), - ); - assert_eq!( - iter.next().unwrap().into_inner()?, - ([2u8].into(), [25u8].into()), - ); // changed here - assert!(iter.next().is_none()); - } - - t3.commit()??; - - t1.insert(env.tree.inner(), [1u8], [0u8]); - - assert!(matches!(t1.commit()?, Err(Conflict))); - - Ok(()) - } - - #[test] - fn tx_ssi_update_fetch_update() -> Result<(), Box> { - let env = setup()?; - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - let new = t1.update_fetch(env.tree.inner(), "hello", |_| Some("world".into()))?; - assert_eq!(new, Some("world".into())); - let old = t2.fetch_update(env.tree.inner(), "hello", |_| Some("world2".into()))?; - assert_eq!(old, None); - - t1.commit()??; - assert!(matches!(t2.commit()?, Err(Conflict))); - - assert_eq!(env.tree.get("hello")?, Some("world".into())); - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - let old = t1.fetch_update(env.tree.inner(), "hello", |_| Some("world3".into()))?; - assert_eq!(old, Some("world".into())); - let new = t2.update_fetch(env.tree.inner(), "hello2", |_| Some("world2".into()))?; - assert_eq!(new, Some("world2".into())); - - t1.commit()??; - t2.commit()??; - - assert_eq!(env.tree.get("hello")?, Some("world3".into())); - assert_eq!(env.tree.get("hello2")?, Some("world2".into())); - - Ok(()) - } - - #[test] - fn tx_ssi_range() -> Result<(), Box> { - let env = setup()?; - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - _ = t1.range(&env.tree, "h"..="hello"); - t1.insert(env.tree.inner(), "foo", "bar"); - - // insert a key INSIDE the range read by t1 - t2.insert(env.tree.inner(), "hello", "world"); - - t2.commit()??; - assert!(matches!(t1.commit()?, Err(Conflict))); - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - _ = t1.range(&env.tree, "h"..="hello"); - t1.insert(env.tree.inner(), "foo", "bar"); - - // insert a key OUTSIDE the range read by t1 - t2.insert(env.tree.inner(), "hello2", "world"); - - t2.commit()??; - t1.commit()??; - - Ok(()) - } - - #[test] - fn tx_ssi_prefix() -> Result<(), Box> { - let env = setup()?; - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - _ = t1.prefix(&env.tree, "hello"); - t1.insert(env.tree.inner(), "foo", "bar"); - - // insert a key MATCHING the prefix read by t1 - t2.insert(env.tree.inner(), "hello", "world"); - - t2.commit()??; - assert!(matches!(t1.commit()?, Err(Conflict))); - - let mut t1 = env.db.write_tx()?; - let mut t2 = env.db.write_tx()?; - - _ = t1.prefix(&env.tree, "hello"); - t1.insert(env.tree.inner(), "foo", "bar"); - - // insert a key NOT MATCHING the range read by t1 - t2.insert(env.tree.inner(), "foobar", "world"); - - t2.commit()??; - t1.commit()??; - - Ok(()) - } -} diff --git a/crates/fjall/src/tx/single_writer/keyspace.rs b/crates/fjall/src/tx/single_writer/keyspace.rs deleted file mode 100644 index ab0ac3179..000000000 --- a/crates/fjall/src/tx/single_writer/keyspace.rs +++ /dev/null @@ -1,469 +0,0 @@ -// 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::{Guard, Keyspace, Readable, SingleWriterTxDatabase}; -use lsm_tree::{UserKey, UserValue}; -use std::path::PathBuf; - -/// Handle to a keyspace of a transactional database -#[derive(Clone)] -pub struct SingleWriterTxKeyspace { - pub(crate) inner: Keyspace, - pub(crate) db: SingleWriterTxDatabase, -} - -impl AsRef for SingleWriterTxKeyspace { - fn as_ref(&self) -> &Keyspace { - self.inner() - } -} - -impl SingleWriterTxKeyspace { - /// Returns the underlying LSM-tree's path. - #[must_use] - pub fn path(&self) -> PathBuf { - self.inner.path().into() - } - - /// Approximates the number of items in the keyspace. - /// - /// For update- or delete-heavy workloads, this value will - /// diverge from the real value, but is a O(1) operation. - /// - /// For insert-only workloads (e.g. logs, time series) - /// this value is reliable. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// assert_eq!(tree.approximate_len(), 0); - /// - /// tree.insert("1", "abc")?; - /// assert_eq!(tree.approximate_len(), 1); - /// - /// tree.remove("1")?; - /// // Oops! approximate_len will not be reliable here - /// assert_eq!(tree.approximate_len(), 2); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn approximate_len(&self) -> usize { - self.inner.approximate_len() - } - - /// Removes an item and returns its value if it existed. - /// - /// The operation will run wrapped in a transaction. - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let taken = tree.take("a")?.unwrap(); - /// assert_eq!(b"abc", &*taken); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn take>(&self, key: K) -> crate::Result> { - self.fetch_update(key, |_| None) - } - - /// Atomically updates an item and returns the previous value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, Slice, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let prev = tree.fetch_update("a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let prev = tree.fetch_update("a", |_| None)?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn fetch_update, F: FnOnce(Option<&UserValue>) -> Option>( - &self, - key: K, - f: F, - ) -> crate::Result> { - let key: UserKey = key.into(); - - let mut tx = self.db.write_tx(); - - let prev = tx.fetch_update(self, key, f)?; - tx.commit()?; - - Ok(prev) - } - - /// Atomically updates an item and returns the new value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, Slice, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let updated = tree.update_fetch("a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"def", &*updated); - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let updated = tree.update_fetch("a", |_| None)?; - /// assert!(updated.is_none()); - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn update_fetch, F: FnOnce(Option<&UserValue>) -> Option>( - &self, - key: K, - f: F, - ) -> crate::Result> { - let key = key.into(); - - let mut tx = self.db.write_tx(); - let updated = tx.update_fetch(self, key, f)?; - tx.commit()?; - - Ok(updated) - } - - /// Inserts a key-value pair into the keyspace. - /// - /// Keys may be up to 65536 bytes long, values up to 2^32 bytes. - /// Shorter keys and values result in better performance. - /// - /// If the key already exists, the item will be overwritten. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// assert!(!db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn insert, V: Into>( - &self, - key: K, - value: V, - ) -> crate::Result<()> { - let mut tx = self.db.write_tx(); - tx.insert(self, key, value); - tx.commit()?; - Ok(()) - } - - /// Removes an item from the keyspace. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// assert!(!db.read_tx().is_empty(&tree)?); - /// - /// tree.remove("a")?; - /// assert!(db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn remove>(&self, key: K) -> crate::Result<()> { - let mut tx = self.db.write_tx(); - tx.remove(self, key); - tx.commit()?; - Ok(()) - } - - /// Removes an item from the keyspace, leaving behind a weak tombstone. - /// - /// 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. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// The operation will run wrapped in a transaction. - /// - /// # Experimental - /// - /// This function is currently experimental. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// assert!(!db.read_tx().is_empty(&tree)?); - /// - /// tree.remove_weak("a")?; - /// assert!(db.read_tx().is_empty(&tree)?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - #[doc(hidden)] - pub fn remove_weak>(&self, key: K) -> crate::Result<()> { - let mut tx = self.db.write_tx(); - tx.remove_weak(self, key); - tx.commit()?; - Ok(()) - } - - /// Retrieves an item from the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("my_value".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn get>(&self, key: K) -> crate::Result> { - self.inner.get(key) - } - - /// Retrieves the size of an item from the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// let len = tree.size_of("a")?.unwrap_or_default(); - /// assert_eq!("my_value".len() as u32, len); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn size_of>(&self, key: K) -> crate::Result> { - self.inner.size_of(key) - } - - /// Returns the first key-value pair in the keyspace. - /// The key in this pair is the minimum key in the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// tree.insert("b", "my_value")?; - /// - /// assert_eq!(b"a", &*tree.first_key_value().unwrap().key()?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn first_key_value(&self) -> Option { - let read_tx = self.db.read_tx(); - read_tx.first_key_value(self) - } - - /// Returns the last key-value pair in the keyspace. - /// The key in this pair is the maximum key in the keyspace. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// tree.insert("b", "my_value")?; - /// - /// assert_eq!(b"b", &*tree.last_key_value().unwrap().key()?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[must_use] - pub fn last_key_value(&self) -> Option { - let read_tx = self.db.read_tx(); - read_tx.last_key_value(self) - } - - /// Returns `true` if the keyspace contains the specified key. - /// - /// The operation will run wrapped in a read snapshot. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "my_value")?; - /// - /// assert!(tree.contains_key("a")?); - /// assert!(!tree.contains_key("b")?); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn contains_key>(&self, key: K) -> crate::Result { - self.inner.contains_key(key) - } - - /// Allows access to the inner keyspace handle, allowing to - /// escape from the transactional context. - #[doc(hidden)] - #[must_use] - pub fn inner(&self) -> &Keyspace { - &self.inner - } -} diff --git a/crates/fjall/src/tx/single_writer/mod.rs b/crates/fjall/src/tx/single_writer/mod.rs deleted file mode 100644 index 4aa57234f..000000000 --- a/crates/fjall/src/tx/single_writer/mod.rs +++ /dev/null @@ -1,189 +0,0 @@ -// 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 keyspace; -mod write_tx; - -use crate::{ - keyspace::KeyspaceKey, Config, Database, KeyspaceCreateOptions, PersistMode, Snapshot, -}; -use std::{ - path::Path, - sync::{Arc, Mutex}, -}; - -pub use keyspace::SingleWriterTxKeyspace; -pub use write_tx::WriteTransaction; - -pub trait Openable { - fn open(config: Config) -> crate::Result - where - Self: Sized; -} - -/// Single-writer transactional database -#[derive(Clone)] -pub struct TxDatabase { - pub(crate) inner: Database, - single_writer_lock: Arc>, -} - -impl Openable for TxDatabase { - fn open(config: Config) -> crate::Result - where - Self: Sized, - { - Ok(Self { - inner: crate::Database::open(config)?, - single_writer_lock: Arc::default(), - }) - } -} - -impl TxDatabase { - /// Creates a new database builder to create or open a database at `path`. - pub fn builder(path: impl AsRef) -> crate::DatabaseBuilder { - crate::DatabaseBuilder::new(path.as_ref()) - } - - #[doc(hidden)] - #[must_use] - pub fn inner(&self) -> &Database { - &self.inner - } - - /// Starts a new writeable transaction. - #[must_use] - #[expect(clippy::missing_panics_doc)] - pub fn write_tx(&self) -> WriteTransaction<'_> { - #[expect(clippy::expect_used)] - let guard = self.single_writer_lock.lock().expect("poisoned tx lock"); - - let mut write_tx = WriteTransaction::new( - self.clone(), - self.inner.supervisor.snapshot_tracker.open(), - guard, - ); - - if !self.inner.config.manual_journal_persist { - write_tx = write_tx.durability(Some(PersistMode::Buffer)); - } - - write_tx - } - - /// Starts a new read-only transaction (a.k.a. [`Snapshot`]). - #[must_use] - pub fn read_tx(&self) -> Snapshot { - self.inner.snapshot() - } - - /// 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, SingleWriterTxDatabase, KeyspaceCreateOptions}; - /// # let folder = tempfile::tempdir()?; - /// let db = SingleWriterTxDatabase::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<()> { - self.inner.persist(mode) - } - - /// 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 { - let keyspace = self.inner.keyspace(name, create_options)?; - - Ok(SingleWriterTxKeyspace { - inner: keyspace, - db: self.clone(), - }) - } - - /// Returns the number of keyspaces. - #[must_use] - pub fn keyspace_count(&self) -> usize { - self.inner.keyspace_count() - } - - /// Gets a list of all keyspace names in the database. - #[must_use] - pub fn list_keyspace_names(&self) -> Vec { - self.inner.list_keyspace_names() - } - - /// Returns `true` if the keyspace with the given name exists. - #[must_use] - pub fn keyspace_exists(&self, name: &str) -> bool { - self.inner.keyspace_exists(name) - } - - /// Returns the current write buffer size (active + sealed memtables). - #[must_use] - pub fn write_buffer_size(&self) -> u64 { - self.inner.write_buffer_size() - } - - /// Returns the number of journal fragments on disk. - #[must_use] - pub fn journal_count(&self) -> usize { - self.inner.journal_count() - } - - /// Returns the disk space usage of the entire database. - /// - /// # Errors - /// - /// Returns error, if an IO error occurred. - pub fn disk_space(&self) -> crate::Result { - self.inner.disk_space() - } - - /// Opens a database in the given directory. - /// - /// # Errors - /// - /// Returns error, if an IO error occurred. - pub fn open(config: Config) -> crate::Result { - let inner = Database::create_or_recover(config)?; - - Ok(Self { - inner, - single_writer_lock: Arc::default(), - }) - } -} diff --git a/crates/fjall/src/tx/single_writer/write_tx.rs b/crates/fjall/src/tx/single_writer/write_tx.rs deleted file mode 100644 index 8e702cc65..000000000 --- a/crates/fjall/src/tx/single_writer/write_tx.rs +++ /dev/null @@ -1,375 +0,0 @@ -// 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, - tx::{single_writer::keyspace::SingleWriterTxKeyspace, write_tx::BaseTransaction}, - Guard, Iter, Keyspace, PersistMode, Readable, SingleWriterTxDatabase, -}; -use lsm_tree::{UserKey, UserValue}; -use std::{ops::RangeBounds, sync::MutexGuard}; - -/// A single-writer (serialized) cross-keyspace transaction -/// -/// Use [`WriteTransaction::commit`] to commit changes to the keyspace(s). -/// -/// Transactions keep a consistent view of the database at the time, -/// meaning old data will not be dropped until it is not referenced by any active transaction. -/// -/// For that reason, you should try to keep transactions short-lived, and make sure they -/// are not held somewhere forever. -#[clippy::has_significant_drop] -pub struct WriteTransaction<'a> { - _guard: MutexGuard<'a, ()>, - inner: BaseTransaction, -} - -impl Readable for WriteTransaction<'_> { - fn get>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - self.inner.get(keyspace, key) - } - - fn contains_key>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result { - self.inner.contains_key(keyspace, key) - } - - fn first_key_value(&self, keyspace: impl AsRef) -> Option { - self.inner.first_key_value(keyspace) - } - - fn last_key_value(&self, keyspace: impl AsRef) -> Option { - self.inner.last_key_value(keyspace) - } - - fn size_of>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - self.inner.size_of(keyspace, key) - } - - fn iter(&self, keyspace: impl AsRef) -> Iter { - self.inner.iter(keyspace) - } - - fn range, R: RangeBounds>( - &self, - keyspace: impl AsRef, - range: R, - ) -> Iter { - self.inner.range(keyspace, range) - } - - fn prefix>(&self, keyspace: impl AsRef, prefix: K) -> Iter { - self.inner.prefix(keyspace, prefix) - } -} - -impl<'tx> WriteTransaction<'tx> { - pub(crate) fn new( - db: SingleWriterTxDatabase, - nonce: SnapshotNonce, - guard: MutexGuard<'tx, ()>, - ) -> Self { - Self { - _guard: guard, - inner: BaseTransaction::new(db.inner, nonce), - } - } - - /// Sets the durability level. - #[must_use] - pub fn durability(mut self, mode: Option) -> Self { - self.inner = self.inner.durability(mode); - self - } - - /// Removes an item and returns its value if it existed. - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx(); - /// - /// let taken = tx.take(&tree, "a")?.unwrap(); - /// assert_eq!(b"abc", &*taken); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn take>( - &mut self, - keyspace: &SingleWriterTxKeyspace, - key: K, - ) -> crate::Result> { - self.inner.take(keyspace.inner(), key) - } - - /// Atomically updates an item and returns the new value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Slice, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx(); - /// - /// let updated = tx.update_fetch(&tree, "a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"def", &*updated); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx(); - /// - /// let updated = tx.update_fetch(&tree, "a", |_| None)?; - /// assert!(updated.is_none()); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn update_fetch, F: FnOnce(Option<&UserValue>) -> Option>( - &mut self, - keyspace: &SingleWriterTxKeyspace, - key: K, - f: F, - ) -> crate::Result> { - self.inner.update_fetch(keyspace.inner(), key, f) - } - - /// Atomically updates an item and returns the previous value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Slice, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx(); - /// - /// let prev = tx.fetch_update(&tree, "a", |_| Some(Slice::from(*b"def")))?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert_eq!(Some("def".as_bytes().into()), item); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # use std::sync::Arc; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "abc")?; - /// - /// let mut tx = db.write_tx(); - /// - /// let prev = tx.fetch_update(&tree, "a", |_| None)?.unwrap(); - /// assert_eq!(b"abc", &*prev); - /// tx.commit()?; - /// - /// let item = tree.get("a")?; - /// assert!(item.is_none()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn fetch_update, F: FnOnce(Option<&UserValue>) -> Option>( - &mut self, - keyspace: &SingleWriterTxKeyspace, - key: K, - f: F, - ) -> crate::Result> { - self.inner.fetch_update(keyspace.inner(), key, f) - } - - // Inserts a key-value pair into the keyspace. - /// - /// Keys may be up to 65536 bytes long, values up to 2^32 bytes. - /// Shorter keys and values result in better performance. - /// - /// If the key already exists, the item will be overwritten. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx(); - /// tx.insert(&tree, "a", "new_value"); - /// - /// drop(tx); - /// - /// // Write was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - pub fn insert, V: Into>( - &mut self, - keyspace: &SingleWriterTxKeyspace, - key: K, - value: V, - ) { - self.inner.insert(keyspace.inner(), key, value); - } - - /// Removes an item from the keyspace. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx(); - /// tx.remove(&tree, "a"); - /// - /// // Read-your-own-write - /// let item = tx.get(&tree, "a")?; - /// assert_eq!(None, item); - /// - /// drop(tx); - /// - /// // Deletion was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - pub fn remove>(&mut self, keyspace: &SingleWriterTxKeyspace, key: K) { - self.inner.remove(keyspace.inner(), key); - } - - /// Removes an item from the keyspace, leaving behind a weak tombstone. - /// - /// 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. - /// - /// # Examples - /// - /// ``` - /// # use fjall::{SingleWriterTxDatabase, KeyspaceCreateOptions, Readable}; - /// # - /// # let folder = tempfile::tempdir()?; - /// # let db = SingleWriterTxDatabase::builder(folder).open()?; - /// # let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - /// tree.insert("a", "previous_value")?; - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// - /// let mut tx = db.write_tx(); - /// tx.remove_weak(&tree, "a"); - /// - /// // Read-your-own-write - /// let item = tx.get(&tree, "a")?; - /// assert_eq!(None, item); - /// - /// drop(tx); - /// - /// // Deletion was not committed - /// assert_eq!(b"previous_value", &*tree.get("a")?.unwrap()); - /// # - /// # Ok::<(), fjall::Error>(()) - /// ``` - #[doc(hidden)] - pub fn remove_weak>(&mut self, keyspace: &SingleWriterTxKeyspace, key: K) { - self.inner.remove_weak(keyspace.inner(), key); - } - - /// Commits the transaction. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn commit(self) -> crate::Result<()> { - self.inner.commit() - } - - /// More explicit alternative to dropping the transaction - /// to roll it back. - pub fn rollback(self) { - self.inner.rollback(); - } -} diff --git a/crates/fjall/src/tx/write_tx.rs b/crates/fjall/src/tx/write_tx.rs deleted file mode 100644 index e339989e9..000000000 --- a/crates/fjall/src/tx/write_tx.rs +++ /dev/null @@ -1,404 +0,0 @@ -// 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::item::Item, snapshot_nonce::SnapshotNonce, Database, Guard, HashMap, Iter, Keyspace, - OwnedWriteBatch, PersistMode, Readable, -}; -use lsm_tree::{AbstractTree, InternalValue, Memtable, SeqNo, UserKey, UserValue}; -use std::{ops::RangeBounds, sync::Arc}; - -fn ignore_tombstone_value(item: InternalValue) -> Option { - if item.is_tombstone() { - None - } else { - Some(item) - } -} - -pub(super) struct BaseTransaction { - /// Database to work with - pub(super) db: Database, - - /// Ephemeral transaction changes - /// - /// Used for RYOW (read-your-own-writes) - pub(crate) memtables: HashMap>, - - /// The snapshot, for repeatable reads - pub(crate) nonce: SnapshotNonce, - - /// Durability level used, see [`PersistMode`]. - pub(crate) durability: Option, - - /// Current sequence number - /// - /// The sequence number starts at 0b1000...0000 and is incremented with each write. - /// - /// This ensures that writes within the transaction are always newer than any existing data. - pub(crate) seqno: SeqNo, -} - -impl Readable for BaseTransaction { - fn get>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - let key = key.as_ref(); - - if let Some(memtable) = self.memtables.get(keyspace) { - if let Some(item) = memtable.get(key, SeqNo::MAX) { - return Ok(ignore_tombstone_value(item).map(|x| x.value)); - } - } - - let res = keyspace.tree.get(key, self.nonce.instant)?; - - Ok(res) - } - - fn contains_key>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result { - let keyspace = keyspace.as_ref(); - let key = key.as_ref(); - - if let Some(memtable) = self.memtables.get(keyspace) { - if let Some(item) = memtable.get(key, SeqNo::MAX) { - return Ok(!item.key.is_tombstone()); - } - } - - let contains = keyspace.tree.contains_key(key, self.nonce.instant)?; - - Ok(contains) - } - - fn first_key_value(&self, keyspace: impl AsRef) -> Option { - self.iter(keyspace.as_ref()).next() - } - - fn last_key_value(&self, keyspace: impl AsRef) -> Option { - self.iter(keyspace.as_ref()).next_back() - } - - fn size_of>( - &self, - keyspace: impl AsRef, - key: K, - ) -> crate::Result> { - let keyspace = keyspace.as_ref(); - let key = key.as_ref(); - - if let Some(memtable) = self.memtables.get(keyspace) { - if let Some(item) = memtable.get(key, SeqNo::MAX) { - // NOTE: Values are limited to u32 in lsm-tree - #[expect(clippy::cast_possible_truncation)] - return Ok(ignore_tombstone_value(item).map(|x| x.value.len() as u32)); - } - } - - let res = keyspace.tree.size_of(key, self.nonce.instant)?; - - Ok(res) - } - - fn iter(&self, keyspace: impl AsRef) -> Iter { - let keyspace = keyspace.as_ref(); - - let iter = keyspace.tree.iter( - self.nonce.instant, - self.memtables - .get(keyspace) - .cloned() - .map(|mt| (mt, self.seqno)), - ); - - Iter::new(self.nonce.clone(), iter) - } - - fn range, R: RangeBounds>( - &self, - keyspace: impl AsRef, - range: R, - ) -> Iter { - let keyspace = keyspace.as_ref(); - - let iter = keyspace.tree.range( - range, - self.nonce.instant, - self.memtables - .get(keyspace) - .cloned() - .map(|mt| (mt, self.seqno)), - ); - - Iter::new(self.nonce.clone(), iter) - } - - fn prefix>(&self, keyspace: impl AsRef, prefix: K) -> Iter { - let keyspace = keyspace.as_ref(); - - let iter = keyspace.tree.prefix( - prefix, - self.nonce.instant, - self.memtables - .get(keyspace) - .cloned() - .map(|mt| (mt, self.seqno)), - ); - - Iter::new(self.nonce.clone(), iter) - } -} - -impl BaseTransaction { - pub(crate) fn new(db: Database, nonce: SnapshotNonce) -> Self { - Self { - db, - memtables: HashMap::default(), - nonce, - durability: None, - seqno: 0x8000_0000_0000_0000, - } - } - - /// Sets the durability level. - #[must_use] - pub(super) fn durability(mut self, mode: Option) -> Self { - self.durability = mode; - self - } - - /// Removes an item and returns its value if it existed. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub(super) fn take>( - &mut self, - keyspace: &Keyspace, - key: K, - ) -> crate::Result> { - self.fetch_update(keyspace, key, |_| None) - } - - /// Atomically updates an item and returns the new value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub(super) fn update_fetch< - K: Into, - F: FnOnce(Option<&UserValue>) -> Option, - >( - &mut self, - keyspace: &Keyspace, - key: K, - f: F, - ) -> crate::Result> { - let key = key.into(); - let prev = self.get(keyspace, &key)?; - let updated = f(prev.as_ref()); - - if let Some(value) = updated.clone() { - // NOTE: Skip insert if the value hasn't changed - if prev.as_ref() != Some(&value) { - self.insert(keyspace, key, value); - } - } else if prev.is_some() { - self.remove(keyspace, key); - } - - Ok(updated) - } - - /// Atomically updates an item and returns the previous value. - /// - /// Returning `None` removes the item if it existed before. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub(super) fn fetch_update< - K: Into, - F: FnOnce(Option<&UserValue>) -> Option, - >( - &mut self, - keyspace: &Keyspace, - key: K, - f: F, - ) -> crate::Result> { - let key = key.into(); - let prev = self.get(keyspace, &key)?; - let updated = f(prev.as_ref()); - - if let Some(value) = updated { - // NOTE: Skip insert if the value hasn't changed - if prev.as_ref() != Some(&value) { - self.insert(keyspace, key, value); - } - } else if prev.is_some() { - self.remove(keyspace, key); - } - - Ok(prev) - } - - /// Inserts a key-value pair into the keyspace. - /// - /// Keys may be up to 65536 bytes long, values up to 2^32 bytes. - /// Shorter keys and values result in better performance. - /// - /// If the key already exists, the item will be overwritten. - pub(super) fn insert, V: Into>( - &mut self, - keyspace: &Keyspace, - key: K, - value: V, - ) { - self.memtables - .entry(keyspace.clone()) - .or_insert_with(|| Arc::new(Memtable::new(0))) - .insert(lsm_tree::InternalValue::from_components( - key, - value, - self.seqno, - lsm_tree::ValueType::Value, - )); - - self.seqno += 1; - } - - /// Removes an item from the keyspace. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - pub(super) fn remove>(&mut self, keyspace: &Keyspace, key: K) { - self.memtables - .entry(keyspace.clone()) - .or_insert_with(|| Arc::new(Memtable::new(0))) - .insert(lsm_tree::InternalValue::new_tombstone(key, self.seqno)); - - self.seqno += 1; - } - - /// Removes an item from the keyspace, leaving behind a weak tombstone. - /// - /// 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. - /// - /// The key may be up to 65536 bytes long. - /// Shorter keys result in better performance. - pub(super) fn remove_weak>(&mut self, keyspace: &Keyspace, key: K) { - self.memtables - .entry(keyspace.clone()) - .or_insert_with(|| Arc::new(Memtable::new(0))) - .insert(lsm_tree::InternalValue::new_weak_tombstone(key, self.seqno)); - - self.seqno += 1; - } - - /// Commits the transaction. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub(super) fn commit(self) -> crate::Result<()> { - // skip all the logic if no keys were written to - if self.memtables.is_empty() { - return Ok(()); - } - - // TODO: instead of using batch, write batch::commit as a generic function that takes - // a impl Iterator - // that way, we don't have to move the memtable(s) into the batch first to commit - let mut batch = OwnedWriteBatch::new(self.db).durability(self.durability); - - for (keyspace, memtable) in self.memtables { - let mut prev_key: Option = None; - - for item in memtable.iter() { - if let Some(prev_key) = &prev_key { - if item.key.user_key == prev_key { - continue; - } - } - - batch.data.push(Item::new( - keyspace.clone(), - item.key.user_key.clone(), - item.value.clone(), - item.key.value_type, - )); - - prev_key = Some(item.key.user_key.clone()); - } - } - - batch.commit()?; - - Ok(()) - } - - /// More explicit alternative to dropping the transaction - /// to roll it back. - #[expect(clippy::unused_self)] - pub(super) fn rollback(self) {} -} - -#[cfg(test)] -mod tests { - use crate::{ - tx::{single_writer::SingleWriterTxKeyspace, write_tx::BaseTransaction}, - KeyspaceCreateOptions, SingleWriterTxDatabase, - }; - use tempfile::TempDir; - - struct TestEnv { - db: SingleWriterTxDatabase, - tree: SingleWriterTxKeyspace, - - #[expect(unused)] - tmpdir: TempDir, - } - - fn setup() -> Result> { - let tmpdir: TempDir = tempfile::tempdir()?; - let db = SingleWriterTxDatabase::builder(tmpdir.path()).open()?; - - let tree = db.keyspace("foo", KeyspaceCreateOptions::default)?; - - Ok(TestEnv { db, tree, tmpdir }) - } - - #[test] - fn update_fetch() -> Result<(), Box> { - let env = setup()?; - - env.tree.insert([2u8], [20u8])?; - - let mut tx = BaseTransaction::new( - env.db.inner.clone(), - env.db.inner().supervisor.snapshot_tracker.open(), - ); - - let new = tx.update_fetch(env.tree.inner(), [2u8], |v| { - v.and_then(|v| v.first().copied()).map(|v| [v + 5].into()) - })?; - - assert_eq!(new, Some([25u8].into())); - tx.commit()?; - - Ok(()) - } -} diff --git a/crates/fjall/src/version.rs b/crates/fjall/src/version.rs index ac2113b03..af946348b 100644 --- a/crates/fjall/src/version.rs +++ b/crates/fjall/src/version.rs @@ -18,6 +18,9 @@ pub enum FormatVersion { /// Fixed-width key/value lengths in LSM table data blocks V4, + + /// 32-bit LSM tree and table IDs + V5, } impl std::fmt::Display for FormatVersion { @@ -33,6 +36,7 @@ impl From for u8 { FormatVersion::V2 => 2, FormatVersion::V3 => 3, FormatVersion::V4 => 4, + FormatVersion::V5 => 5, } } } @@ -46,6 +50,7 @@ impl TryFrom for FormatVersion { 2 => Ok(Self::V2), 3 => Ok(Self::V3), 4 => Ok(Self::V4), + 5 => Ok(Self::V5), _ => Err(()), } } @@ -110,13 +115,13 @@ mod tests { } #[test] - pub fn version_serialize_and_deserialize_4() -> crate::Result<()> { + pub fn version_serialize_and_deserialize_5() -> crate::Result<()> { let mut bytes = vec![]; - FormatVersion::V4.write_file_header(&mut bytes)?; - assert_eq!(bytes, &[b'F', b'J', b'L', 4]); + FormatVersion::V5.write_file_header(&mut bytes)?; + assert_eq!(bytes, &[b'F', b'J', b'L', 5]); assert_eq!( FormatVersion::parse_file_header(&bytes), - Some(FormatVersion::V4), + Some(FormatVersion::V5), ); Ok(()) } diff --git a/crates/fjall/src/worker_pool.rs b/crates/fjall/src/worker_pool.rs index a8eecb220..dcf836d8a 100644 --- a/crates/fjall/src/worker_pool.rs +++ b/crates/fjall/src/worker_pool.rs @@ -3,13 +3,13 @@ // (found in the LICENSE-* files in the repository) use crate::{ - compaction::worker::run as run_compaction, flush::worker::run as run_flush, poison::PoisonDart, - stats::Stats, supervisor::Supervisor, Keyspace, + Keyspace, compaction::worker::run as run_compaction, flush::worker::run as run_flush, + poison::PoisonDart, stats::Stats, supervisor::Supervisor, }; use lsm_tree::MemtableId; use std::{ borrow::Cow, - sync::{atomic::AtomicUsize, Arc, Mutex}, + sync::{Arc, Mutex, atomic::AtomicUsize}, thread::JoinHandle, }; diff --git a/crates/fjall/src/write_buffer_manager.rs b/crates/fjall/src/write_buffer_manager.rs index d06d9065a..3c6a2a569 100644 --- a/crates/fjall/src/write_buffer_manager.rs +++ b/crates/fjall/src/write_buffer_manager.rs @@ -2,7 +2,7 @@ // 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::AtomicU64, Arc}; +use std::sync::{Arc, atomic::AtomicU64}; /// Keeps track of the size of the database's write buffer #[derive(Clone, Default, Debug)] diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/a/config b/crates/fjall/test_fixture/v1_keyspace/partitions/a/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/a/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/a/levels b/crates/fjall/test_fixture/v1_keyspace/partitions/a/levels deleted file mode 100644 index 67a331d07..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/a/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/a/segments/0 b/crates/fjall/test_fixture/v1_keyspace/partitions/a/segments/0 deleted file mode 100644 index 37084ea40..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/a/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/a/version b/crates/fjall/test_fixture/v1_keyspace/partitions/a/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/a/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/b/config b/crates/fjall/test_fixture/v1_keyspace/partitions/b/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/b/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/b/levels b/crates/fjall/test_fixture/v1_keyspace/partitions/b/levels deleted file mode 100644 index 648cea504..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/b/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/b/segments/1 b/crates/fjall/test_fixture/v1_keyspace/partitions/b/segments/1 deleted file mode 100644 index 9aa9de25c..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/b/segments/1 and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/b/version b/crates/fjall/test_fixture/v1_keyspace/partitions/b/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/b/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/c/config b/crates/fjall/test_fixture/v1_keyspace/partitions/c/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/c/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/c/levels b/crates/fjall/test_fixture/v1_keyspace/partitions/c/levels deleted file mode 100644 index 00c888fcc..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/c/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/partitions/c/version b/crates/fjall/test_fixture/v1_keyspace/partitions/c/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/partitions/c/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace/version b/crates/fjall/test_fixture/v1_keyspace/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/config b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/levels b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/levels deleted file mode 100644 index 67a331d07..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/segments/0 b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/segments/0 deleted file mode 100644 index 37084ea40..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/version b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/a/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/config b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/levels b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/levels deleted file mode 100644 index 648cea504..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/segments/1 b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/segments/1 deleted file mode 100644 index 9aa9de25c..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/segments/1 and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/version b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/b/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/config b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/levels b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/levels deleted file mode 100644 index 00c888fcc..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/version b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/partitions/c/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/version b/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/fjall/test_fixture/v1_keyspace_corrupt_journal/version and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/journals/2 b/crates/fjall/test_fixture/v2_keyspace/journals/2 deleted file mode 100644 index 1c2bd9d6e..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/journals/2 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/config b/crates/fjall/test_fixture/v2_keyspace/partitions/default1/config deleted file mode 100644 index 1fe458313..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/levels b/crates/fjall/test_fixture/v2_keyspace/partitions/default1/levels deleted file mode 100644 index 23ecc5525..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/manifest b/crates/fjall/test_fixture/v2_keyspace/partitions/default1/manifest deleted file mode 100644 index 969bbb4ca..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/segments/0 b/crates/fjall/test_fixture/v2_keyspace/partitions/default1/segments/0 deleted file mode 100644 index d956091ee..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default1/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/.vlog b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/.vlog deleted file mode 100644 index 43b822654..000000000 --- a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/.vlog +++ /dev/null @@ -1 +0,0 @@ -VLG \ No newline at end of file diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/segments/0 b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/segments/0 deleted file mode 100644 index fbc0380b4..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/vlog_manifest b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/vlog_manifest deleted file mode 100644 index 3836075e3..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/blobs/vlog_manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/config b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/config deleted file mode 100644 index 09c376594..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/levels b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/levels deleted file mode 100644 index 23ecc5525..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/manifest b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/manifest deleted file mode 100644 index 445fd0922..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/segments/0 b/crates/fjall/test_fixture/v2_keyspace/partitions/default2/segments/0 deleted file mode 100644 index dbdcb6be6..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace/partitions/default2/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace/version b/crates/fjall/test_fixture/v2_keyspace/version deleted file mode 100644 index 29e6c1aa3..000000000 --- a/crates/fjall/test_fixture/v2_keyspace/version +++ /dev/null @@ -1 +0,0 @@ -FJL \ No newline at end of file diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/journals/2 b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/journals/2 deleted file mode 100644 index 908e21c95..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/journals/2 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/config b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/config deleted file mode 100644 index 1fe458313..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/levels b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/levels deleted file mode 100644 index 23ecc5525..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/manifest b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/manifest deleted file mode 100644 index 969bbb4ca..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/segments/0 b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/segments/0 deleted file mode 100644 index d956091ee..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default1/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/.vlog b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/.vlog deleted file mode 100644 index 43b822654..000000000 --- a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/.vlog +++ /dev/null @@ -1 +0,0 @@ -VLG \ No newline at end of file diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/segments/0 b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/segments/0 deleted file mode 100644 index fbc0380b4..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/vlog_manifest b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/vlog_manifest deleted file mode 100644 index 3836075e3..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/blobs/vlog_manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/config b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/config deleted file mode 100644 index 09c376594..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/levels b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/levels deleted file mode 100644 index 23ecc5525..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/manifest b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/manifest deleted file mode 100644 index 445fd0922..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/segments/0 b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/segments/0 deleted file mode 100644 index dbdcb6be6..000000000 Binary files a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/partitions/default2/segments/0 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/version b/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/version deleted file mode 100644 index 29e6c1aa3..000000000 --- a/crates/fjall/test_fixture/v2_keyspace_corrupt_journal/version +++ /dev/null @@ -1 +0,0 @@ -FJL \ No newline at end of file diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/0.sealed b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/0.sealed deleted file mode 100644 index 4a0a05ead..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/0.sealed and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/1.sealed b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/1.sealed deleted file mode 100644 index 88a33b123..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/1.sealed and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/2 b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/2 deleted file mode 100644 index ef2241820..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/2 and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/3 b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/journals/3 deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/config b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/config deleted file mode 100644 index 6c58a71dc..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/config and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/levels b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/levels deleted file mode 100644 index 8571bd380..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/levels and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/manifest b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/manifest deleted file mode 100644 index 969bbb4ca..000000000 Binary files a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/partitions/default/manifest and /dev/null differ diff --git a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/version b/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/version deleted file mode 100644 index 29e6c1aa3..000000000 --- a/crates/fjall/test_fixture/v2_sealed_journal_shenanigans/version +++ /dev/null @@ -1 +0,0 @@ -FJL \ No newline at end of file diff --git a/crates/fjall/tests/batch.rs b/crates/fjall/tests/batch.rs index 99b2d2c7d..fb2f30542 100644 --- a/crates/fjall/tests/batch.rs +++ b/crates/fjall/tests/batch.rs @@ -1,4 +1,4 @@ -use fjall::{Database, KeyspaceCreateOptions, KvSeparationOptions}; +use fjall::{Database, KeyspaceCreateOptions}; use test_log::test; #[test] @@ -21,33 +21,6 @@ fn batch_simple() -> fjall::Result<()> { Ok(()) } -#[test] -fn blob_batch_simple() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Some(KvSeparationOptions::default())) - })?; - - let blob = "oxygen".repeat(128_000); - - let mut batch = db.batch(); - - assert_eq!(tree.len()?, 0); - batch.insert(&tree, "1", &blob); - batch.insert(&tree, "3", "abc"); - batch.insert(&tree, "5", "abc"); - assert_eq!(tree.len()?, 0); - - batch.commit()?; - assert_eq!(tree.len()?, 3); - - assert_eq!(&*tree.get("1")?.unwrap(), blob.as_bytes()); - - Ok(()) -} - #[test] fn batch_multi_keys() -> fjall::Result<()> { let folder = tempfile::tempdir()?; diff --git a/crates/fjall/tests/blob_kv_simple.rs b/crates/fjall/tests/blob_kv_simple.rs deleted file mode 100644 index f83a7ea84..000000000 --- a/crates/fjall/tests/blob_kv_simple.rs +++ /dev/null @@ -1,63 +0,0 @@ -use fjall::{Database, KeyspaceCreateOptions, KvSeparationOptions}; -use lsm_tree::AbstractTree; -use test_log::test; - -#[test] -fn blob_kv_simple() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Some(KvSeparationOptions::default())) - })?; - - assert_eq!(tree.len()?, 0); - tree.insert("1", "oxygen".repeat(1_000_000))?; - tree.insert("3", "abc")?; - tree.insert("5", "abc")?; - assert_eq!(tree.len()?, 3); - - tree.rotate_memtable_and_wait()?; - - if let fjall::AnyTree::Blob(tree) = &tree.tree { - assert!(tree.index.disk_space() < 1_000); - - // NOTE: The data is compressed quite well, so it's way less than 1M - assert!(tree.disk_space() > 5_000); - - assert_eq!(1, tree.blob_file_count()); - } else { - unreachable!(); - } - - Ok(()) -} - -#[test] -fn blob_kv_simple_recovery() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - { - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default() - .with_kv_separation(Some(KvSeparationOptions::default())) - })?; - - assert_eq!(tree.len()?, 0); - tree.insert("1", "oxygen".repeat(1_000_000))?; - tree.insert("3", "abc")?; - tree.insert("5", "abc")?; - assert_eq!(tree.len()?, 3); - - tree.rotate_memtable_and_wait()?; - } - - { - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - assert_eq!(tree.len()?, 3); - } - - Ok(()) -} diff --git a/crates/fjall/tests/clear_dirty_read.rs b/crates/fjall/tests/clear_dirty_read.rs deleted file mode 100644 index 4f58ae390..000000000 --- a/crates/fjall/tests/clear_dirty_read.rs +++ /dev/null @@ -1,35 +0,0 @@ -use fjall::Readable; - -#[test_log::test] -fn clear_dirty_read() -> fjall::Result<()> { - use fjall::{KeyspaceCreateOptions, SingleWriterTxDatabase}; - - let folder = tempfile::tempdir()?; - - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - assert!(!tree.contains_key("a")?); - - let mut tx = db.write_tx(); - tx.insert(&tree, "a", "a"); - tx.commit()?; - - let snapshot = db.read_tx(); - assert!(snapshot.contains_key(&tree, "a")?); - - tree.inner().clear()?; - - assert!(snapshot.contains_key(&tree, "a")?); - - assert!(!tree.contains_key("a")?); - assert_eq!(0, tree.approximate_len()); - - { - let snapshot = db.read_tx(); - assert!(!snapshot.contains_key(&tree, "a")?); - } - - Ok(()) -} diff --git a/crates/fjall/tests/fifo_dirty_read.rs b/crates/fjall/tests/fifo_dirty_read.rs deleted file mode 100644 index 747bf9068..000000000 --- a/crates/fjall/tests/fifo_dirty_read.rs +++ /dev/null @@ -1,31 +0,0 @@ -use fjall::Readable; - -#[test_log::test] -fn fifo_dirty_read() -> fjall::Result<()> { - use fjall::{KeyspaceCreateOptions, SingleWriterTxDatabase}; - use std::sync::Arc; - - let folder = tempfile::tempdir()?; - - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default() - .compaction_strategy(Arc::new(fjall::compaction::Fifo::new(1, None))) - })?; - - assert!(!tree.contains_key("a")?); - - let mut tx = db.write_tx(); - tx.insert(&tree, "a", "a"); - tx.commit()?; - - let snapshot = db.read_tx(); - assert!(snapshot.contains_key(&tree, "a")?); - - tree.inner().rotate_memtable_and_wait()?; - - assert!(snapshot.contains_key(&tree, "a")?); - - Ok(()) -} diff --git a/crates/fjall/tests/format_v4.rs b/crates/fjall/tests/format_v5.rs similarity index 82% rename from crates/fjall/tests/format_v4.rs rename to crates/fjall/tests/format_v5.rs index 1539bdb75..9dc9af9f9 100644 --- a/crates/fjall/tests/format_v4.rs +++ b/crates/fjall/tests/format_v5.rs @@ -1,7 +1,7 @@ use fjall::{Database, Error, FormatVersion, KeyspaceCreateOptions}; #[test] -fn new_database_uses_v4_and_reopens_fixed_width_tables() -> fjall::Result<()> { +fn new_database_uses_v5_and_reopens_fixed_width_tables() -> fjall::Result<()> { let folder = tempfile::tempdir()?; { @@ -16,7 +16,7 @@ fn new_database_uses_v4_and_reopens_fixed_width_tables() -> fjall::Result<()> { assert_eq!( std::fs::read(folder.path().join("version"))?, - [b'F', b'J', b'L', 4], + [b'F', b'J', b'L', 5], ); let database = Database::builder(folder.path()).open()?; @@ -29,13 +29,13 @@ fn new_database_uses_v4_and_reopens_fixed_width_tables() -> fjall::Result<()> { } #[test] -fn v3_database_requires_resync() -> fjall::Result<()> { +fn v4_database_requires_resync() -> fjall::Result<()> { let folder = tempfile::tempdir()?; - std::fs::write(folder.path().join("version"), [b'F', b'J', b'L', 3])?; + std::fs::write(folder.path().join("version"), [b'F', b'J', b'L', 4])?; assert!(matches!( Database::builder(folder.path()).open(), - Err(Error::InvalidVersion(Some(FormatVersion::V3))), + Err(Error::InvalidVersion(Some(FormatVersion::V4))), )); Ok(()) } diff --git a/crates/fjall/tests/journal_large_value.rs b/crates/fjall/tests/journal_large_value.rs index d94fc711d..0642628ab 100644 --- a/crates/fjall/tests/journal_large_value.rs +++ b/crates/fjall/tests/journal_large_value.rs @@ -1,6 +1,6 @@ // Regression test for https://github.com/fjall-rs/fjall/issues/68 -use fjall::{Database, KeyspaceCreateOptions, KvSeparationOptions}; +use fjall::{Database, KeyspaceCreateOptions}; #[test_log::test] fn journal_recover_large_value() -> fjall::Result<()> { @@ -24,32 +24,3 @@ fn journal_recover_large_value() -> fjall::Result<()> { Ok(()) } - -#[test_log::test] -fn journal_recover_large_value_blob() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let large_value = "a".repeat(128_000); - - { - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default() - .with_kv_separation(Some(KvSeparationOptions::default())) - })?; - tree.insert("a", &large_value)?; - tree.insert("b", "b")?; - } - - { - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default() - .with_kv_separation(Some(KvSeparationOptions::default())) - })?; - assert_eq!(large_value.as_bytes(), &*tree.get("a")?.unwrap()); - assert_eq!(b"b", &*tree.get("b")?.unwrap()); - } - - Ok(()) -} diff --git a/crates/fjall/tests/keyspace_recover.rs b/crates/fjall/tests/keyspace_recover.rs index 960688102..08bf2236f 100644 --- a/crates/fjall/tests/keyspace_recover.rs +++ b/crates/fjall/tests/keyspace_recover.rs @@ -2,78 +2,11 @@ use fjall::config::{ BlockSizePolicy, FilterPolicy, FilterPolicyEntry, HashRatioPolicy, PinningPolicy, RestartIntervalPolicy, }; -use fjall::{AbstractTree, CompressionType, Database, KeyspaceCreateOptions, KvSeparationOptions}; -use lsm_tree::compaction::CompactionStrategy; -use std::sync::Arc; +use fjall::{AbstractTree, Database, KeyspaceCreateOptions}; use test_log::test; const ITEM_COUNT: usize = 100; -#[test] -fn reload_keyspace_config_fifo() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let strategy = fjall::compaction::Fifo::new(555, Some(6)); - - let expected_kvs = strategy.get_config(); - - { - let db = Database::builder(&folder).open()?; - - let _tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().compaction_strategy(Arc::new(strategy)) - })?; - }; - - { - let db = Database::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - assert_eq!( - expected_kvs, - tree.config.compaction_strategy.get_config(), - "compaction strategy config does not match", - ); - } - - Ok(()) -} - -#[test] -fn reload_keyspace_config_leveled() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let strategy = fjall::compaction::Leveled::default() - .with_l0_threshold(6) - .with_level_ratio_policy(vec![4.0, 6.0, 8.0]) - .with_table_target_size(100_000_000); - - let expected_kvs = strategy.get_config(); - - { - let db = Database::builder(&folder).open()?; - - let _tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().compaction_strategy(Arc::new(strategy)) - })?; - }; - - { - let db = Database::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - assert_eq!( - expected_kvs, - tree.config.compaction_strategy.get_config(), - "compaction strategy config does not match", - ); - } - - Ok(()) -} - #[test] fn reload_keyspace_config() -> fjall::Result<()> { let folder = tempfile::tempdir()?; @@ -195,41 +128,6 @@ fn reload_keyspace_config() -> fjall::Result<()> { Ok(()) } -#[test] -fn reload_keyspace_config_blob_opts() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - { - let db = Database::builder(&folder).open()?; - - let _tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Some( - KvSeparationOptions::default() - .age_cutoff(0.55) - .compression(CompressionType::None) - .file_target_size(124) - .separation_threshold(515) - .staleness_threshold(0.77), - )) - })?; - }; - - { - let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - let blob_opts = tree.config.kv_separation_opts.as_ref().unwrap(); - - assert_eq!(blob_opts.compression, CompressionType::None); - assert_eq!(blob_opts.age_cutoff, 0.55); - assert_eq!(blob_opts.file_target_size, 124); - assert_eq!(blob_opts.separation_threshold, 515); - assert_eq!(blob_opts.staleness_threshold, 0.77); - } - - Ok(()) -} - #[test] fn reload_with_keyspaces() -> fjall::Result<()> { let folder = tempfile::tempdir()?; diff --git a/crates/fjall/tests/keyspace_snapshot.rs b/crates/fjall/tests/keyspace_snapshot.rs index fe2371fff..ca4565e8a 100644 --- a/crates/fjall/tests/keyspace_snapshot.rs +++ b/crates/fjall/tests/keyspace_snapshot.rs @@ -7,9 +7,7 @@ fn keyspace_iter_dirty_read() -> fjall::Result<()> { let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Default::default()) - })?; + let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; tree.insert("a#1", "a")?; tree.insert("a#2", "b")?; @@ -32,12 +30,8 @@ fn keyspace_snapshot_read() -> fjall::Result<()> { let db = Database::builder(&folder).open()?; - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Default::default()) - })?; - let tree2 = db.keyspace("default2", || { - KeyspaceCreateOptions::default().with_kv_separation(Default::default()) - })?; + let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; + let tree2 = db.keyspace("default2", KeyspaceCreateOptions::default)?; tree.insert("a#1", "a")?; tree.insert("a#2", "b")?; diff --git a/crates/fjall/tests/keyspace_v1_load_fixture.rs b/crates/fjall/tests/keyspace_v1_load_fixture.rs deleted file mode 100644 index 273ab6f55..000000000 --- a/crates/fjall/tests/keyspace_v1_load_fixture.rs +++ /dev/null @@ -1,30 +0,0 @@ -use fjall::Database; -use test_log::test; - -#[test] -fn db_load_v1() -> fjall::Result<()> { - let folder = "test_fixture/v1_keyspace"; - - let result = Database::builder(folder).open(); - - matches!( - result, - Err(fjall::Error::InvalidVersion(Some(fjall::FormatVersion::V1))) - ); - - Ok(()) -} - -#[test] -fn db_load_v1_corrupt_journal() -> fjall::Result<()> { - let folder = "test_fixture/v1_keyspace_corrupt_journal"; - - let result = Database::builder(folder).open(); - - matches!( - result, - Err(fjall::Error::InvalidVersion(Some(fjall::FormatVersion::V1))) - ); - - Ok(()) -} diff --git a/crates/fjall/tests/keyspace_v2_load_fixture.rs b/crates/fjall/tests/keyspace_v2_load_fixture.rs deleted file mode 100644 index aa8480bd1..000000000 --- a/crates/fjall/tests/keyspace_v2_load_fixture.rs +++ /dev/null @@ -1,32 +0,0 @@ -use fjall::{Database, FormatVersion, JournalRecoveryError}; -use test_log::test; - -#[test] -fn db_load_v2() -> fjall::Result<()> { - let folder = "test_fixture/v2_keyspace"; - - let result = Database::builder(folder).open(); - - matches!( - result, - Err(fjall::Error::InvalidVersion(Some(FormatVersion::V2))) - ); - - Ok(()) -} - -#[test] -fn db_load_v2_corrupt_journal() -> fjall::Result<()> { - let folder = "test_fixture/v2_keyspace_corrupt_journal"; - - let result = Database::builder(folder).open(); - - matches!( - result, - Err(fjall::Error::JournalRecovery( - JournalRecoveryError::ChecksumMismatch - )), - ); - - Ok(()) -} diff --git a/crates/fjall/tests/prefix_complex.rs b/crates/fjall/tests/prefix_complex.rs deleted file mode 100644 index f46e4c2a6..000000000 --- a/crates/fjall/tests/prefix_complex.rs +++ /dev/null @@ -1,45 +0,0 @@ -use fjall::{Database, KeyspaceCreateOptions}; -use test_log::test; - -#[test] -fn keyspace_prefix_carl() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let db = Database::builder(&folder).open()?; - - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Default::default()) - })?; - - tree.insert("a#1", "a".repeat(4_096))?; - tree.insert("a#2", "b".repeat(4_096))?; - tree.insert("a#3", "c".repeat(4_096))?; - - tree.rotate_memtable_and_wait()?; - - { - let mut iter = tree.prefix("a#"); - - let guard = iter.next().expect("should exist"); - let key = guard.key()?; // does not load blob - assert_eq!(b"a#1", &*key); - - let guard = iter.next().expect("should exist"); - let (key, value) = guard.into_inner()?; // loads blob - assert_eq!( - (b"a#2" as &[u8], &b"b".repeat(4_096) as &[u8]), - (&*key, &*value), - ); - - let guard = iter.next().expect("should exist"); - let (key, value) = guard.into_inner()?; // loads blob - assert_eq!( - (b"a#3" as &[u8], &b"c".repeat(4_096) as &[u8]), - (&*key, &*value), - ); - - assert!(iter.next().is_none(), "iter should be consumed"); - } - - Ok(()) -} diff --git a/crates/fjall/tests/standard_keyspace.rs b/crates/fjall/tests/standard_keyspace.rs index ac056c3a0..983e954d9 100644 --- a/crates/fjall/tests/standard_keyspace.rs +++ b/crates/fjall/tests/standard_keyspace.rs @@ -13,10 +13,10 @@ fn standard_fast_paths_preserve_results_and_snapshot() -> fjall::Result<()> { let tree = db.keyspace("standard", KeyspaceCreateOptions::default)?; let mut ingestion = tree.start_ingestion()?; - ingestion.write_prevalidated("a1", "one")?; - ingestion.write_prevalidated("a2", "two")?; - ingestion.write_prevalidated("b1", "three")?; - ingestion.write_prevalidated_weak_tombstone("c1")?; + ingestion.write("a1", "one")?; + ingestion.write("a2", "two")?; + ingestion.write("b1", "three")?; + ingestion.write_weak_tombstone("c1")?; ingestion.finish_exclusive()?; assert_eq!(tree.get_standard("a2")?.as_deref(), Some(b"two".as_slice())); diff --git a/crates/fjall/tests/tx_ryow.rs b/crates/fjall/tests/tx_ryow.rs deleted file mode 100644 index 69e99d03e..000000000 --- a/crates/fjall/tests/tx_ryow.rs +++ /dev/null @@ -1,31 +0,0 @@ -use fjall::Readable; - -#[test_log::test] -fn tx_ryow() -> fjall::Result<()> { - use fjall::{KeyspaceCreateOptions, SingleWriterTxDatabase}; - - let folder = tempfile::tempdir()?; - - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - let mut tx = db.write_tx(); - - assert!(!tx.contains_key(&tree, "a")?); - - tx.insert(&tree, "a", "a"); - assert!(tx.contains_key(&tree, "a")?); - - tx.remove(&tree, "a"); - assert!(!tx.contains_key(&tree, "a")?); - - tx.insert(&tree, "a", "a"); - tx.insert(&tree, "a", "c"); - assert_eq!(b"c", &*tx.get(&tree, "a")?.unwrap()); - - tx.remove(&tree, "a"); - assert!(!tx.contains_key(&tree, "a")?); - - Ok(()) -} diff --git a/crates/fjall/tests/tx_ryow_snapshot.rs b/crates/fjall/tests/tx_ryow_snapshot.rs deleted file mode 100644 index 7453fedad..000000000 --- a/crates/fjall/tests/tx_ryow_snapshot.rs +++ /dev/null @@ -1,59 +0,0 @@ -use fjall::Readable; - -/// Tests that modifications to a write transaction do not dirty a previously started snapshot -#[test_log::test] -fn tx_ryow_snapshot() -> fjall::Result<()> { - use fjall::{KeyspaceCreateOptions, SingleWriterTxDatabase}; - - let folder = tempfile::tempdir()?; - - let db = SingleWriterTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - let mut tx = db.write_tx(); - - { - let iter = tx.iter(&tree); - for x in 0u64..100 { - tx.insert(&tree, x.to_be_bytes(), "a"); - } - assert_eq!(0, iter.count()); - } - - assert_eq!(100, tx.iter(&tree).count()); - - Ok(()) -} - -#[test_log::test] -fn tx_ryow_snapshot_ssi() -> fjall::Result<()> { - use fjall::{KeyspaceCreateOptions, OptimisticTxDatabase}; - - let folder = tempfile::tempdir()?; - - let db = OptimisticTxDatabase::builder(&folder).open()?; - - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - let mut tx = db.write_tx()?; - - { - let iter = tx.iter(&tree); - for x in 0u64..100 { - tx.insert(&tree, x.to_be_bytes(), "a"); - } - assert_eq!(0, iter.count()); - } - - assert_eq!(100, tx.iter(&tree).count()); - - tree.insert("2", "2")?; - assert_eq!(100, tx.iter(&tree).count()); - - let iter = tx.iter(&tree); - tree.insert("3", "3")?; - assert_eq!(100, iter.count()); - - Ok(()) -} diff --git a/crates/fjall/tests/write_buffer_size.rs b/crates/fjall/tests/write_buffer_size.rs index 507713f9a..02b373430 100644 --- a/crates/fjall/tests/write_buffer_size.rs +++ b/crates/fjall/tests/write_buffer_size.rs @@ -1,4 +1,4 @@ -use fjall::{Database, KeyspaceCreateOptions, KvSeparationOptions}; +use fjall::{Database, KeyspaceCreateOptions}; use test_log::test; #[test] @@ -27,32 +27,3 @@ fn write_buffer_size_after_insert() -> fjall::Result<()> { Ok(()) } - -#[test] -fn write_buffer_size_blob() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let db = Database::builder(&folder).open()?; - - let tree = db.keyspace("default", || { - KeyspaceCreateOptions::default().with_kv_separation(Some(KvSeparationOptions::default())) - })?; - assert_eq!(0, db.write_buffer_size()); - - tree.insert("asd", "def")?; - - let write_buffer_size_after = db.write_buffer_size(); - assert!(write_buffer_size_after > 0); - - let mut batch = db.batch(); - batch.insert(&tree, "dsa", "qwe"); - batch.commit()?; - - let write_buffer_size_after_batch = db.write_buffer_size(); - assert!(write_buffer_size_after_batch > write_buffer_size_after); - - tree.rotate_memtable_and_wait()?; - assert_eq!(0, db.write_buffer_size()); - - Ok(()) -} diff --git a/crates/fjall/tests/write_tx.rs b/crates/fjall/tests/write_tx.rs deleted file mode 100644 index 01fced988..000000000 --- a/crates/fjall/tests/write_tx.rs +++ /dev/null @@ -1,20 +0,0 @@ -use fjall::{KeyspaceCreateOptions, Readable, SingleWriterTxDatabase}; -use test_log::test; - -#[test] -fn write_tx_multi_keys() -> fjall::Result<()> { - let folder = tempfile::tempdir()?; - - let db = SingleWriterTxDatabase::builder(&folder).open()?; - let tree = db.keyspace("default", KeyspaceCreateOptions::default)?; - - let mut wtx = db.write_tx(); - wtx.insert(&tree, "1", "abc"); - wtx.insert(&tree, "1", "def"); - wtx.insert(&tree, "1", "ghi"); - assert_eq!(&*wtx.get(&tree, "1")?.unwrap(), b"ghi"); - wtx.commit()?; - assert_eq!(&*tree.get("1")?.unwrap(), b"ghi"); - - Ok(()) -} diff --git a/crates/lsm-tree/.config/nextest.toml b/crates/lsm-tree/.config/nextest.toml deleted file mode 100644 index 6079e36c6..000000000 --- a/crates/lsm-tree/.config/nextest.toml +++ /dev/null @@ -1,2 +0,0 @@ -[profile.default] -slow-timeout = { period = "10s", terminate-after = 6 } diff --git a/crates/lsm-tree/.gitignore b/crates/lsm-tree/.gitignore deleted file mode 100644 index c31cd55da..000000000 --- a/crates/lsm-tree/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# 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 - -.directory - -.lsm.data -.test* -.bench - -mutants* -profile.json -fuzz*/**/out* - -old diff --git a/crates/lsm-tree/.rustfmt.toml b/crates/lsm-tree/.rustfmt.toml deleted file mode 100644 index 1c7741475..000000000 --- a/crates/lsm-tree/.rustfmt.toml +++ /dev/null @@ -1,3 +0,0 @@ -reorder_imports = true -# group_imports = "StdExternalCrate" -# imports_granularity = "crate" diff --git a/crates/lsm-tree/CONTRIBUTING.md b/crates/lsm-tree/CONTRIBUTING.md deleted file mode 100644 index b5aab0e18..000000000 --- a/crates/lsm-tree/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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/lsm-tree/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 diff --git a/crates/lsm-tree/Cargo.toml b/crates/lsm-tree/Cargo.toml index b83a8315d..d58813838 100644 --- a/crates/lsm-tree/Cargo.toml +++ b/crates/lsm-tree/Cargo.toml @@ -5,11 +5,8 @@ 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 = ["lsmt", "lsm", "rocksdb", "leveldb", "key-value"] -categories = ["data-structures", "database-implementations", "algorithms"] [lib] name = "lsm_tree" @@ -18,42 +15,35 @@ path = "src/lib.rs" [features] default = [] lz4 = ["dep:lz4_flex"] -bytes_1 = ["dep:bytes"] -metrics = [] [dependencies] -bytes = { version = "1", optional = true } byteorder = { package = "byteorder-lite", version = "0.1.0" } byteview = { workspace = true } crossbeam-skiplist = "0.1.3" -enum_dispatch = "0.3.13" interval-heap = "0.0.5" log = { workspace = true } lz4_flex = { workspace = true, features = ["checked-decode"], optional = true } -quick_cache = { version = "0.6.16", default-features = false, features = [] } +quick_cache = { version = "0.7.0", default-features = false, features = [] } rustc-hash = { workspace = true } -self_cell = "1.2.0" +self_cell = "1.3.0" sfa = "~1.0.0" tempfile = { workspace = true } -varint-rs = "2.2.0" -xxhash-rust = { version = "0.8.15", features = ["xxh3"] } +varint-rs = "2.2.1" +xxhash-rust = { version = "0.8.18", features = ["xxh3"] } [dev-dependencies] -criterion = { version = "0.8.0", features = ["html_reports"] } +criterion = { version = "0.8.2", features = ["html_reports"] } fs_extra = "1.3.0" -nanoid = "0.4.0" -rand = "0.9.2" +nanoid = "0.5.0" +rand = "0.10.2" strum = { version = "0.28.0", features = ["derive"] } -test-log = "0.2.18" +test-log = "0.2.21" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', ] } -[package.metadata.cargo-all-features] -denylist = [] - [[bench]] name = "merge" harness = false diff --git a/crates/lsm-tree/README.md b/crates/lsm-tree/README.md deleted file mode 100644 index 533fdac36..000000000 --- a/crates/lsm-tree/README.md +++ /dev/null @@ -1,83 +0,0 @@ -

- -

- -[![CI](https://github.com/fjall-rs/lsm-tree/actions/workflows/test.yml/badge.svg)](https://github.com/fjall-rs/lsm-tree/actions/workflows/test.yml) -[![docs.rs](https://img.shields.io/docsrs/lsm-tree?color=green)](https://docs.rs/lsm-tree) -[![Crates.io](https://img.shields.io/crates/v/lsm-tree?color=blue)](https://crates.io/crates/lsm-tree) -![MSRV](https://img.shields.io/badge/MSRV-1.90.0-blue) -[![dependency status](https://deps.rs/repo/github/fjall-rs/lsm-tree/status.svg)](https://deps.rs/repo/github/fjall-rs/lsm-tree) - -A K.I.S.S. implementation of log-structured merge trees (LSM-trees/LSMTs) in Rust. - -> [!NOTE] -> This crate only provides a primitive LSM-tree, not a full storage engine. -> For example, it does not ship with a write-ahead log. -> You probably want to use https://github.com/fjall-rs/fjall instead. - -## About - -This is the most feature-rich LSM-tree implementation in Rust! It features: - -- Thread-safe `BTreeMap`-like API -- Mostly [safe](./UNSAFE.md) & 100% stable Rust -- Block-based tables with compression support & prefix truncation - - Optional block hash indexes in data blocks for faster point lookups [[3]](#footnotes) - - Per-level filter/index block pinning configuration -- Range & prefix searching with forward and reverse iteration -- Block caching to keep hot data in memory -- File descriptor caching with upper bound to reduce `fopen` syscalls -- *AMQ* filters (currently Bloom filters) to improve point lookup performance -- Multi-versioning of KVs, enabling snapshot reads -- Optionally partitioned block index & filters for better cache efficiency [[1]](#footnotes) -- Leveled and FIFO compaction -- Optional key-value separation for large value workloads [[2]](#footnotes), with automatic garbage collection -- Single deletion tombstones ("weak" deletion) -- Optional compaction filters to run custom logic during compactions - -Keys are limited to 65536 bytes, values are limited to 2^32 bytes. -As is normal with any kind of storage engine, larger keys and values have a bigger performance impact. - -## Sponsors - - - - - - Orbitinghail - - - -## Feature flags - -### lz4 - -Allows using `LZ4` compression, powered by [`lz4_flex`](https://github.com/PSeitz/lz4_flex). - -*Disabled by default.* - -### bytes_1 - -Uses [`bytes`](https://github.com/tokio-rs/bytes) 1.x as the underlying `Slice` type. - -*Disabled by default.* - -## Run unit benchmarks - -```bash -cargo bench --features lz4 -``` - -## License - -All source code is licensed under MIT OR Apache-2.0. - -All contributions are to be licensed as MIT OR Apache-2.0. - -## Footnotes - -[1] https://rocksdb.org/blog/2017/05/12/partitioned-index-filter.html - -[2] https://github.com/facebook/rocksdb/wiki/BlobDB - -[3] https://rocksdb.org/blog/2018/08/23/data-block-hash-index.html diff --git a/crates/lsm-tree/benches/block.rs b/crates/lsm-tree/benches/block.rs index f0ae8beb7..f1d08ad30 100644 --- a/crates/lsm-tree/benches/block.rs +++ b/crates/lsm-tree/benches/block.rs @@ -1,7 +1,7 @@ -use criterion::{criterion_group, criterion_main, BatchSize, Criterion, Throughput}; +use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; use lsm_tree::{ - table::block::{Block, BlockType}, CompressionType, + table::block::{Block, BlockType}, }; fn input(size: usize) -> Vec { diff --git a/crates/lsm-tree/benches/bloom.rs b/crates/lsm-tree/benches/bloom.rs index 4c2d8dfe1..5f72db1f6 100644 --- a/crates/lsm-tree/benches/bloom.rs +++ b/crates/lsm-tree/benches/bloom.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use rand::{Rng, RngCore}; // Not really worth it anymore on new CPUs...? diff --git a/crates/lsm-tree/benches/fd_table.rs b/crates/lsm-tree/benches/fd_table.rs index 79632fa1f..8e2637643 100644 --- a/crates/lsm-tree/benches/fd_table.rs +++ b/crates/lsm-tree/benches/fd_table.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use std::sync::Arc; fn file_descriptor_table(c: &mut Criterion) { diff --git a/crates/lsm-tree/benches/level_manifest.rs b/crates/lsm-tree/benches/level_manifest.rs index c260a58b9..4b9d86792 100644 --- a/crates/lsm-tree/benches/level_manifest.rs +++ b/crates/lsm-tree/benches/level_manifest.rs @@ -1,5 +1,5 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use lsm_tree::{config::BlockSizePolicy, AbstractTree, Config}; +use criterion::{Criterion, criterion_group, criterion_main}; +use lsm_tree::{AbstractTree, Config, config::BlockSizePolicy}; fn iterate_tables(c: &mut Criterion) { let mut group = c.benchmark_group("Iterate version tables"); diff --git a/crates/lsm-tree/benches/memtable.rs b/crates/lsm-tree/benches/memtable.rs index db708ddfb..a89e18b66 100644 --- a/crates/lsm-tree/benches/memtable.rs +++ b/crates/lsm-tree/benches/memtable.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use lsm_tree::{InternalValue, Memtable, SeqNo}; use nanoid::nanoid; diff --git a/crates/lsm-tree/benches/merge.rs b/crates/lsm-tree/benches/merge.rs index cb5f24024..75a2aeb56 100644 --- a/crates/lsm-tree/benches/merge.rs +++ b/crates/lsm-tree/benches/merge.rs @@ -1,6 +1,6 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use lsm_tree::merge::{BoxedIterator, Merger}; -use lsm_tree::{mvcc_stream::MvccStream, InternalValue, Memtable}; +use lsm_tree::{InternalValue, Memtable, mvcc_stream::MvccStream}; use nanoid::nanoid; fn merger(c: &mut Criterion) { diff --git a/crates/lsm-tree/benches/tree.rs b/crates/lsm-tree/benches/tree.rs index 6e457b93d..c8667c675 100644 --- a/crates/lsm-tree/benches/tree.rs +++ b/crates/lsm-tree/benches/tree.rs @@ -1,7 +1,6 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use lsm_tree::{ - config::BlockSizePolicy, AbstractTree, Cache, Config, Guard, KvSeparationOptions, SeqNo, - SequenceNumberCounter, + AbstractTree, Cache, Config, Guard, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, }; use std::{path::Path, sync::Arc}; use tempfile::tempdir; @@ -376,32 +375,11 @@ fn disjoint_tree_minmax(c: &mut Criterion) { }); } -fn blob_tree_get(c: &mut Criterion) { - let folder = tempfile::tempdir().unwrap(); - - let tree = config(folder.path()) - .use_cache(Cache::with_capacity_bytes(0).into()) - .with_kv_separation(Some(KvSeparationOptions::default())) - .open() - .unwrap(); - - let value = b"powek5bowa".repeat(100); - - tree.insert("mykey", &value, 0); - - c.bench_function("blob tree get", |b| { - b.iter(|| { - tree.get("mykey", SeqNo::MAX).unwrap().unwrap(); - }); - }); -} - // TODO: benchmark point read disjoint vs non-disjoint level vs disjoint *tree* // TODO: benchmark .prefix().next() and .next_back(), disjoint and non-disjoint criterion_group!( benches, - blob_tree_get, disjoint_tree_minmax, disk_point_read, full_scan, diff --git a/crates/lsm-tree/logo.png b/crates/lsm-tree/logo.png deleted file mode 100644 index d5873dd9a..000000000 Binary files a/crates/lsm-tree/logo.png and /dev/null differ diff --git a/crates/lsm-tree/renovate.json b/crates/lsm-tree/renovate.json deleted file mode 100644 index 39a2b6e9a..000000000 --- a/crates/lsm-tree/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ] -} diff --git a/crates/lsm-tree/src/abstract_tree.rs b/crates/lsm-tree/src/abstract_tree.rs index 74f1a1f3b..5998818f7 100644 --- a/crates/lsm-tree/src/abstract_tree.rs +++ b/crates/lsm-tree/src/abstract_tree.rs @@ -3,20 +3,17 @@ // (found in the LICENSE-* files in the repository) use crate::{ - iter_guard::IterGuardImpl, table::Table, version::Version, vlog::BlobFile, AnyTree, BlobTree, - Config, Guard, InternalValue, KvPair, Memtable, SeqNo, TableId, Tree, UserKey, UserValue, + Config, Guard, InternalValue, Memtable, SeqNo, TableId, UserKey, UserValue, + iter_guard::IterGuardImpl, table::Table, version::Version, }; use std::{ ops::RangeBounds, sync::{Arc, MutexGuard, RwLockWriteGuard}, }; -pub type RangeItem = crate::Result; - -type FlushToTablesResult = (Vec, Option>); +type FlushToTablesResult = Vec
; /// Generic Tree API -#[enum_dispatch::enum_dispatch] pub trait AbstractTree { /// Debug method for tracing the MVCC history of a key. #[doc(hidden)] @@ -100,14 +97,8 @@ pub trait AbstractTree { drop(version_history); - if let Some((tables, blob_files)) = self.flush_to_tables(stream)? { - self.register_tables( - &tables, - blob_files.as_deref(), - None, - &sealed_ids, - seqno_threshold, - )?; + if let Some(tables) = self.flush_to_tables(stream)? { + self.register_tables(&tables, &sealed_ids, seqno_threshold)?; } Ok(Some(flushed_size)) @@ -153,17 +144,6 @@ pub trait AbstractTree { /// Returns the approximate number of values reclaimable once weak tombstones can be GC'd. fn weak_tombstone_reclaimable_count(&self) -> u64; - /// Drops tables that are fully contained in a given range. - /// - /// Accepts any `RangeBounds`, including unbounded or exclusive endpoints. - /// If the normalized lower bound is greater than the upper bound, the - /// method returns without performing any work. - /// - /// # Errors - /// - /// Will return `Err` only if an IO error occurs. - fn drop_range, R: RangeBounds>(&self, range: R) -> crate::Result<()>; - /// Drops all tables and clears all memtables atomically. /// /// # Errors @@ -178,11 +158,6 @@ pub trait AbstractTree { /// Will return `Err` if an IO error occurs. fn major_compact(&self, target_size: u64, seqno_threshold: SeqNo) -> crate::Result<()>; - /// Returns the disk space used by stale blobs. - fn stale_blob_bytes(&self) -> u64 { - 0 - } - /// Gets the disk space usage of all filters in the tree. /// /// May not correspond to the actual memory size because filter blocks may be paged out. @@ -197,10 +172,6 @@ pub trait AbstractTree { /// Gets the length of the version free list. fn version_free_list_len(&self) -> usize; - /// Returns the metrics structure. - #[cfg(feature = "metrics")] - fn metrics(&self) -> &Arc; - /// Acquires the flush lock which is required to call [`Tree::flush`]. fn get_flush_lock(&self) -> MutexGuard<'_, ()>; @@ -226,8 +197,6 @@ pub trait AbstractTree { fn register_tables( &self, tables: &[Table], - blob_files: Option<&[BlobFile]>, - frag_map: Option, sealed_memtables_to_delete: &[crate::tree::inner::MemtableId], gc_watermark: SeqNo, ) -> crate::Result<()>; @@ -265,17 +234,6 @@ pub trait AbstractTree { /// Returns the active memtable. fn active_memtable(&self) -> Arc; - /// Returns the tree type. - fn tree_type(&self) -> crate::TreeType { - // NOTE: This is only really safe to do, because we validate - // that the config's kv_separation_opts is consistent with the tree type during recovery. - if self.tree_config().kv_separation_opts.is_some() { - crate::TreeType::Blob - } else { - crate::TreeType::Standard - } - } - /// Seals the active memtable. fn rotate_memtable(&self) -> Option>; @@ -292,9 +250,6 @@ pub trait AbstractTree { /// Can be used to determine whether to write stall. fn l0_run_count(&self) -> usize; - /// Returns the number of blob files currently in the tree. - fn blob_file_count(&self) -> usize; - /// Approximates the number of items in the tree. fn approximate_len(&self) -> usize; diff --git a/crates/lsm-tree/src/any_tree.rs b/crates/lsm-tree/src/any_tree.rs deleted file mode 100644 index 29cfbaf6b..000000000 --- a/crates/lsm-tree/src/any_tree.rs +++ /dev/null @@ -1,17 +0,0 @@ -// 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::{BlobTree, Tree}; -use enum_dispatch::enum_dispatch; - -/// May be a standard [`Tree`] or a [`BlobTree`] -#[derive(Clone)] -#[enum_dispatch(AbstractTree)] -pub enum AnyTree { - /// Standard LSM-tree, see [`Tree`] - Standard(Tree), - - /// Key-value separated LSM-tree, see [`BlobTree`] - Blob(BlobTree), -} diff --git a/crates/lsm-tree/src/blob_tree/gc.rs b/crates/lsm-tree/src/blob_tree/gc.rs deleted file mode 100644 index a905faeeb..000000000 --- a/crates/lsm-tree/src/blob_tree/gc.rs +++ /dev/null @@ -1,307 +0,0 @@ -// 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::{ - blob_tree::handle::BlobIndirection, coding::Decode, compaction::stream::DroppedKvCallback, - version::BlobFileList, vlog::BlobFileId, -}; - -/// Tracks fragmentation information in a blob file -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct FragmentationEntry { - /// Number of unreferenced (garbage) blobs - pub(crate) len: usize, - - /// Unreferenced (garbage) blob bytes that could be freed (compressed) - pub(crate) bytes: u64, - - /// Unreferenced (garbage) blob bytes that could be freed from disk - pub(crate) on_disk_bytes: u64, -} - -impl FragmentationEntry { - #[must_use] - pub fn new(len: usize, bytes: u64, on_disk_bytes: u64) -> Self { - Self { - len, - bytes, - on_disk_bytes, - } - } -} - -/// Tracks fragmentation information in a value log (list of blob files) -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct FragmentationMap(crate::HashMap); - -impl std::ops::Deref for FragmentationMap { - type Target = crate::HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for FragmentationMap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FragmentationMap { - /// Returns the number of bytes that could be freed from disk. - #[must_use] - pub fn stale_bytes(&self) -> u64 { - self.0.values().map(|x| x.on_disk_bytes).sum() - } - - /// Removes blob file entries that are not part of the value log (anymore) - /// to reduce linear memory growth. - pub fn prune(&mut self, value_log: &BlobFileList) { - self.0.retain(|&k, _| value_log.contains_key(k)); - } - - /// Merges a fragmentation map into another. - /// - /// This is used after a compaction stream is summed up (using the expiration callback), to apply - /// the diff to the tree's fragmentation stats. - pub fn merge_into(self, other: &mut Self) { - for (blob_file_id, diff) in self.0 { - other - .0 - .entry(blob_file_id) - .and_modify(|counter| { - counter.bytes += diff.bytes; - counter.len += diff.len; - counter.on_disk_bytes += diff.on_disk_bytes; - }) - .or_insert(diff); - } - } -} - -impl crate::coding::Encode for FragmentationMap { - fn encode_into(&self, writer: &mut W) -> Result<(), crate::Error> { - use byteorder::{WriteBytesExt, LE}; - - #[expect( - clippy::cast_possible_truncation, - reason = "there are always less than 4 billion blob files" - )] - writer.write_u32::(self.len() as u32)?; - - for (blob_file_id, item) in self.iter() { - writer.write_u64::(*blob_file_id)?; - - #[expect( - clippy::cast_possible_truncation, - reason = "there are always less than 4 billion blobs in a blob file" - )] - writer.write_u32::(item.len as u32)?; - - writer.write_u64::(item.bytes)?; - - writer.write_u64::(item.on_disk_bytes)?; - } - - Ok(()) - } -} - -impl crate::coding::Decode for FragmentationMap { - fn decode_from(reader: &mut R) -> Result - where - Self: Sized, - { - use byteorder::{ReadBytesExt, LE}; - - let len = reader.read_u32::()?; - let mut map = - crate::HashMap::with_capacity_and_hasher(len as usize, rustc_hash::FxBuildHasher); - - for _ in 0..len { - let id = reader.read_u64::()?; - let len = reader.read_u32::()? as usize; - let bytes = reader.read_u64::()?; - let on_disk_bytes = reader.read_u64::()?; - map.insert(id, FragmentationEntry::new(len, bytes, on_disk_bytes)); - } - - Ok(Self(map)) - } -} - -impl DroppedKvCallback for FragmentationMap { - fn on_dropped(&mut self, kv: &crate::InternalValue) { - if kv.key.value_type.is_indirection() { - let mut reader = &kv.value[..]; - - #[expect( - clippy::expect_used, - reason = "data is read and checked for corruption, so we expect to be able to deserialize BlobIndirection fine" - )] - let vptr = - BlobIndirection::decode_from(&mut reader).expect("should parse BlobIndirection"); - - let size = u64::from(vptr.size); - let on_disk_size = u64::from(vptr.vhandle.on_disk_size); - - self.0 - .entry(vptr.vhandle.blob_file_id) - .and_modify(|counter| { - counter.len += 1; - counter.bytes += size; - counter.on_disk_bytes += on_disk_size; - }) - .or_insert_with(|| FragmentationEntry { - bytes: size, - on_disk_bytes: on_disk_size, - len: 1, - }); - } - } -} - -#[cfg(test)] -#[expect(clippy::expect_used, clippy::indexing_slicing)] -mod tests { - use super::*; - use crate::{ - coding::{Decode, Encode}, - compaction::stream::CompactionStream, - value::InternalValue, - vlog::ValueHandle, - ValueType, - }; - use std::collections::HashMap; - use test_log::test; - - #[test] - fn frag_map_merge_into() { - let mut map = FragmentationMap(HashMap::default()); - map.0.insert( - 0, - FragmentationEntry { - len: 1, - bytes: 1_000, - on_disk_bytes: 500, - }, - ); - map.0.insert( - 1, - FragmentationEntry { - len: 2, - bytes: 2_000, - on_disk_bytes: 1_000, - }, - ); - - // test merge_into - let mut diff = FragmentationMap(HashMap::default()); - diff.0.insert( - 0, - FragmentationEntry { - len: 3, - bytes: 3_000, - on_disk_bytes: 1_500, - }, - ); - diff.0.insert( - 3, - FragmentationEntry { - len: 4, - bytes: 4_000, - on_disk_bytes: 2_000, - }, - ); - - diff.merge_into(&mut map); - - assert_eq!(map.0.len(), 3); - assert_eq!(map.0[&0].len, 4); - assert_eq!(map.0[&0].bytes, 4_000); - assert_eq!(map.0[&0].on_disk_bytes, 2_000); - assert_eq!(map.0[&1].len, 2); - assert_eq!(map.0[&1].bytes, 2_000); - assert_eq!(map.0[&1].on_disk_bytes, 1_000); - assert_eq!(map.0[&3].len, 4); - assert_eq!(map.0[&3].bytes, 4_000); - assert_eq!(map.0[&3].on_disk_bytes, 2_000); - } - - #[test] - fn frag_map_roundtrip() { - let map = FragmentationMap({ - let mut map = HashMap::default(); - map.insert( - 0, - FragmentationEntry { - len: 1, - bytes: 1_000, - on_disk_bytes: 500, - }, - ); - map.insert( - 1, - FragmentationEntry { - len: 2, - bytes: 2_000, - on_disk_bytes: 1_000, - }, - ); - map - }); - - let encoded = map.encode_into_vec(); - let decoded = FragmentationMap::decode_from(&mut &encoded[..]).expect("should decode map"); - assert_eq!(map, decoded); - } - - #[test] - #[expect(clippy::unwrap_used)] - fn compaction_stream_gc_count_drops() -> crate::Result<()> { - #[rustfmt::skip] - let vec = &[ - InternalValue::from_components("a", b"abc", 1, ValueType::Value), - - InternalValue::from_components("a", BlobIndirection { - size: 1000, - vhandle: ValueHandle { - blob_file_id: 0, - on_disk_size: 500, - offset: 0, - } - }.encode_into_vec(), 0, ValueType::Indirection), - ]; - - let mut my_watcher = FragmentationMap::default(); - - let iter = vec.iter().cloned().map(Ok); - let mut iter = CompactionStream::new(iter, 1_000).with_drop_callback(&mut my_watcher); - - assert_eq!( - // TODO: Seqno is normally reset to 0 - InternalValue::from_components(*b"a", b"abc", 1, ValueType::Value), - iter.next().unwrap()?, - ); - - assert_eq!( - { - let mut map = HashMap::default(); - map.insert( - 0, - FragmentationEntry { - len: 1, - bytes: 1_000, - on_disk_bytes: 500, - }, - ); - map - }, - my_watcher.0, - ); - - Ok(()) - } -} diff --git a/crates/lsm-tree/src/blob_tree/handle.rs b/crates/lsm-tree/src/blob_tree/handle.rs deleted file mode 100644 index 364508482..000000000 --- a/crates/lsm-tree/src/blob_tree/handle.rs +++ /dev/null @@ -1,32 +0,0 @@ -// 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::{ - coding::{Decode, Encode}, - vlog::ValueHandle, -}; -use std::io::{Read, Write}; -use varint_rs::{VarintReader, VarintWriter}; - -#[derive(Copy, Clone, Debug)] -pub struct BlobIndirection { - pub(crate) vhandle: ValueHandle, - pub(crate) size: u32, -} - -impl Encode for BlobIndirection { - fn encode_into(&self, writer: &mut W) -> Result<(), crate::Error> { - self.vhandle.encode_into(writer)?; - writer.write_u32_varint(self.size)?; - Ok(()) - } -} - -impl Decode for BlobIndirection { - fn decode_from(reader: &mut R) -> Result { - let vhandle = ValueHandle::decode_from(reader)?; - let size = reader.read_u32_varint()?; - Ok(Self { vhandle, size }) - } -} diff --git a/crates/lsm-tree/src/blob_tree/ingest.rs b/crates/lsm-tree/src/blob_tree/ingest.rs deleted file mode 100644 index 92795c6bf..000000000 --- a/crates/lsm-tree/src/blob_tree/ingest.rs +++ /dev/null @@ -1,274 +0,0 @@ -// 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::{ - blob_tree::handle::BlobIndirection, file::BLOBS_FOLDER, table::Table, - tree::ingest::Ingestion as TableIngestion, vlog::BlobFileWriter, SeqNo, UserKey, UserValue, -}; - -/// Bulk ingestion for [`BlobTree`] -/// -/// Items NEED to be added in ascending key order. -/// -/// Uses table ingestion for the index and a blob file writer for large -/// values so both streams advance together. -pub struct BlobIngestion<'a> { - tree: &'a crate::BlobTree, - pub(crate) table: TableIngestion<'a>, - pub(crate) blob: BlobFileWriter, - seqno: SeqNo, - separation_threshold: u32, - last_key: Option, -} - -impl<'a> BlobIngestion<'a> { - /// Creates a new ingestion. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn new(tree: &'a crate::BlobTree) -> crate::Result { - #[expect( - clippy::expect_used, - reason = "cannot define blob tree without kv separation options" - )] - let kv = tree - .index - .config - .kv_separation_opts - .as_ref() - .expect("kv separation options should exist"); - - let blob_file_size = kv.file_target_size; - - let table = TableIngestion::new(&tree.index)?; - let blob = BlobFileWriter::new( - tree.index.0.blob_file_id_counter.clone(), - tree.index.config.path.join(BLOBS_FOLDER), - tree.index.id, - tree.index.config.descriptor_table.clone(), - )? - .use_target_size(blob_file_size) - .use_compression(kv.compression); - - let separation_threshold = kv.separation_threshold; - - Ok(Self { - tree, - table, - blob, - seqno: 0, - separation_threshold, - last_key: None, - }) - } - - /// Writes a key-value pair. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write(&mut self, key: UserKey, value: UserValue) -> crate::Result<()> { - // Check order before any blob I/O to avoid partial writes on failure - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - - #[expect(clippy::cast_possible_truncation)] - let value_size = value.len() as u32; - - if value_size >= self.separation_threshold { - let vhandle = self.blob.write(&key, self.seqno, &value)?; - - let indirection = BlobIndirection { - vhandle, - size: value_size, - }; - - let cloned_key = key.clone(); - let res = self.table.write_indirection(key, indirection); - if res.is_ok() { - self.last_key = Some(cloned_key); - } - res - } else { - let cloned_key = key.clone(); - let res = self.table.write(key, value); - if res.is_ok() { - self.last_key = Some(cloned_key); - } - res - } - } - - /// Writes a tombstone for a key. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write_tombstone(&mut self, key: UserKey) -> crate::Result<()> { - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - - let cloned_key = key.clone(); - let res = self.table.write_tombstone(key); - if res.is_ok() { - self.last_key = Some(cloned_key); - } - res - } - - /// Writes a weak tombstone for a key. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write_weak_tombstone(&mut self, key: UserKey) -> crate::Result<()> { - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - - let cloned_key = key.clone(); - let res = self.table.write_weak_tombstone(key); - if res.is_ok() { - self.last_key = Some(cloned_key); - } - res - } - - /// Finishes the ingestion. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - #[allow(clippy::significant_drop_tightening)] - pub fn finish(self) -> crate::Result<()> { - use crate::AbstractTree; - - let index = self.index().clone(); - - // CRITICAL SECTION: Atomic flush + seqno allocation + registration - // - // For BlobTree, we must coordinate THREE components atomically: - // 1. Index tree memtable flush - // 2. Value log blob files - // 3. Index tree tables (with blob indirections) - // - // The sequence ensures all components see the same global_seqno: - // 1. Acquire flush lock on index tree - // 2. Flush index tree active memtable - // 3. Finalize blob writer (creates blob files) - // 4. Finalize table writer (creates index tables) - // 5. Allocate next global seqno - // 6. Recover tables with that seqno - // 7. Register version with same seqno + blob files - // - // This prevents race conditions where blob files and their index - // entries could have mismatched sequence numbers. - let flush_lock = index.get_flush_lock(); - - // Flush any pending index memtable writes to ensure ingestion sees - // a consistent snapshot of the index. - // We call rotate + flush directly because we already hold the lock. - index.rotate_memtable(); - index.flush(&flush_lock, 0)?; - - // Finalize the blob writer first, ensuring all large values are - // written to blob files before we finalize the index tables that - // reference them. - let blob_files = self.blob.finish()?; - - // Finalize the table writer, creating index tables with blob - // indirections pointing to the blob files we just created. - let results = self.table.writer.finish()?; - - // Acquire locks for version registration on the index tree. We must - // hold both the compaction state lock and version history lock to - // safely modify the tree's version. - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - let mut _compaction_state = index.compaction_state.lock().expect("lock is poisoned"); - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - let mut version_lock = index.version_history.write().expect("lock is poisoned"); - - // Allocate the next global sequence number. This seqno will be shared - // by all ingested tables, blob files, and the version that registers - // them, ensuring consistent MVCC snapshots across the value log. - let global_seqno = index.config.seqno.next(); - - // Recover all created index tables, assigning them the global_seqno - // we just allocated. These tables contain indirections to the blob - // files created above, so they must share the same sequence number - // for MVCC correctness. - // - // We intentionally do NOT pin filter/index blocks here. Large ingests - // are typically placed in level 1, and pinning would increase memory - // pressure unnecessarily. - let created_tables = results - .into_iter() - .map(|(table_id, checksum)| -> crate::Result
{ - Table::recover( - index - .config - .path - .join(crate::file::TABLES_FOLDER) - .join(table_id.to_string()), - checksum, - global_seqno, - index.id, - index.config.cache.clone(), - index.config.descriptor_table.clone(), - false, - false, - #[cfg(feature = "metrics")] - index.metrics.clone(), - ) - }) - .collect::>>()?; - - // Upgrade the version with our ingested tables and blob files, using - // the global_seqno we allocated earlier. This ensures the version, - // tables, and blob files all share the same sequence number, which is - // critical for GC correctness - we must not delete blob files that are - // still referenced by visible snapshots. - // - // We use upgrade_version_with_seqno (instead of upgrade_version) because - // we need precise control over the seqno: it must match the seqno we - // already assigned to the recovered tables. - version_lock.upgrade_version_with_seqno( - &index.config.path, - |current| { - let mut copy = current.clone(); - copy.version = - copy.version - .with_new_l0_run(&created_tables, Some(&blob_files), None); - Ok(copy) - }, - global_seqno, - &self.tree.index.config.visible_seqno, - )?; - - // Perform maintenance on the version history (e.g., clean up old versions). - // We use gc_watermark=0 since ingestion doesn't affect sealed memtables. - if let Err(e) = version_lock.maintenance(&index.config.path, 0) { - log::warn!("Version GC failed: {e:?}"); - } - - Ok(()) - } - - #[inline] - fn index(&self) -> &crate::Tree { - &self.tree.index - } -} diff --git a/crates/lsm-tree/src/blob_tree/mod.rs b/crates/lsm-tree/src/blob_tree/mod.rs deleted file mode 100644 index d38e528c7..000000000 --- a/crates/lsm-tree/src/blob_tree/mod.rs +++ /dev/null @@ -1,631 +0,0 @@ -// 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 gc; -pub mod handle; -pub mod ingest; - -#[doc(hidden)] -pub use gc::{FragmentationEntry, FragmentationMap}; - -use crate::{ - abstract_tree::{AbstractTree, RangeItem}, - coding::Decode, - iter_guard::{IterGuard, IterGuardImpl}, - table::Table, - tree::inner::MemtableId, - value::InternalValue, - version::Version, - vlog::{Accessor, BlobFile, BlobFileWriter}, - Cache, Config, Memtable, SeqNo, TableId, TreeId, UserKey, UserValue, -}; -use handle::BlobIndirection; -use std::{ - ops::RangeBounds, - path::{Path, PathBuf}, - sync::{Arc, MutexGuard}, -}; - -/// Iterator value guard -pub struct Guard { - tree: crate::BlobTree, - version: Version, - kv: crate::Result, -} - -impl IterGuard for Guard { - fn into_inner_if( - self, - pred: impl Fn(&UserKey) -> bool, - ) -> crate::Result<(UserKey, Option)> { - let kv = self.kv?; - - if pred(&kv.key.user_key) { - resolve_value_handle( - self.tree.id(), - self.tree.blobs_folder.as_path(), - &self.tree.index.config.cache, - &self.version, - kv, - ) - .map(|(k, v)| (k, Some(v))) - } else { - Ok((kv.key.user_key, None)) - } - } - - fn key(self) -> crate::Result { - self.kv.map(|kv| kv.key.user_key) - } - - fn size(self) -> crate::Result { - let kv = self.kv?; - - if kv.key.value_type.is_indirection() { - let mut cursor = std::io::Cursor::new(kv.value); - Ok(BlobIndirection::decode_from(&mut cursor)?.size) - } else { - #[expect(clippy::cast_possible_truncation, reason = "values are u32 max length")] - Ok(kv.value.len() as u32) - } - } - - fn into_inner(self) -> crate::Result<(UserKey, UserValue)> { - resolve_value_handle( - self.tree.id(), - self.tree.blobs_folder.as_path(), - &self.tree.index.config.cache, - &self.version, - self.kv?, - ) - } -} - -fn resolve_value_handle( - tree_id: TreeId, - blobs_folder: &Path, - cache: &Cache, - version: &Version, - item: InternalValue, -) -> RangeItem { - if item.key.value_type.is_indirection() { - let mut cursor = std::io::Cursor::new(item.value); - let vptr = BlobIndirection::decode_from(&mut cursor)?; - - // Resolve indirection using value log - match Accessor::new(&version.blob_files).get( - tree_id, - blobs_folder, - &item.key.user_key, - &vptr.vhandle, - cache, - ) { - Ok(Some(v)) => { - let k = item.key.user_key; - Ok((k, v)) - } - Ok(None) => { - panic!( - "value handle ({:?} => {:?}) did not match any blob - this is a bug; version={}", - item.key.user_key, vptr.vhandle, - version.id(), - ); - } - Err(e) => Err(e), - } - } else { - let k = item.key.user_key; - let v = item.value; - Ok((k, v)) - } -} - -/// A key-value-separated log-structured merge tree -/// -/// This tree is a composite structure, consisting of an -/// index tree (LSM-tree) and a log-structured value log -/// to reduce write amplification. -#[derive(Clone)] -pub struct BlobTree { - /// Index tree that holds value handles or small inline values - #[doc(hidden)] - pub index: crate::Tree, - - blobs_folder: Arc, -} - -impl BlobTree { - pub(crate) fn open(config: Config) -> crate::Result { - use crate::file::{fsync_directory, BLOBS_FOLDER}; - - let index = crate::Tree::open(config)?; - - let blobs_folder = index.config.path.join(BLOBS_FOLDER); - std::fs::create_dir_all(&blobs_folder)?; - fsync_directory(&blobs_folder)?; - - let blob_file_id_to_continue_with = index - .current_version() - .blob_files - .list_ids() - .max() - .map(|x| x + 1) - .unwrap_or_default(); - - index - .0 - .blob_file_id_counter - .set(blob_file_id_to_continue_with); - - Ok(Self { - index, - blobs_folder: Arc::new(blobs_folder), - }) - } -} - -impl AbstractTree for BlobTree { - fn print_trace(&self, key: &[u8]) -> crate::Result<()> { - self.index.print_trace(key) - } - - fn table_file_cache_size(&self) -> usize { - self.index.table_file_cache_size() - } - - fn get_version_history_lock( - &self, - ) -> std::sync::RwLockWriteGuard<'_, crate::version::SuperVersions> { - self.index.get_version_history_lock() - } - - fn next_table_id(&self) -> TableId { - self.index.next_table_id() - } - - fn id(&self) -> crate::TreeId { - self.index.id() - } - - fn get_internal_entry(&self, key: &[u8], seqno: SeqNo) -> crate::Result> { - self.index.get_internal_entry(key, seqno) - } - - fn current_version(&self) -> Version { - self.index.current_version() - } - - #[cfg(feature = "metrics")] - fn metrics(&self) -> &Arc { - self.index.metrics() - } - - fn version_free_list_len(&self) -> usize { - self.index.version_free_list_len() - } - - fn prefix>( - &self, - prefix: K, - seqno: SeqNo, - index: Option<(Arc, SeqNo)>, - ) -> Box + Send + 'static> { - use crate::range::prefix_to_range; - - let super_version = self.index.get_version_for_snapshot(seqno); - let tree = self.clone(); - - let range = prefix_to_range(prefix.as_ref()); - - Box::new( - crate::Tree::create_internal_range(super_version.clone(), &range, seqno, index).map( - move |kv| { - IterGuardImpl::Blob(Guard { - tree: tree.clone(), - version: super_version.version.clone(), - kv, - }) - }, - ), - ) - } - - fn range, R: RangeBounds>( - &self, - range: R, - seqno: SeqNo, - index: Option<(Arc, SeqNo)>, - ) -> Box + Send + 'static> { - let super_version = self.index.get_version_for_snapshot(seqno); - let tree = self.clone(); - - Box::new( - crate::Tree::create_internal_range(super_version.clone(), &range, seqno, index).map( - move |kv| { - IterGuardImpl::Blob(Guard { - tree: tree.clone(), - version: super_version.version.clone(), - kv, - }) - }, - ), - ) - } - - fn tombstone_count(&self) -> u64 { - self.index.tombstone_count() - } - - fn weak_tombstone_count(&self) -> u64 { - self.index.weak_tombstone_count() - } - - fn weak_tombstone_reclaimable_count(&self) -> u64 { - self.index.weak_tombstone_reclaimable_count() - } - - fn drop_range, R: RangeBounds>(&self, range: R) -> crate::Result<()> { - self.index.drop_range(range) - } - - fn clear(&self) -> crate::Result<()> { - let config = self.tree_config(); - let mut versions = self.get_version_history_lock(); - - versions.upgrade_version( - &config.path, - |v| { - let mut copy = v.clone(); - copy.active_memtable = - Arc::new(Memtable::new(self.index.memtable_id_counter.next())); - copy.sealed_memtables = Arc::default(); - copy.version = Version::new(v.version.id() + 1, self.tree_type()); - Ok(copy) - }, - &config.seqno, - &config.visible_seqno, - ) - } - - fn major_compact(&self, target_size: u64, seqno_threshold: SeqNo) -> crate::Result<()> { - self.index.major_compact(target_size, seqno_threshold) - } - - fn clear_active_memtable(&self) { - self.index.clear_active_memtable(); - } - - fn l0_run_count(&self) -> usize { - self.index.l0_run_count() - } - - fn blob_file_count(&self) -> usize { - self.current_version().blob_file_count() - } - - // NOTE: We skip reading from the value log - // because the vHandles already store the value size - fn size_of>(&self, key: K, seqno: SeqNo) -> crate::Result> { - let Some(item) = self.index.get_internal_entry(key.as_ref(), seqno)? else { - return Ok(None); - }; - - Ok(Some(if item.key.value_type.is_indirection() { - let mut cursor = std::io::Cursor::new(item.value); - let vptr = BlobIndirection::decode_from(&mut cursor)?; - vptr.size - } else { - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - { - item.value.len() as u32 - } - })) - } - - fn stale_blob_bytes(&self) -> u64 { - self.current_version().gc_stats().stale_bytes() - } - - fn filter_size(&self) -> u64 { - self.index.filter_size() - } - - fn pinned_filter_size(&self) -> usize { - self.index.pinned_filter_size() - } - - fn pinned_block_index_size(&self) -> usize { - self.index.pinned_block_index_size() - } - - fn sealed_memtable_count(&self) -> usize { - self.index.sealed_memtable_count() - } - - fn get_flush_lock(&self) -> MutexGuard<'_, ()> { - self.index.get_flush_lock() - } - - fn flush_to_tables( - &self, - stream: impl Iterator>, - ) -> crate::Result, Option>)>> { - use crate::{ - coding::Encode, file::BLOBS_FOLDER, file::TABLES_FOLDER, - table::multi_writer::MultiWriter, - }; - - let start = std::time::Instant::now(); - - let table_folder = self.index.config.path.join(TABLES_FOLDER); - - let data_block_size = self.index.config.data_block_size_policy.get(0); - - let data_block_restart_interval = - self.index.config.data_block_restart_interval_policy.get(0); - let index_block_restart_interval = - self.index.config.index_block_restart_interval_policy.get(0); - - let data_block_compression = self.index.config.data_block_compression_policy.get(0); - let index_block_compression = self.index.config.index_block_compression_policy.get(0); - - let data_block_hash_ratio = self.index.config.data_block_hash_ratio_policy.get(0); - - let index_partitioning = self.index.config.index_block_partitioning_policy.get(0); - let filter_partitioning = self.index.config.filter_block_partitioning_policy.get(0); - - log::debug!("Flushing memtable(s) and performing key-value separation, data_block_restart_interval={data_block_restart_interval}, index_block_restart_interval={index_block_restart_interval}, data_block_size={data_block_size}, data_block_compression={data_block_compression:?}, index_block_compression={index_block_compression:?}"); - log::debug!("=> to table(s) in {}", table_folder.display()); - log::debug!("=> to blob file(s) at {}", self.blobs_folder.display()); - - let mut table_writer = MultiWriter::new( - table_folder.clone(), - self.index.table_id_counter.clone(), - 64 * 1_024 * 1_024, - 0, - )? - .use_data_block_restart_interval(data_block_restart_interval) - .use_index_block_restart_interval(index_block_restart_interval) - .use_data_block_compression(data_block_compression) - .use_index_block_compression(index_block_compression) - .use_data_block_size(data_block_size) - .use_data_block_hash_ratio(data_block_hash_ratio) - .use_bloom_policy({ - use crate::config::FilterPolicyEntry::{Bloom, None}; - use crate::table::filter::BloomConstructionPolicy; - - match self.index.config.filter_policy.get(0) { - Bloom(policy) => policy, - None => BloomConstructionPolicy::BitsPerKey(0.0), - } - }); - - if index_partitioning { - table_writer = table_writer.use_partitioned_index(); - } - if filter_partitioning { - table_writer = table_writer.use_partitioned_filter(); - } - - #[expect( - clippy::expect_used, - reason = "cannot create blob tree without defining kv separation options" - )] - let kv_opts = self - .index - .config - .kv_separation_opts - .as_ref() - .expect("kv separation options should exist"); - - let mut blob_writer = BlobFileWriter::new( - self.index.0.blob_file_id_counter.clone(), - self.index.config.path.join(BLOBS_FOLDER), - self.id(), - self.index.config.descriptor_table.clone(), - )? - .use_target_size(kv_opts.file_target_size) - .use_compression(kv_opts.compression); - - let separation_threshold = kv_opts.separation_threshold; - - for item in stream { - let item = item?; - - if item.is_tombstone() { - // NOTE: Still need to add tombstone to index tree - // But no blob to blob writer - table_writer.write(InternalValue::new(item.key, UserValue::empty()))?; - continue; - } - - let value = item.value; - - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - let value_size = value.len() as u32; - - if value_size >= separation_threshold { - let vhandle = blob_writer.write(&item.key.user_key, item.key.seqno, &value)?; - - let indirection = BlobIndirection { - vhandle, - size: value_size, - }; - - table_writer.write({ - let mut vptr = - InternalValue::new(item.key.clone(), indirection.encode_into_vec()); - vptr.key.value_type = crate::ValueType::Indirection; - vptr - })?; - - table_writer.register_blob(indirection); - } else { - table_writer.write(InternalValue::new(item.key, value))?; - } - } - - let blob_files = blob_writer.finish()?; - - let result = table_writer.finish()?; - - log::debug!("Flushed memtable(s) in {:?}", start.elapsed()); - - let pin_filter = self.index.config.filter_block_pinning_policy.get(0); - let pin_index = self.index.config.index_block_pinning_policy.get(0); - - // Load tables - let tables = result - .into_iter() - .map(|(table_id, checksum)| -> crate::Result
{ - Table::recover( - table_folder.join(table_id.to_string()), - checksum, - 0, - self.index.id, - self.index.config.cache.clone(), - self.index.config.descriptor_table.clone(), - pin_filter, - pin_index, - #[cfg(feature = "metrics")] - self.index.metrics.clone(), - ) - }) - .collect::>>()?; - - Ok(Some((tables, Some(blob_files)))) - } - - fn register_tables( - &self, - tables: &[Table], - blob_files: Option<&[BlobFile]>, - frag_map: Option, - sealed_memtables_to_delete: &[MemtableId], - gc_watermark: SeqNo, - ) -> crate::Result<()> { - self.index.register_tables( - tables, - blob_files, - frag_map, - sealed_memtables_to_delete, - gc_watermark, - ) - } - - fn compact( - &self, - strategy: Arc, - seqno_threshold: SeqNo, - ) -> crate::Result<()> { - self.index.compact(strategy, seqno_threshold) - } - - fn get_next_table_id(&self) -> TableId { - self.index.get_next_table_id() - } - - fn tree_config(&self) -> &Config { - &self.index.config - } - - fn get_highest_seqno(&self) -> Option { - self.index.get_highest_seqno() - } - - fn active_memtable(&self) -> Arc { - self.index.active_memtable() - } - - fn rotate_memtable(&self) -> Option> { - self.index.rotate_memtable() - } - - fn table_count(&self) -> usize { - self.index.table_count() - } - - fn level_table_count(&self, idx: usize) -> Option { - self.index.level_table_count(idx) - } - - fn approximate_len(&self) -> usize { - self.index.approximate_len() - } - - // NOTE: Override the default implementation to not fetch - // data from the value log, so we get much faster key reads - fn is_empty(&self, seqno: SeqNo, index: Option<(Arc, SeqNo)>) -> crate::Result { - self.index.is_empty(seqno, index) - } - - // NOTE: Override the default implementation to not fetch - // data from the value log, so we get much faster key reads - fn contains_key>(&self, key: K, seqno: SeqNo) -> crate::Result { - self.index.contains_key(key, seqno) - } - - // NOTE: Override the default implementation to not fetch - // data from the value log, so we get much faster scans - fn len(&self, seqno: SeqNo, index: Option<(Arc, SeqNo)>) -> crate::Result { - self.index.len(seqno, index) - } - - fn disk_space(&self) -> u64 { - let version = self.current_version(); - self.index.disk_space() + version.blob_files.on_disk_size() - } - - fn get_highest_memtable_seqno(&self) -> Option { - self.index.get_highest_memtable_seqno() - } - - fn get_highest_persisted_seqno(&self) -> Option { - self.index.get_highest_persisted_seqno() - } - - fn insert, V: Into>( - &self, - key: K, - value: V, - seqno: SeqNo, - ) -> (u64, u64) { - self.index.insert(key, value.into(), seqno) - } - - fn get>(&self, key: K, seqno: SeqNo) -> crate::Result> { - let key = key.as_ref(); - - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - let super_version = self - .index - .version_history - .read() - .expect("lock is poisoned") - .get_version_for_snapshot(seqno); - - let Some(item) = crate::Tree::get_internal_entry_from_version(&super_version, key, seqno)? - else { - return Ok(None); - }; - - let (_, v) = resolve_value_handle( - self.id(), - self.blobs_folder.as_path(), - &self.index.config.cache, - &super_version.version, - item, - )?; - - Ok(Some(v)) - } - - fn remove>(&self, key: K, seqno: SeqNo) -> (u64, u64) { - self.index.remove(key, seqno) - } - - fn remove_weak>(&self, key: K, seqno: SeqNo) -> (u64, u64) { - self.index.remove_weak(key, seqno) - } -} diff --git a/crates/lsm-tree/src/cache.rs b/crates/lsm-tree/src/cache.rs index 465c80c8c..ce01ecfda 100644 --- a/crates/lsm-tree/src/cache.rs +++ b/crates/lsm-tree/src/cache.rs @@ -2,45 +2,31 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) +use crate::GlobalTableId; use crate::table::block::Header; use crate::table::{Block, BlockOffset}; -use crate::{GlobalTableId, UserValue}; -use quick_cache::sync::Cache as QuickCache; use quick_cache::Weighter; - -const TAG_BLOCK: u8 = 0; -const TAG_BLOB: u8 = 1; - -#[derive(Clone)] -enum Item { - Block(Block), - Blob(UserValue), -} +use quick_cache::sync::Cache as QuickCache; #[derive(Eq, std::hash::Hash, PartialEq)] -struct CacheKey(u8, u64, u64, u64); +struct CacheKey(GlobalTableId, u64); -impl From<(u8, u64, u64, u64)> for CacheKey { - fn from((tag, root_id, table_id, offset): (u8, u64, u64, u64)) -> Self { - Self(tag, root_id, table_id, offset) +impl CacheKey { + fn from_id(id: GlobalTableId, offset: BlockOffset) -> Self { + Self(id, *offset) } } #[derive(Clone)] struct BlockWeighter; -impl Weighter for BlockWeighter { - fn weight(&self, _: &CacheKey, item: &Item) -> u64 { - use Item::{Blob, Block}; - - match item { - Block(b) => (Header::serialized_len() as u64) + u64::from(b.header.uncompressed_length), - Blob(b) => b.len() as u64, - } +impl Weighter for BlockWeighter { + fn weight(&self, _: &CacheKey, block: &Block) -> u64 { + (Header::serialized_len() as u64) + u64::from(block.header.uncompressed_length) } } -/// Cache, in which blocks or blobs are cached in-memory +/// Cache in which table blocks are cached in memory /// after being retrieved from disk /// /// This speeds up consecutive queries to nearby data, improving @@ -67,7 +53,7 @@ impl Weighter for BlockWeighter { pub struct Cache { // NOTE: rustc_hash performed best: https://fjall-rs.github.io/post/fjall-2-1 /// Concurrent cache implementation - data: QuickCache, + data: QuickCache, /// Capacity in bytes capacity: u64, @@ -115,47 +101,12 @@ impl Cache { #[doc(hidden)] #[must_use] pub fn get_block(&self, id: GlobalTableId, offset: BlockOffset) -> Option { - let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into(); - - Some(match self.data.get(&key)? { - Item::Block(block) => block, - Item::Blob(_) => unreachable!("invalid cache item"), - }) + let key = CacheKey::from_id(id, offset); + self.data.get(&key) } #[doc(hidden)] pub fn insert_block(&self, id: GlobalTableId, offset: BlockOffset, block: Block) { - self.data.insert( - (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into(), - Item::Block(block), - ); - } - - #[doc(hidden)] - pub fn insert_blob( - &self, - vlog_id: crate::TreeId, - vhandle: &crate::vlog::ValueHandle, - value: UserValue, - ) { - self.data.insert( - (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into(), - Item::Blob(value), - ); - } - - #[doc(hidden)] - #[must_use] - pub fn get_blob( - &self, - vlog_id: crate::TreeId, - vhandle: &crate::vlog::ValueHandle, - ) -> Option { - let key: CacheKey = (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into(); - - Some(match self.data.get(&key)? { - Item::Blob(blob) => blob, - Item::Block(_) => unreachable!("invalid cache item"), - }) + self.data.insert(CacheKey::from_id(id, offset), block); } } diff --git a/crates/lsm-tree/src/compaction/drop_range.rs b/crates/lsm-tree/src/compaction/drop_range.rs deleted file mode 100644 index 4134a195b..000000000 --- a/crates/lsm-tree/src/compaction/drop_range.rs +++ /dev/null @@ -1,100 +0,0 @@ -// 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::{Choice, CompactionStrategy}; -use crate::compaction::state::CompactionState; -use crate::version::Version; -use crate::{config::Config, slice::Slice, version::run::Ranged, KeyRange}; -use crate::{HashSet, Table}; -use std::ops::{Bound, RangeBounds}; - -#[derive(Clone, Debug)] -pub struct OwnedBounds { - pub start: Bound, - pub end: Bound, -} - -impl RangeBounds for OwnedBounds { - fn start_bound(&self) -> Bound<&Slice> { - match &self.start { - Bound::Unbounded => Bound::Unbounded, - Bound::Included(key) => Bound::Included(key), - Bound::Excluded(key) => Bound::Excluded(key), - } - } - - fn end_bound(&self) -> Bound<&Slice> { - match &self.end { - Bound::Unbounded => Bound::Unbounded, - Bound::Included(key) => Bound::Included(key), - Bound::Excluded(key) => Bound::Excluded(key), - } - } -} - -impl OwnedBounds { - #[must_use] - pub fn contains(&self, range: &KeyRange) -> bool { - let lower_ok = match &self.start { - Bound::Unbounded => true, - Bound::Included(key) => key.as_ref() <= range.min().as_ref(), - Bound::Excluded(key) => key.as_ref() < range.min().as_ref(), - }; - - if !lower_ok { - return false; - } - - match &self.end { - Bound::Unbounded => true, - Bound::Included(key) => key.as_ref() >= range.max().as_ref(), - Bound::Excluded(key) => key.as_ref() > range.max().as_ref(), - } - } -} - -/// Drops all tables that are **contained** in a key range -pub struct Strategy { - bounds: OwnedBounds, -} - -impl Strategy { - /// Configures a new `DropRange` compaction strategy. - #[must_use] - pub fn new(bounds: OwnedBounds) -> Self { - Self { bounds } - } -} - -impl CompactionStrategy for Strategy { - fn get_name(&self) -> &'static str { - "DropRangeCompaction" - } - - fn choose(&self, version: &Version, _: &Config, state: &CompactionState) -> Choice { - let table_ids: HashSet<_> = version - .iter_levels() - .flat_map(|lvl| lvl.iter()) - .flat_map(|run| { - run.range_overlap_indexes(&self.bounds) - .and_then(|(lo, hi)| run.get(lo..=hi)) - .unwrap_or_default() - .iter() - .filter(|x| self.bounds.contains(x.key_range())) - }) - .map(Table::id) - .collect(); - - // NOTE: This should generally not occur because of the - // tree-level major compaction lock - // But just as a fail-safe... - let some_hidden = table_ids.iter().any(|&id| state.hidden_set().is_hidden(id)); - - if some_hidden { - Choice::DoNothing - } else { - Choice::Drop(table_ids) - } - } -} diff --git a/crates/lsm-tree/src/compaction/fifo.rs b/crates/lsm-tree/src/compaction/fifo.rs deleted file mode 100644 index 7c2a17fd1..000000000 --- a/crates/lsm-tree/src/compaction/fifo.rs +++ /dev/null @@ -1,310 +0,0 @@ -// 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::{Choice, CompactionStrategy}; -use crate::{ - compaction::state::CompactionState, config::Config, time::unix_timestamp, version::Version, - HashSet, KvPair, -}; - -#[doc(hidden)] -pub const NAME: &str = "FifoCompaction"; - -/// FIFO-style compaction -/// -/// Limits the tree size to roughly `limit` bytes, deleting the oldest table(s) -/// when the threshold is reached. -/// -/// Will also merge tables if the number of tables in level 0 grows too much, which -/// could cause write stalls. -/// -/// Additionally, a (lazy) TTL can be configured to drop old tables. -/// -/// ###### Caution -/// -/// Only use it for specific workloads where: -/// -/// 1) You only want to store recent data (unimportant logs, ...) -/// 2) The key order of inserts is strictly monotonically increasing or decreasing -/// 3) You only insert new data (no updates/deletes) -#[derive(Clone)] -pub struct Strategy { - /// Data set size limit in bytes - pub limit: u64, - - /// TTL in seconds, will be disabled if 0 or None - pub ttl_seconds: Option, -} - -impl Strategy { - /// Configures a new `Fifo` compaction strategy - #[must_use] - pub fn new(limit: u64, ttl_seconds: Option) -> Self { - Self { limit, ttl_seconds } - } -} - -impl CompactionStrategy for Strategy { - fn get_name(&self) -> &'static str { - NAME - } - - fn get_config(&self) -> Vec { - vec![ - ( - crate::UserKey::from("fifo_limit"), - crate::UserValue::from(self.limit.to_le_bytes()), - ), - ( - crate::UserKey::from("fifo_ttl"), - crate::UserValue::from(if self.ttl_seconds.is_some() { - [1u8] - } else { - [0u8] - }), - ), - ( - crate::UserKey::from("fifo_ttl_seconds"), - crate::UserValue::from(self.ttl_seconds.map(u64::to_le_bytes).unwrap_or_default()), - ), - ] - } - - fn choose(&self, version: &Version, _: &Config, state: &CompactionState) -> Choice { - let first_level = version.l0(); - - // Early return avoids unnecessary work and keeps FIFO a no-op when there is nothing to do. - if first_level.is_empty() { - return Choice::DoNothing; - } - - assert!(first_level.is_disjoint(), "L0 needs to be disjoint"); - - assert!( - !version.level_is_busy(0, state.hidden_set()), - "FIFO compaction never compacts", - ); - - // Account for both table file bytes and value-log (blob) bytes to enforce the true space limit. - let db_size = first_level.size() + version.blob_files.on_disk_size(); - - let mut ids_to_drop = HashSet::default(); - - // Compute TTL cutoff once and perform a single pass to mark expired tables and - // accumulate their sizes. Also collect non-expired tables for possible size-based drops. - let ttl_cutoff = match self.ttl_seconds { - Some(s) if s > 0 => Some( - unix_timestamp() - .as_nanos() - .saturating_sub(u128::from(s) * 1_000_000_000u128), - ), - _ => None, - }; - - let mut ttl_dropped_bytes = 0u64; - let mut alive = Vec::new(); - - for table in first_level.iter().flat_map(|run| run.iter()) { - let expired = - ttl_cutoff.is_some_and(|cutoff| u128::from(table.metadata.created_at) <= cutoff); - - if expired { - ids_to_drop.insert(table.id()); - let linked_blob_file_bytes = table.referenced_blob_bytes().unwrap_or_default(); - ttl_dropped_bytes += table.file_size() + linked_blob_file_bytes; - } else { - alive.push(table); - } - } - - // Subtract TTL-selected bytes to see if we're still over the limit. - let size_after_ttl = db_size.saturating_sub(ttl_dropped_bytes); - - // If we still exceed the limit, drop additional oldest tables until within the limit. - if size_after_ttl > self.limit { - let overshoot = size_after_ttl - self.limit; - - let mut collected_bytes = 0; - - // Oldest-first list by creation time from the non-expired set. - alive.sort_by_key(|t| t.metadata.created_at); - - for table in alive { - if collected_bytes >= overshoot { - break; - } - - ids_to_drop.insert(table.id()); - - let linked_blob_file_bytes = table.referenced_blob_bytes().unwrap_or_default(); - collected_bytes += table.file_size() + linked_blob_file_bytes; - } - } - - if ids_to_drop.is_empty() { - Choice::DoNothing - } else { - Choice::Drop(ids_to_drop) - } - } -} - -#[cfg(test)] -mod tests { - use super::Strategy; - use crate::{AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter}; - use std::sync::Arc; - - #[test] - fn fifo_empty_levels() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - let fifo = Arc::new(Strategy::new(1, None)); - tree.compact(fifo, 0)?; - - assert_eq!(0, tree.table_count()); - Ok(()) - } - - #[test] - fn fifo_below_limit() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - for i in 0..4u8 { - tree.insert([b'k', i].as_slice(), "v", u64::from(i)); - tree.flush_active_memtable(u64::from(i))?; - } - - let before = tree.table_count(); - let fifo = Arc::new(Strategy::new(u64::MAX, None)); - tree.compact(fifo, 4)?; - - assert_eq!(before, tree.table_count()); - Ok(()) - } - - #[test] - fn fifo_more_than_limit() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - for i in 0..4u8 { - tree.insert([b'k', i].as_slice(), "v", u64::from(i)); - tree.flush_active_memtable(u64::from(i))?; - } - - let before = tree.table_count(); - // Very small limit forces dropping oldest tables - let fifo = Arc::new(Strategy::new(1, None)); - tree.compact(fifo, 4)?; - - assert!(tree.table_count() < before); - Ok(()) - } - - #[test] - fn fifo_more_than_limit_blobs() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for i in 0..3u8 { - tree.insert([b'k', i].as_slice(), "$", u64::from(i)); - tree.flush_active_memtable(u64::from(i))?; - } - - let before = tree.table_count(); - let fifo = Arc::new(Strategy::new(1, None)); - tree.compact(fifo, 3)?; - - assert!(tree.table_count() < before); - Ok(()) - } - - #[test] - fn fifo_ttl() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - // Freeze time and create first (older) table at t=1000s - crate::time::set_unix_timestamp_for_test(Some(std::time::Duration::from_secs(1_000))); - tree.insert("a", "1", 0); - tree.flush_active_memtable(0)?; - - // Advance time and create second (newer) table at t=1005s - crate::time::set_unix_timestamp_for_test(Some(std::time::Duration::from_secs(1_005))); - tree.insert("b", "2", 1); - tree.flush_active_memtable(1)?; - - // Now set current time to t=1011s; with TTL=10s, cutoff=1001s => drop first only - crate::time::set_unix_timestamp_for_test(Some(std::time::Duration::from_secs(1_011))); - - assert_eq!(2, tree.table_count()); - - let fifo = Arc::new(Strategy::new(u64::MAX, Some(10))); - tree.compact(fifo, 2)?; - - assert_eq!(1, tree.table_count()); - - // Reset override - crate::time::set_unix_timestamp_for_test(None); - Ok(()) - } - - #[test] - fn fifo_ttl_then_limit_additional_drops_blob_unit() -> crate::Result<()> { - let dir = tempfile::tempdir()?; - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - // Create two tables; we will expire them via time override and force additional drops via limit. - tree.insert("a", "$", 0); - tree.flush_active_memtable(0)?; - tree.insert("b", "$", 1); - tree.flush_active_memtable(1)?; - - crate::time::set_unix_timestamp_for_test(Some(std::time::Duration::from_secs(10_000_000))); - - // TTL=1s will mark both expired; very small limit ensures size-based collection path is also exercised. - let fifo = Arc::new(Strategy::new(1, Some(1))); - tree.compact(fifo, 2)?; - - assert_eq!(0, tree.table_count()); - - crate::time::set_unix_timestamp_for_test(None); - Ok(()) - } -} diff --git a/crates/lsm-tree/src/compaction/filter.rs b/crates/lsm-tree/src/compaction/filter.rs index e1f6f045b..fe06347ea 100644 --- a/crates/lsm-tree/src/compaction/filter.rs +++ b/crates/lsm-tree/src/compaction/filter.rs @@ -3,17 +3,10 @@ //! Compaction filters allow users to run custom logic during compactions, e.g. custom cleanup rules such as TTL. //! Because compactions run in background workers, using compactions filters instead of scans can massively increase the efficiency of the storage engine. use crate::{ - coding::{Decode, Encode}, - compaction::{ - stream::{StreamFilter, StreamFilterVerdict}, - worker::Options, - }, - key::InternalKey, - version::Version, - vlog::{Accessor, BlobFileWriter, ValueHandle}, - BlobIndirection, InternalValue, KvSeparationOptions, UserKey, UserValue, ValueType, + InternalValue, UserKey, UserValue, ValueType, + compaction::stream::{StreamFilter, StreamFilterVerdict}, }; -use std::{panic::RefUnwindSafe, path::Path}; +use std::panic::RefUnwindSafe; /// Verdict returned by a [`CompactionFilter`]. #[non_exhaustive] @@ -91,33 +84,9 @@ pub trait Factory: Send + Sync + RefUnwindSafe { fn make_filter(&self, ctx: &Context) -> Box; } -struct AccessorShared<'a> { - opts: &'a Options, - version: &'a Version, - blobs_folder: &'a Path, -} - -impl AccessorShared<'_> { - /// Fetches a value from the blob store. - fn get_indirect_value( - &self, - user_key: &[u8], - vhandle: &ValueHandle, - ) -> crate::Result> { - Accessor::new(&self.version.blob_files).get( - self.opts.tree_id, - self.blobs_folder, - user_key, - vhandle, - &self.opts.config.cache, - ) - } -} - /// Accessor for the key/value from a compaction filter pub struct ItemAccessor<'a> { item: &'a InternalValue, - shared: &'a AccessorShared<'a>, } impl<'a> ItemAccessor<'a> { @@ -127,45 +96,16 @@ impl<'a> ItemAccessor<'a> { &self.item.key.user_key } - /// Returns whether this item's value is stored separately. - #[must_use] - #[doc(hidden)] - pub fn is_indirection(&self) -> bool { - self.item.key.value_type.is_indirection() - } - /// Get the value of this item. /// /// # Errors /// - /// This method will return an error if blob retrieval fails. + /// This method will return an error if reading the value fails. pub fn value(&self) -> crate::Result { - match self.item.key.value_type { - crate::ValueType::Value => Ok(self.item.value.clone()), - crate::ValueType::Indirection => { - // Resolve and read the value from a blob file. - let mut reader = &self.item.value[..]; - let indirection = BlobIndirection::decode_from(&mut reader)?; - let vhandle = indirection.vhandle; - - let value = self - .shared - .get_indirect_value(&self.item.key.user_key, &vhandle)?; - - if let Some(value) = value { - Ok(value) - } else { - log::error!( - "failed to read referenced blob file during execution of compaction filter. key: {:?}, vptr: {indirection:?}", - self.item.key, - ); - Err(crate::Error::Unrecoverable) - } - } - crate::ValueType::WeakTombstone | crate::ValueType::Tombstone => { - unreachable!("tombstones are filtered out before calling filter") - } + if self.item.is_tombstone() { + unreachable!("tombstones are filtered out before calling filter"); } + Ok(self.item.value.clone()) } } @@ -174,76 +114,12 @@ impl<'a> ItemAccessor<'a> { // NOTE: this slightly helps insulate CompactionStream from lifetime spam pub(crate) struct StreamFilterAdapter<'a, 'b: 'a> { filter: Option<&'a mut (dyn CompactionFilter + 'b)>, - shared: AccessorShared<'a>, - blob_opts: Option<&'a KvSeparationOptions>, - blob_writer: &'a mut Option, ctx: &'a Context, } impl<'a, 'b: 'a> StreamFilterAdapter<'a, 'b> { - pub fn new( - filter: Option<&'a mut (dyn CompactionFilter + 'b)>, - opts: &'a Options, - version: &'a Version, - blobs_folder: &'a Path, - blob_writer: &'a mut Option, - ctx: &'a Context, - ) -> Self { - Self { - filter, - shared: AccessorShared { - opts, - version, - blobs_folder, - }, - blob_opts: opts.config.kv_separation_opts.as_ref(), - blob_writer, - ctx, - } - } - - /// Redirects a write to a blob file if KV separation is enabled and - /// the value meets the separation threshold. - fn handle_write( - &mut self, - prev_key: &InternalKey, - new_value: UserValue, - ) -> crate::Result<(ValueType, UserValue)> { - let Some(blob_opts) = self.blob_opts else { - return Ok((ValueType::Value, new_value)); - }; - - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - let value_size = new_value.len() as u32; - - if value_size < blob_opts.separation_threshold { - return Ok((ValueType::Value, new_value)); - } - - let writer = if let Some(writer) = self.blob_writer { - writer - } else { - // Instantiate writer as necessary - let writer = BlobFileWriter::new( - self.shared.opts.blob_file_id_generator.clone(), - self.shared.blobs_folder, - self.shared.opts.tree_id, - self.shared.opts.config.descriptor_table.clone(), - )? - .use_target_size(blob_opts.file_target_size) - .use_compression(blob_opts.compression); - - self.blob_writer.insert(writer) - }; - - let vhandle = writer.write(&prev_key.user_key, prev_key.seqno, &new_value)?; - - let indirection = BlobIndirection { - vhandle, - size: value_size, - }; - - Ok((ValueType::Indirection, indirection.encode_into_vec().into())) + pub fn new(filter: Option<&'a mut (dyn CompactionFilter + 'b)>, ctx: &'a Context) -> Self { + Self { filter, ctx } } } @@ -253,13 +129,7 @@ impl<'a, 'b: 'a> StreamFilter for StreamFilterAdapter<'a, 'b> { return Ok(StreamFilterVerdict::Keep); }; - match filter.filter_item( - ItemAccessor { - item, - shared: &self.shared, - }, - self.ctx, - )? { + match filter.filter_item(ItemAccessor { item }, self.ctx)? { Verdict::Destroy => Ok(StreamFilterVerdict::Drop), Verdict::Keep => Ok(StreamFilterVerdict::Keep), Verdict::Remove => Ok(StreamFilterVerdict::Replace(( @@ -270,9 +140,9 @@ impl<'a, 'b: 'a> StreamFilter for StreamFilterAdapter<'a, 'b> { ValueType::WeakTombstone, UserValue::empty(), ))), - Verdict::ReplaceValue(new_value) => self - .handle_write(&item.key, new_value) - .map(StreamFilterVerdict::Replace), + Verdict::ReplaceValue(new_value) => { + Ok(StreamFilterVerdict::Replace((ValueType::Value, new_value))) + } } } } diff --git a/crates/lsm-tree/src/compaction/flavour.rs b/crates/lsm-tree/src/compaction/flavour.rs index 9a5b34723..73ef739e9 100644 --- a/crates/lsm-tree/src/compaction/flavour.rs +++ b/crates/lsm-tree/src/compaction/flavour.rs @@ -2,64 +2,28 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::blob_tree::handle::BlobIndirection; -use crate::blob_tree::FragmentationMap; -use crate::coding::{Decode, Encode}; -use crate::compaction::worker::Options; -use crate::compaction::Input as CompactionPayload; -use crate::file::TABLES_FOLDER; -use crate::table::multi_writer::MultiWriter; -use crate::version::{SuperVersions, Version}; -use crate::vlog::blob_file::scanner::ScanEntry; -use crate::vlog::{BlobFileId, BlobFileMergeScanner, BlobFileWriter}; -use crate::{BlobFile, HashSet, InternalValue, Table}; -use std::iter::Peekable; +use crate::{ + InternalValue, Table, + compaction::{Input as CompactionPayload, worker::Options}, + file::TABLES_FOLDER, + table::multi_writer::MultiWriter, + version::{SuperVersions, Version}, +}; use std::time::Instant; -/// Drains all blobs that come "before" the given vptr. -fn drain_blobs>>( - scanner: &mut Peekable, - key: &[u8], - vptr: &BlobIndirection, -) -> crate::Result<()> { - loop { - let Some(blob) = scanner.next_if(|x| match x { - Ok((entry, blob_file_id)) => { - entry.key != key - || (*blob_file_id != vptr.vhandle.blob_file_id) - || (entry.offset < vptr.vhandle.offset) - } - Err(_) => true, - }) else { - break; - }; - let (entry, _) = blob?; - - assert!(entry.key <= key, "vptr was not matched with blob"); - } - - Ok(()) -} - pub(super) fn prepare_table_writer( version: &Version, opts: &Options, payload: &CompactionPayload, ) -> crate::Result { let table_base_folder = opts.config.path.join(TABLES_FOLDER); - let dst_lvl = payload.canonical_level.into(); - let data_block_size = opts.config.data_block_size_policy.get(dst_lvl); - let data_block_restart_interval = opts.config.data_block_restart_interval_policy.get(dst_lvl); let index_block_restart_interval = opts.config.index_block_restart_interval_policy.get(dst_lvl); - let data_block_compression = opts.config.data_block_compression_policy.get(dst_lvl); let index_block_compression = opts.config.index_block_compression_policy.get(dst_lvl); - let data_block_hash_ratio = opts.config.data_block_hash_ratio_policy.get(dst_lvl); - let index_partitioning = opts.config.index_block_partitioning_policy.get(dst_lvl); let filter_partitioning = opts.config.filter_block_partitioning_policy.get(dst_lvl); @@ -116,219 +80,7 @@ pub(super) fn prepare_table_writer( })) } -// TODO: find a better name -pub(super) trait CompactionFlavour { - fn write(&mut self, item: InternalValue) -> crate::Result<()>; - - #[warn(clippy::too_many_arguments)] - fn finish( - self: Box, - super_version: &mut SuperVersions, - opts: &Options, - payload: &CompactionPayload, - dst_lvl: usize, - blob_frag_map: FragmentationMap, - extra_blob_files: Vec, - ) -> crate::Result<()>; -} - -/// Compaction worker that will relocate blobs that sit in blob files that are being rewritten -pub struct RelocatingCompaction { - inner: StandardCompaction, - blob_scanner: Peekable, - blob_writer: BlobFileWriter, - rewriting_blob_file_ids: HashSet, - rewriting_blob_files: Vec, -} - -impl RelocatingCompaction { - pub fn new( - inner: StandardCompaction, - blob_scanner: Peekable, - blob_writer: BlobFileWriter, - rewriting_blob_files: Vec, - ) -> Self { - Self { - inner, - blob_scanner, - blob_writer, - rewriting_blob_file_ids: rewriting_blob_files.iter().map(BlobFile::id).collect(), - rewriting_blob_files, - } - } - - // TODO: vvv validate/unit test this vvv - fn drain_blobs(&mut self, key: &[u8], indirection: &BlobIndirection) -> crate::Result<()> { - drain_blobs(&mut self.blob_scanner, key, indirection) - } -} - -impl CompactionFlavour for RelocatingCompaction { - fn write(&mut self, item: InternalValue) -> crate::Result<()> { - if item.key.value_type.is_indirection() { - let mut reader = &item.value[..]; - - let indirection = BlobIndirection::decode_from(&mut reader).inspect_err(|e| { - log::error!("Failed to deserialize blob indirection {item:?}: {e:?}"); - })?; - - log::trace!( - "{:?}:{} => encountered indirection: {indirection:?}", - item.key.user_key, - item.key.seqno, - ); - - let indirection = if self - .rewriting_blob_file_ids - .contains(&indirection.vhandle.blob_file_id) - { - self.drain_blobs(&item.key.user_key, &indirection)?; - - #[expect(clippy::expect_used, reason = "vptr is expected to match with blob")] - let (blob_entry, blob_file_id) = self - .blob_scanner - .next() - .expect("vptr was not matched with blob (scanner is unexpectedly exhausted)")?; - - assert_eq!( - blob_file_id, indirection.vhandle.blob_file_id, - "matched blob has different blob file ID than vptr", - ); - assert_eq!( - blob_entry.key, item.key.user_key, - "matched blob has different key than vptr", - ); - assert_eq!( - blob_entry.offset, indirection.vhandle.offset, - "matched blob has different offset than vptr", - ); - - log::trace!( - "=> use blob: {:?}:{} offset: {} from BF {}", - blob_entry.key, - blob_entry.seqno, - blob_entry.offset, - blob_file_id, - ); - - log::trace!("RELOCATE to {indirection:?}"); - - let new_indirection = BlobIndirection { - vhandle: self.blob_writer.write_raw( - &item.key.user_key, - item.key.seqno, - &blob_entry.value, - blob_entry.uncompressed_len, - )?, - size: indirection.size, - }; - - debug_assert_eq!( - new_indirection.vhandle.on_disk_size, indirection.vhandle.on_disk_size, - "redirecting blob should not change its size", - ); - - self.inner - .table_writer - .write(InternalValue::from_components( - item.key.user_key, - new_indirection.encode_into_vec(), - item.key.seqno, - crate::ValueType::Indirection, - ))?; - - new_indirection - } else { - // This blob is not part of the rewritten blob files - // So just pass it through - log::trace!("Pass through {indirection:?} because it is not being relocated"); - self.inner.table_writer.write(item)?; - - indirection - }; - - self.inner.table_writer.register_blob(indirection); - } else { - self.inner.table_writer.write(item)?; - } - - Ok(()) - } - - fn finish( - mut self: Box, - super_version: &mut SuperVersions, - opts: &Options, - payload: &CompactionPayload, - dst_lvl: usize, - blob_frag_map_diff: FragmentationMap, - extra_blob_files: Vec, - ) -> crate::Result<()> { - log::debug!( - "Relocating compaction done in {:?}", - self.inner.start.elapsed(), - ); - - let table_ids_to_delete = std::mem::take(&mut self.inner.tables_to_rewrite); - - let created_tables = self.inner.consume_writer(opts, dst_lvl)?; - let mut created_blob_files = self.blob_writer.finish()?; - created_blob_files.extend(extra_blob_files); - - let mut blob_files_to_drop = self.rewriting_blob_files; - - let current_version = super_version.latest_version(); - - for blob_file in current_version.version.blob_files.iter() { - if blob_file.is_dead(current_version.version.gc_stats()) { - blob_files_to_drop.push(blob_file.clone()); - } - } - - super_version.upgrade_version( - &opts.config.path, - |current| { - let mut copy = current.clone(); - - copy.version = copy.version.with_merge( - &payload.table_ids.iter().copied().collect::>(), - &created_tables, - payload.dest_level as usize, - if blob_frag_map_diff.is_empty() { - None - } else { - Some(blob_frag_map_diff) - }, - created_blob_files, - &blob_files_to_drop - .iter() - .map(BlobFile::id) - .collect::>(), - ); - - Ok(copy) - }, - &opts.global_seqno, - &opts.visible_seqno, - )?; - - // NOTE: If the application were to crash >here< it's fine - // The tables/blob files are not referenced anymore, and will be - // cleaned up upon recovery - for table in table_ids_to_delete { - table.mark_as_deleted(); - } - - for blob_file in blob_files_to_drop { - blob_file.mark_as_deleted(); - } - - Ok(()) - } -} - -/// Standard compaction worker that just passes through all its data -pub struct StandardCompaction { +pub(super) struct StandardCompaction { start: Instant, table_writer: MultiWriter, tables_to_rewrite: Vec
, @@ -343,16 +95,18 @@ impl StandardCompaction { } } - fn consume_writer(self, opts: &Options, dst_lvl: usize) -> crate::Result> { - let table_base_folder = self.table_writer.base_path.clone(); + pub fn write(&mut self, item: InternalValue) -> crate::Result<()> { + self.table_writer.write(item) + } + fn consume_writer(self, opts: &Options, dst_lvl: usize) -> crate::Result> { let pin_filter = opts.config.filter_block_pinning_policy.get(dst_lvl); let pin_index = opts.config.index_block_pinning_policy.get(dst_lvl); + let (table_base_folder, results) = self.table_writer.finish()?; - self.table_writer - .finish()? + results .into_iter() - .map(|(table_id, checksum)| -> crate::Result
{ + .map(|(table_id, checksum)| { Table::recover( table_base_folder.join(table_id.to_string()), checksum, @@ -362,182 +116,42 @@ impl StandardCompaction { opts.config.descriptor_table.clone(), pin_filter, pin_index, - #[cfg(feature = "metrics")] - opts.metrics.clone(), ) }) - .collect::>>() - } -} - -impl CompactionFlavour for StandardCompaction { - fn write(&mut self, item: InternalValue) -> crate::Result<()> { - let indirection = if item.key.value_type.is_indirection() { - Some({ - let mut reader = &item.value[..]; - BlobIndirection::decode_from(&mut reader)? - }) - } else { - None - }; - - self.table_writer.write(item)?; - - if let Some(indirection) = indirection { - self.table_writer.register_blob(indirection); - } - - Ok(()) + .collect() } - fn finish( - mut self: Box, + pub fn finish( + mut self, super_version: &mut SuperVersions, opts: &Options, payload: &CompactionPayload, dst_lvl: usize, - blob_frag_map: FragmentationMap, - extra_blob_files: Vec, ) -> crate::Result<()> { log::debug!("Compaction done in {:?}", self.start.elapsed()); - let table_ids_to_delete = std::mem::take(&mut self.tables_to_rewrite); - + let tables_to_delete = std::mem::take(&mut self.tables_to_rewrite); let created_tables = self.consume_writer(opts, dst_lvl)?; - let mut blob_files_to_drop = Vec::default(); - - let current_version = super_version.latest_version(); - - for blob_file in current_version.version.blob_files.iter() { - if blob_file.is_dead(current_version.version.gc_stats()) { - blob_files_to_drop.push(blob_file.clone()); - } - } - super_version.upgrade_version( &opts.config.path, |current| { let mut copy = current.clone(); - copy.version = copy.version.with_merge( &payload.table_ids.iter().copied().collect::>(), &created_tables, payload.dest_level as usize, - if blob_frag_map.is_empty() { - None - } else { - Some(blob_frag_map) - }, - extra_blob_files, - &blob_files_to_drop - .iter() - .map(BlobFile::id) - .collect::>(), ); - Ok(copy) }, &opts.global_seqno, &opts.visible_seqno, )?; - // NOTE: If the application were to crash >here< it's fine - // The tables are not referenced anymore, and will be - // cleaned up upon recovery - for table in table_ids_to_delete { + for table in tables_to_delete { table.mark_as_deleted(); } - for blob_file in blob_files_to_drop { - blob_file.mark_as_deleted(); - } - - Ok(()) - } -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::{vlog::ValueHandle, UserKey, UserValue}; - - #[expect(clippy::unnecessary_wraps)] - fn entry( - blob_file_id: BlobFileId, - key: &[u8], - offset: u64, - ) -> crate::Result<(ScanEntry, BlobFileId)> { - Ok(( - ScanEntry { - key: UserKey::from(key), - offset, - seqno: 0, - uncompressed_len: 0, - value: UserValue::empty(), - }, - blob_file_id, - )) - } - - #[test] - fn drain_blobs_simple() -> crate::Result<()> { - let mut iter = [ - entry(0, b"a", 0), - entry(0, b"a", 1), - entry(0, b"a", 2), - entry(0, b"a", 3), - entry(0, b"a", 4), - ] - .into_iter() - .peekable(); - - drain_blobs( - &mut iter, - b"a", - &BlobIndirection { - size: 0, - vhandle: ValueHandle { - blob_file_id: 0, - offset: 4, - on_disk_size: 0, - }, - }, - )?; - - assert_eq!(entry(0, b"a", 4)?, iter.next().unwrap()?); - - Ok(()) - } - - #[test] - fn drain_blobs_multiple_keys() -> crate::Result<()> { - let mut iter = [ - entry(0, b"a", 0), - entry(0, b"b", 0), - entry(0, b"c", 0), - entry(0, b"d", 0), - entry(0, b"e", 0), - ] - .into_iter() - .peekable(); - - drain_blobs( - &mut iter, - b"e", - &BlobIndirection { - size: 0, - vhandle: ValueHandle { - blob_file_id: 0, - offset: 0, - on_disk_size: 0, - }, - }, - )?; - - assert_eq!(entry(0, b"e", 0)?, iter.next().unwrap()?); - Ok(()) } } diff --git a/crates/lsm-tree/src/compaction/leveled/mod.rs b/crates/lsm-tree/src/compaction/leveled/mod.rs index f4bd190ab..b8b2f7439 100644 --- a/crates/lsm-tree/src/compaction/leveled/mod.rs +++ b/crates/lsm-tree/src/compaction/leveled/mod.rs @@ -7,12 +7,12 @@ mod test; use super::{Choice, CompactionStrategy, Input as CompactionInput}; use crate::{ - compaction::state::{hidden_set::HiddenSet, CompactionState}, + HashSet, TableId, + compaction::state::{CompactionState, hidden_set::HiddenSet}, config::Config, slice_windows::{GrowingWindowsExt, ShrinkingWindowsExt}, - table::{util::aggregate_run_key_range, Table}, + table::{Table, util::aggregate_run_key_range}, version::{Run, Version}, - HashSet, TableId, }; /// Tries to find the most optimal compaction set from one level into the other. @@ -237,42 +237,6 @@ impl CompactionStrategy for Strategy { NAME } - fn get_config(&self) -> Vec { - vec![ - ( - crate::UserKey::from("leveled_l0_threshold"), - crate::UserValue::from(self.l0_threshold.to_le_bytes()), - ), - ( - crate::UserKey::from("leveled_target_size"), - crate::UserValue::from(self.target_size.to_le_bytes()), - ), - ( - crate::UserKey::from("leveled_level_ratio_policy"), - crate::UserValue::from({ - use byteorder::{LittleEndian, WriteBytesExt}; - - let mut v = vec![]; - - #[expect( - clippy::expect_used, - clippy::cast_possible_truncation, - reason = "writing into Vec should not fail; policies have length of 255 max" - )] - v.write_u8(self.level_ratio_policy.len() as u8) - .expect("cannot fail"); - - for &f in &self.level_ratio_policy { - #[expect(clippy::expect_used, reason = "writing into Vec should not fail")] - v.write_f32::(f).expect("cannot fail"); - } - - v - }), - ), - ] - } - #[expect(clippy::too_many_lines)] fn choose(&self, version: &Version, _: &Config, state: &CompactionState) -> Choice { assert!(version.level_count() == 7, "should have exactly 7 levels"); diff --git a/crates/lsm-tree/src/compaction/leveled/test.rs b/crates/lsm-tree/src/compaction/leveled/test.rs index 5486c86d8..2022d5447 100644 --- a/crates/lsm-tree/src/compaction/leveled/test.rs +++ b/crates/lsm-tree/src/compaction/leveled/test.rs @@ -116,8 +116,8 @@ fn leveled_l0_reached_limit_disjoint_l1() -> crate::Result<()> { tree.flush_active_memtable(0)?; } - let fifo = Arc::new(Strategy::default()); - tree.compact(fifo, 0)?; + let strategy = Arc::new(Strategy::default()); + tree.compact(strategy, 0)?; assert_eq!(1, tree.table_count()); diff --git a/crates/lsm-tree/src/compaction/maintenance.rs b/crates/lsm-tree/src/compaction/maintenance.rs deleted file mode 100644 index aa0974c7a..000000000 --- a/crates/lsm-tree/src/compaction/maintenance.rs +++ /dev/null @@ -1,179 +0,0 @@ -// 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::{Choice, CompactionStrategy}; -use crate::{config::Config, level_manifest::LevelManifest, segment::Segment, HashSet, SegmentId}; - -const L0_SEGMENT_CAP: usize = 20; - -/// Maintenance compactor -/// -/// This is a hidden compaction strategy that may be called by other strategies. -/// -/// It cleans up L0 if it grows too large. -#[derive(Default)] -pub struct Strategy; - -/// Choose a run of segments that has the least file size sum. -/// -/// This minimizes the compaction time (+ write amp) for a set of segments we -/// want to partially compact. -pub fn choose_least_effort_compaction(segments: &[Segment], n: usize) -> HashSet { - let num_segments = segments.len(); - - // Ensure that n is not greater than the number of segments - assert!( - n <= num_segments, - "N must be less than or equal to the number of segments" - ); - - let windows = segments.windows(n); - - let window = windows - .min_by_key(|window| window.iter().map(|s| s.file_size()).sum::()) - .expect("should have at least one window"); - - window.iter().map(Segment::id).collect() -} - -impl CompactionStrategy for Strategy { - fn get_name(&self) -> &'static str { - "MaintenanceStrategy" - } - - fn choose(&self, levels: &LevelManifest, _: &Config) -> Choice { - todo!() - } -} -/* -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - cache::Cache, - compaction::{Choice, CompactionStrategy}, - config::Config, - descriptor_table::FileDescriptorTable, - file::LEVELS_MANIFEST_FILE, - level_manifest::LevelManifest, - segment::{ - block::offset::BlockOffset, - block_index::{two_level_index::TwoLevelBlockIndex, BlockIndexImpl}, - file_offsets::FileOffsets, - meta::Metadata, - SegmentInner, - }, - super_segment::Segment, - KeyRange, - }; - use std::sync::{atomic::AtomicBool, Arc}; - use test_log::test; - - #[allow(clippy::expect_used)] - fn fixture_segment(id: SegmentId, created_at: u128) -> Segment { - todo!() - /* let cache = Arc::new(Cache::with_capacity_bytes(10 * 1_024 * 1_024)); - - let block_index = TwoLevelBlockIndex::new((0, id).into(), cache.clone()); - let block_index = Arc::new(BlockIndexImpl::TwoLevel(block_index)); - - SegmentInner { - tree_id: 0, - descriptor_table: Arc::new(FileDescriptorTable::new(512, 1)), - block_index, - - offsets: FileOffsets { - bloom_ptr: BlockOffset(0), - range_filter_ptr: BlockOffset(0), - index_block_ptr: BlockOffset(0), - metadata_ptr: BlockOffset(0), - range_tombstones_ptr: BlockOffset(0), - tli_ptr: BlockOffset(0), - pfx_ptr: BlockOffset(0), - }, - - metadata: Metadata { - data_block_count: 0, - index_block_count: 0, - data_block_size: 4_096, - index_block_size: 4_096, - created_at, - id, - file_size: 1, - compression: crate::segment::meta::CompressionType::None, - table_type: crate::segment::meta::TableType::Block, - item_count: 0, - key_count: 0, - key_range: KeyRange::new((vec![].into(), vec![].into())), - tombstone_count: 0, - range_tombstone_count: 0, - uncompressed_size: 0, - seqnos: (0, created_at as u64), - }, - cache, - - bloom_filter: Some(crate::bloom::BloomFilter::with_fp_rate(1, 0.1)), - - path: "a".into(), - is_deleted: AtomicBool::default(), - } - .into() */ - } - - #[test] - fn maintenance_empty_level() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - let compactor = Strategy; - - let levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - - assert_eq!( - compactor.choose(&levels, &Config::default()), - Choice::DoNothing - ); - - Ok(()) - } - - #[test] - fn maintenance_below_limit() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - let compactor = Strategy; - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - for id in 0..5 { - levels.add(fixture_segment(id, u128::from(id))); - } - - assert_eq!( - compactor.choose(&levels, &Config::default()), - Choice::DoNothing - ); - - Ok(()) - } - - #[test] - fn maintenance_l0_too_large() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - let compactor = Strategy; - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - for id in 0..(L0_SEGMENT_CAP + 2) { - levels.add(fixture_segment(id as u64, id as u128)); - } - - assert_eq!( - compactor.choose(&levels, &Config::default()), - Choice::Merge(crate::compaction::Input { - dest_level: 0, - segment_ids: [0, 1, 2].into_iter().collect::>(), - target_size: u64::MAX - }) - ); - - Ok(()) - } -} - */ diff --git a/crates/lsm-tree/src/compaction/major.rs b/crates/lsm-tree/src/compaction/major.rs index 0e973cc87..8a5241fce 100644 --- a/crates/lsm-tree/src/compaction/major.rs +++ b/crates/lsm-tree/src/compaction/major.rs @@ -4,7 +4,7 @@ use super::{Choice, CompactionStrategy, Input as CompactionInput}; use crate::{ - compaction::state::CompactionState, config::Config, table::Table, version::Version, HashSet, + HashSet, compaction::state::CompactionState, config::Config, table::Table, version::Version, }; /// Compacts all tables into the last level diff --git a/crates/lsm-tree/src/compaction/mod.rs b/crates/lsm-tree/src/compaction/mod.rs index 8aeea631c..5a3385628 100644 --- a/crates/lsm-tree/src/compaction/mod.rs +++ b/crates/lsm-tree/src/compaction/mod.rs @@ -4,38 +4,26 @@ //! Contains compaction strategies -pub(crate) mod fifo; -pub(crate) mod leveled; -// pub(crate) mod maintenance; -pub(crate) mod drop_range; pub mod filter; mod flavour; +pub(crate) mod leveled; pub(crate) mod major; -pub(crate) mod movedown; pub(crate) mod pulldown; pub(crate) mod state; pub(crate) mod stream; -// pub(crate) mod tiered; pub(crate) mod worker; -pub use fifo::Strategy as Fifo; pub use filter::{CompactionFilter, Factory, ItemAccessor, Verdict}; pub use leveled::Strategy as Leveled; -// pub use tiered::Strategy as SizeTiered; - -pub use {fifo::NAME as FIFO_COMPACTION_NAME, leveled::NAME as LEVELED_COMPACTION_NAME}; /// Alias for `Leveled` pub type Levelled = Leveled; -#[doc(hidden)] -pub use movedown::Strategy as MoveDown; - #[doc(hidden)] pub use pulldown::Strategy as PullDown; use crate::{ - compaction::state::CompactionState, config::Config, version::Version, HashSet, KvPair, TableId, + HashSet, TableId, compaction::state::CompactionState, config::Config, version::Version, }; /// Input for compactor @@ -71,12 +59,6 @@ pub enum Choice { /// Compacts some tables into a new level. Merge(Input), - - /// Delete tables without doing compaction. - /// - /// This may be used by a compaction strategy that wants to delete old data - /// without having to compact it away, like [`fifo::Strategy`]. - Drop(HashSet), } /// Trait for a compaction strategy @@ -88,11 +70,6 @@ pub trait CompactionStrategy { /// Gets the compaction strategy name. fn get_name(&self) -> &'static str; - #[doc(hidden)] - fn get_config(&self) -> Vec { - vec![] - } - /// Decides on what to do based on the current state of the LSM-tree's levels fn choose(&self, version: &Version, config: &Config, state: &CompactionState) -> Choice; } diff --git a/crates/lsm-tree/src/compaction/movedown.rs b/crates/lsm-tree/src/compaction/movedown.rs deleted file mode 100644 index 2b9902a88..000000000 --- a/crates/lsm-tree/src/compaction/movedown.rs +++ /dev/null @@ -1,38 +0,0 @@ -// 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::{Choice, CompactionStrategy, Input}; -use crate::{compaction::state::CompactionState, table::Table, version::Version, Config}; - -/// Moves down a level into the destination level. -pub struct Strategy(pub u8, pub u8); - -impl CompactionStrategy for Strategy { - fn get_name(&self) -> &'static str { - "MoveDownCompaction" - } - - fn choose(&self, version: &Version, _: &Config, state: &CompactionState) -> Choice { - if version.level_is_busy(usize::from(self.0), state.hidden_set()) { - return Choice::DoNothing; - } - - let Some(level) = version.level(self.0.into()) else { - return Choice::DoNothing; - }; - - let table_ids = level - .iter() - .flat_map(|run| run.iter()) - .map(Table::id) - .collect(); - - Choice::Move(Input { - table_ids, - dest_level: self.1, - canonical_level: self.1, - target_size: u64::MAX, - }) - } -} diff --git a/crates/lsm-tree/src/compaction/pulldown.rs b/crates/lsm-tree/src/compaction/pulldown.rs index 09a558fd5..135f642ae 100644 --- a/crates/lsm-tree/src/compaction/pulldown.rs +++ b/crates/lsm-tree/src/compaction/pulldown.rs @@ -4,9 +4,9 @@ use super::{Choice, CompactionStrategy}; use crate::{ - compaction::{state::CompactionState, Input}, - version::Version, Config, + compaction::{Input, state::CompactionState}, + version::Version, }; /// Pulls down and merges a level into the destination level. diff --git a/crates/lsm-tree/src/compaction/state/mod.rs b/crates/lsm-tree/src/compaction/state/mod.rs index 3a4d10fce..332eb42f3 100644 --- a/crates/lsm-tree/src/compaction/state/mod.rs +++ b/crates/lsm-tree/src/compaction/state/mod.rs @@ -61,10 +61,6 @@ mod tests { let table_count_before_major_compact = tree.table_count(); - let crate::AnyTree::Standard(tree) = tree else { - unreachable!(); - }; - // { // // NOTE: Purposefully change level manifest to have invalid path // // to force an I/O error @@ -76,12 +72,13 @@ mod tests { assert!(tree.major_compact(u64::MAX, 4).is_err()); - assert!(tree - .compaction_state - .lock() - .expect("lock is poisoned") - .hidden_set() - .is_empty()); + assert!( + tree.compaction_state + .lock() + .expect("lock is poisoned") + .hidden_set() + .is_empty() + ); assert_eq!(table_count_before_major_compact, tree.table_count()); diff --git a/crates/lsm-tree/src/compaction/stream.rs b/crates/lsm-tree/src/compaction/stream.rs index 45a63e4c9..6857cf35d 100644 --- a/crates/lsm-tree/src/compaction/stream.rs +++ b/crates/lsm-tree/src/compaction/stream.rs @@ -7,14 +7,6 @@ use std::iter::Peekable; type Item = crate::Result; -/// A callback that receives all dropped KVs -/// -/// Used for counting blobs that are not referenced anymore because of -/// vHandles that are being dropped through compaction. -pub trait DroppedKvCallback { - fn on_dropped(&mut self, kv: &InternalValue); -} - /// Verdict returned by [`StreamFilter`] #[derive(Debug)] pub enum StreamFilterVerdict { @@ -46,16 +38,13 @@ impl StreamFilter for NoFilter { /// Consumes a stream of KVs and emits a new stream according to GC and tombstone rules /// /// This iterator is used during flushing & compaction. -pub struct CompactionStream<'a, I: Iterator, F: StreamFilter = NoFilter> { +pub struct CompactionStream, F: StreamFilter = NoFilter> { /// KV stream inner: Peekable, /// MVCC watermark to get rid of old versions gc_seqno_threshold: SeqNo, - /// Event emitter that receives all dropped KVs - dropped_callback: Option<&'a mut dyn DroppedKvCallback>, - /// Stream filter filter: F, @@ -64,7 +53,7 @@ pub struct CompactionStream<'a, I: Iterator, F: StreamFilter = NoFi zero_seqnos: bool, } -impl> CompactionStream<'_, I, NoFilter> { +impl> CompactionStream { /// Initializes a new merge iterator #[must_use] pub fn new(iter: I, gc_seqno_threshold: SeqNo) -> Self { @@ -73,7 +62,6 @@ impl> CompactionStream<'_, I, NoFilter> { Self { inner: iter, gc_seqno_threshold, - dropped_callback: None, filter: NoFilter, evict_tombstones: false, zero_seqnos: false, @@ -81,13 +69,12 @@ impl> CompactionStream<'_, I, NoFilter> { } } -impl<'a, I: Iterator, F: StreamFilter + 'a> CompactionStream<'a, I, F> { +impl, F: StreamFilter> CompactionStream { /// Installs a filter into this stream. - pub fn with_filter(self, filter: NF) -> CompactionStream<'a, I, NF> { + pub fn with_filter(self, filter: NF) -> CompactionStream { CompactionStream { inner: self.inner, gc_seqno_threshold: self.gc_seqno_threshold, - dropped_callback: self.dropped_callback, filter, evict_tombstones: self.evict_tombstones, zero_seqnos: self.zero_seqnos, @@ -99,12 +86,6 @@ impl<'a, I: Iterator, F: StreamFilter + 'a> CompactionStream<'a, I, self } - /// Installs a callback that receives all dropped KVs. - pub fn with_drop_callback(mut self, cb: &'a mut dyn DroppedKvCallback) -> Self { - self.dropped_callback = Some(cb); - self - } - /// Sets sequence numbers to zero if they are below the snapshot watermark. /// /// This can save a lot of space, because "0" only takes 1 byte, and sequence numbers are monotonically increasing. @@ -118,15 +99,7 @@ impl<'a, I: Iterator, F: StreamFilter + 'a> CompactionStream<'a, I, loop { let Some(next) = self.inner.next_if(|kv| { if let Ok(kv) = kv { - let expired = kv.key.user_key == key; - - if expired { - if let Some(watcher) = &mut self.dropped_callback { - watcher.on_dropped(kv); - } - } - - expired + kv.key.user_key == key } else { true } @@ -139,7 +112,7 @@ impl<'a, I: Iterator, F: StreamFilter + 'a> CompactionStream<'a, I, } } -impl<'a, I: Iterator, F: StreamFilter + 'a> Iterator for CompactionStream<'a, I, F> { +impl, F: StreamFilter> Iterator for CompactionStream { type Item = Item; fn next(&mut self) -> Option { @@ -150,18 +123,10 @@ impl<'a, I: Iterator, F: StreamFilter + 'a> Iterator for Compaction match fail_iter!(self.filter.filter_item(&head)) { StreamFilterVerdict::Keep => { /* Do nothing */ } StreamFilterVerdict::Replace((new_type, new_value)) => { - // If we are replacing this item's value, call the dropped callback for the previous item - if let Some(watcher) = &mut self.dropped_callback { - watcher.on_dropped(&head); - } head.value = new_value; head.key.value_type = new_type; } StreamFilterVerdict::Drop => { - if let Some(watcher) = &mut self.dropped_callback { - watcher.on_dropped(&head); - } - // Ignore continue; } @@ -223,7 +188,7 @@ impl<'a, I: Iterator, F: StreamFilter + 'a> Iterator for Compaction #[cfg(test)] mod tests { use super::*; - use crate::{value::InternalValue, ValueType}; + use crate::{ValueType, value::InternalValue}; use test_log::test; macro_rules! stream { @@ -259,49 +224,6 @@ mod tests { }; } - #[derive(Default)] - struct TrackCallback { - items: Vec, - } - - impl DroppedKvCallback for TrackCallback { - fn on_dropped(&mut self, kv: &InternalValue) { - self.items.push(kv.clone()); - } - } - - #[test] - #[expect(clippy::unwrap_used)] - fn compaction_stream_expired_callback_1() -> crate::Result<()> { - #[rustfmt::skip] - let vec = stream![ - "a", "", "T", - "a", "", "T", - "a", "", "T", - ]; - - let mut my_watcher = TrackCallback::default(); - - let iter = vec.iter().cloned().map(Ok); - let mut iter = CompactionStream::new(iter, 1_000).with_drop_callback(&mut my_watcher); - - assert_eq!( - InternalValue::from_components(*b"a", *b"", 999, ValueType::Tombstone), - iter.next().unwrap()?, - ); - iter_closed!(iter); - - assert_eq!( - [ - InternalValue::from_components("a", "", 998, ValueType::Tombstone), - InternalValue::from_components("a", "", 997, ValueType::Tombstone), - ], - &*my_watcher.items, - ); - - Ok(()) - } - #[test] #[expect(clippy::unwrap_used)] fn compaction_stream_seqno_zeroing_1() -> crate::Result<()> { @@ -648,11 +570,8 @@ mod tests { "b", "b", "V", ]; - let mut drop_cb = TrackCallback { items: vec![] }; let iter = vec.iter().cloned().map(Ok); - let iter = CompactionStream::new(iter, 995) - .with_filter(Filter(b"7")) - .with_drop_callback(&mut drop_cb); + let iter = CompactionStream::new(iter, 995).with_filter(Filter(b"7")); let out: Vec<_> = iter.map(Result::unwrap).collect(); @@ -664,21 +583,11 @@ mod tests { "a", "", "T", "a", "", "T", ]); - - let fc = InternalValue::from_components; - - #[rustfmt::skip] - assert_eq!(drop_cb.items, [ - fc(b"a", b"6", 996, ValueType::Value), - fc(b"a", b"5", 995, ValueType::Value), - fc(b"a", b"4", 994, ValueType::Value), - fc(b"b", b"b", 999, ValueType::Value), - ]); } pub mod custom_mvcc { use super::*; - use byteorder::{ReadBytesExt, WriteBytesExt, BE}; + use byteorder::{BE, ReadBytesExt, WriteBytesExt}; use test_log::test; /// MVCC trailer size (anything but user key) @@ -774,14 +683,11 @@ mod tests { kv(b"abc", 2, b"a", false), ]; - let mut drop_cb = TrackCallback { items: vec![] }; let iter = vec.iter().cloned().map(Ok); - let iter = CompactionStream::new(iter, 995) - .with_filter(Filter { - mvcc_watermark: 5, - prev_user_key: None, - }) - .with_drop_callback(&mut drop_cb); + let iter = CompactionStream::new(iter, 995).with_filter(Filter { + mvcc_watermark: 5, + prev_user_key: None, + }); let out: Vec<_> = iter.map(Result::unwrap).collect(); @@ -807,14 +713,11 @@ mod tests { kv(b"d", 0, b"c", false), ]; - let mut drop_cb = TrackCallback { items: vec![] }; let iter = vec.iter().cloned().map(Ok); - let iter = CompactionStream::new(iter, 995) - .with_filter(Filter { - mvcc_watermark: 3, - prev_user_key: None, - }) - .with_drop_callback(&mut drop_cb); + let iter = CompactionStream::new(iter, 995).with_filter(Filter { + mvcc_watermark: 3, + prev_user_key: None, + }); let out: Vec<_> = iter.map(Result::unwrap).collect(); diff --git a/crates/lsm-tree/src/compaction/tiered.rs b/crates/lsm-tree/src/compaction/tiered.rs deleted file mode 100644 index 7ee618766..000000000 --- a/crates/lsm-tree/src/compaction/tiered.rs +++ /dev/null @@ -1,343 +0,0 @@ -// 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::{Choice, CompactionStrategy}; -use crate::{compaction::state::CompactionState, version::Version, Config}; - -// fn desired_level_size_in_bytes(level_idx: u8, ratio: u8, base_size: u32) -> usize { -// (ratio as usize).pow(u32::from(level_idx + 1)) * (base_size as usize) -// } - -/// Size-tiered compaction strategy (STCS) -/// -/// If a level reaches a threshold, it is merged into a larger table to the next level. -/// -/// STCS suffers from high read and temporary doubled space amplification, but has good write amplification. -#[derive(Clone)] -pub struct Strategy { - /// Base size - pub base_size: u32, - - /// Size ratio between levels of the LSM tree (a.k.a fanout, growth rate). - /// - /// This is the exponential growth of the from one - /// level to the next - /// - /// A level target size is: base_size * level_ratio.pow(#level + 1) - #[expect(clippy::doc_markdown)] - pub level_ratio: u8, -} - -impl Strategy { - /// Creates a new STCS strategy with custom base size - #[must_use] - pub fn new(base_size: u32, level_ratio: u8) -> Self { - Self { - base_size, - level_ratio, - } - } -} - -impl Default for Strategy { - fn default() -> Self { - Self { - base_size: 64 * 1_024 * 1_024, - level_ratio: 4, - } - } -} - -impl CompactionStrategy for Strategy { - fn get_name(&self) -> &'static str { - "TieredStrategy" - } - - fn choose(&self, version: &Version, _config: &Config, state: &CompactionState) -> Choice { - unimplemented!() - } -} -/* -#[cfg(test)] -mod tests { - use super::Strategy; - use crate::{ - cache::Cache, - compaction::{Choice, CompactionStrategy, Input as CompactionInput}, - config::Config, - descriptor_table::FileDescriptorTable, - file::LEVELS_MANIFEST_FILE, - level_manifest::LevelManifest, - segment::{ - block::offset::BlockOffset, - block_index::{two_level_index::TwoLevelBlockIndex, BlockIndexImpl}, - file_offsets::FileOffsets, - meta::{Metadata, SegmentId}, - SegmentInner, - }, - super_segment::Segment, - HashSet, KeyRange, SeqNo, - }; - use std::sync::{atomic::AtomicBool, Arc}; - use test_log::test; - - #[allow(clippy::expect_used)] - fn fixture_segment(id: SegmentId, size_mib: u64, max_seqno: SeqNo) -> Segment { - todo!() - - /* let cache = Arc::new(Cache::with_capacity_bytes(10 * 1_024 * 1_024)); - - let block_index = TwoLevelBlockIndex::new((0, id).into(), cache.clone()); - let block_index = Arc::new(BlockIndexImpl::TwoLevel(block_index)); - - SegmentInner { - tree_id: 0, - descriptor_table: Arc::new(FileDescriptorTable::new(512, 1)), - block_index, - - offsets: FileOffsets { - bloom_ptr: BlockOffset(0), - range_filter_ptr: BlockOffset(0), - index_block_ptr: BlockOffset(0), - metadata_ptr: BlockOffset(0), - range_tombstones_ptr: BlockOffset(0), - tli_ptr: BlockOffset(0), - pfx_ptr: BlockOffset(0), - }, - - metadata: Metadata { - data_block_count: 0, - index_block_count: 0, - data_block_size: 4_096, - index_block_size: 4_096, - created_at: 0, - id, - file_size: size_mib * 1_024 * 1_024, - compression: crate::segment::meta::CompressionType::None, - table_type: crate::segment::meta::TableType::Block, - item_count: 0, - key_count: 0, - key_range: KeyRange::new((vec![].into(), vec![].into())), - tombstone_count: 0, - range_tombstone_count: 0, - uncompressed_size: size_mib * 1_024 * 1_024, - seqnos: (0, max_seqno), - }, - cache, - - bloom_filter: Some(BloomFilter::with_fp_rate(1, 0.1)), - - path: "a".into(), - is_deleted: AtomicBool::default(), - } - .into() */ - } - - #[test] - fn tiered_empty_levels() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 8, - }; - - let levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - - assert_eq!( - compactor.choose(&levels, &Config::default()), - Choice::DoNothing - ); - - Ok(()) - } - - #[test] - fn tiered_default_l0() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 4, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - - levels.add(fixture_segment(1, 8, 5)); - assert_eq!(compactor.choose(&levels, &config), Choice::DoNothing); - - levels.add(fixture_segment(2, 8, 6)); - assert_eq!(compactor.choose(&levels, &config), Choice::DoNothing); - - levels.add(fixture_segment(3, 8, 7)); - assert_eq!(compactor.choose(&levels, &config), Choice::DoNothing); - - levels.add(fixture_segment(4, 8, 8)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 1, - segment_ids: set![1, 2, 3, 4], - target_size: u64::MAX, - }) - ); - - Ok(()) - } - - #[test] - fn tiered_ordering() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 2, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - - levels.add(fixture_segment(1, 8, 0)); - levels.add(fixture_segment(2, 8, 1)); - levels.add(fixture_segment(3, 8, 2)); - levels.add(fixture_segment(4, 8, 3)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 1, - segment_ids: set![1, 2], - target_size: u64::MAX, - }) - ); - - Ok(()) - } - - #[test] - fn tiered_more_than_min() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 4, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - levels.add(fixture_segment(1, 8, 5)); - levels.add(fixture_segment(2, 8, 6)); - levels.add(fixture_segment(3, 8, 7)); - levels.add(fixture_segment(4, 8, 8)); - - levels.insert_into_level(1, fixture_segment(5, 8 * 4, 9)); - levels.insert_into_level(1, fixture_segment(6, 8 * 4, 10)); - levels.insert_into_level(1, fixture_segment(7, 8 * 4, 11)); - levels.insert_into_level(1, fixture_segment(8, 8 * 4, 12)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 2, - segment_ids: set![5, 6, 7, 8], - target_size: u64::MAX, - }) - ); - - Ok(()) - } - - #[test] - fn tiered_many_segments() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 2, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - levels.add(fixture_segment(1, 8, 5)); - levels.add(fixture_segment(2, 8, 6)); - levels.add(fixture_segment(3, 8, 7)); - levels.add(fixture_segment(4, 8, 8)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 1, - segment_ids: set![1, 2], - target_size: u64::MAX, - }) - ); - - Ok(()) - } - - #[test] - fn tiered_deeper_level() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 2, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - levels.add(fixture_segment(1, 8, 5)); - - levels.insert_into_level(1, fixture_segment(2, 8 * 2, 6)); - levels.insert_into_level(1, fixture_segment(3, 8 * 2, 7)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 2, - segment_ids: set![2, 3], - target_size: u64::MAX, - }) - ); - - let tempdir = tempfile::tempdir()?; - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - - levels.insert_into_level(2, fixture_segment(2, 8 * 4, 5)); - levels.insert_into_level(2, fixture_segment(3, 8 * 4, 6)); - - assert_eq!( - compactor.choose(&levels, &config), - Choice::Merge(CompactionInput { - dest_level: 3, - segment_ids: set![2, 3], - target_size: u64::MAX, - }) - ); - - Ok(()) - } - - #[test] - fn tiered_last_level() -> crate::Result<()> { - let tempdir = tempfile::tempdir()?; - - let compactor = Strategy { - base_size: 8 * 1_024 * 1_024, - level_ratio: 2, - }; - let config = Config::default(); - - let mut levels = LevelManifest::create_new(4, tempdir.path().join(LEVELS_MANIFEST_FILE))?; - levels.insert_into_level(3, fixture_segment(2, 8, 5)); - levels.insert_into_level(3, fixture_segment(3, 8, 5)); - - assert_eq!(compactor.choose(&levels, &config), Choice::DoNothing); - - Ok(()) - } -} - */ diff --git a/crates/lsm-tree/src/compaction/worker.rs b/crates/lsm-tree/src/compaction/worker.rs index da8d88f14..010d092fd 100644 --- a/crates/lsm-tree/src/compaction/worker.rs +++ b/crates/lsm-tree/src/compaction/worker.rs @@ -4,31 +4,25 @@ use super::{CompactionStrategy, Input as CompactionPayload}; use crate::{ - blob_tree::FragmentationMap, + Config, InternalValue, SeqNo, SequenceNumberCounter, Table, TableId, compaction::{ + Choice, filter::{Context, StreamFilterAdapter}, - flavour::{RelocatingCompaction, StandardCompaction}, + flavour::StandardCompaction, state::CompactionState, stream::CompactionStream, - Choice, }, - file::BLOBS_FOLDER, merge::Merger, run_scanner::RunScanner, stop_signal::StopSignal, tree::inner::TreeId, version::{Run, SuperVersions, Version}, - vlog::{BlobFileMergeScanner, BlobFileScanner, BlobFileWriter}, - BlobFile, Config, HashSet, InternalValue, SeqNo, SequenceNumberCounter, Table, TableId, }; use std::{ sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard}, time::Instant, }; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - pub type CompactionReader<'a> = Box> + 'a>; /// Compaction options @@ -41,8 +35,6 @@ pub struct Options { pub table_id_generator: SequenceNumberCounter, - pub blob_file_id_generator: SequenceNumberCounter, - /// Configuration of tree. pub config: Arc, @@ -59,9 +51,6 @@ pub struct Options { pub mvcc_gc_watermark: u64, pub compaction_state: Arc>, - - #[cfg(feature = "metrics")] - pub metrics: Arc, } impl Options { @@ -71,7 +60,6 @@ impl Options { visible_seqno: tree.config.visible_seqno.clone(), tree_id: tree.id, table_id_generator: tree.table_id_counter.clone(), - blob_file_id_generator: tree.blob_file_id_counter.clone(), config: tree.config.clone(), version_history: tree.version_history.clone(), stop_signal: tree.stop_signal.clone(), @@ -79,9 +67,6 @@ impl Options { mvcc_gc_watermark: 0, compaction_state: tree.compaction_state.clone(), - - #[cfg(feature = "metrics")] - metrics: tree.metrics.clone(), } } } @@ -118,15 +103,6 @@ pub fn do_compaction(opts: &Options) -> crate::Result<()> { move_tables(&compaction_state, opts, &payload) } - Choice::Drop(payload) => { - drop(version_history_lock); - - drop_tables( - compaction_state, - opts, - &payload.into_iter().collect::>(), - ) - } Choice::DoNothing => { log::trace!("Compactor chose to do nothing"); Ok(()) @@ -150,7 +126,7 @@ fn create_compaction_stream<'a>( version: &Version, to_compact: &[TableId], eviction_seqno: SeqNo, -) -> crate::Result>>>> { +) -> crate::Result>>>> { let mut readers: Vec> = vec![]; let mut found = 0; @@ -195,9 +171,9 @@ fn move_tables( .should_decline_compaction(payload.table_ids.iter().copied()) { log::warn!( - "Compaction task created by {:?} contained hidden tables, declining to run it - please report this at https://github.com/fjall-rs/lsm-tree/issues/new?template=bug_report.md", - opts.strategy.get_name(), - ); + "Compaction task created by {:?} contained hidden tables, declining to run it - please report this at https://github.com/fjall-rs/lsm-tree/issues/new?template=bug_report.md", + opts.strategy.get_name(), + ); return Ok(()); } @@ -226,87 +202,6 @@ fn move_tables( Ok(()) } -/// Picks blob files to rewrite (defragment) -fn pick_blob_files_to_rewrite( - picked_tables: &HashSet, - current_version: &Version, - blob_opts: &crate::KvSeparationOptions, -) -> crate::Result> { - use crate::Table; - - // We start off by getting all the blob files that are referenced by the tables - // that we want to compact. - let linked_blob_files = picked_tables - .iter() - .map(|&id| { - current_version.get_table(id).unwrap_or_else(|| { - panic!("Table {id} should exist"); - }) - }) - .map(Table::list_blob_file_references) - .collect::, _>>()?; - - // Then we filter all blob files that are not fragmented or old enough. - let mut linked_blob_files = linked_blob_files - .into_iter() - .flatten() - .flatten() - .map(|blob_file_ref| { - current_version - .blob_files - .get(blob_file_ref.blob_file_id) - .unwrap_or_else(|| { - panic!("Blob file {} should exist", blob_file_ref.blob_file_id); - }) - }) - .filter(|blob_file| { - blob_file.is_stale(current_version.gc_stats(), blob_opts.staleness_threshold) - }) - .filter(|blob_file| { - // NOTE: Dead blob files are dropped anyway during current_version change commit - !blob_file.is_dead(current_version.gc_stats()) - }) - .collect::>() - .into_iter() - .collect::>(); - - linked_blob_files.sort_by_key(|a| a.id()); - - #[expect( - clippy::cast_precision_loss, - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - reason = "precision loss and truncation are acceptable for cutoff calculation" - )] - let cutoff_point = { - let len = linked_blob_files.len() as f32; - (len * blob_opts.age_cutoff) as usize - }; - linked_blob_files.drain(cutoff_point..); - - // IMPORTANT: Additionally, we also have to check if any other tables reference any of our candidate blob files. - // We have to *not* include blob files that are referenced by other tables, because otherwise those - // blob references would point into nothing (becoming dangling). - for table in current_version.iter_tables() { - if picked_tables.contains(&table.id()) { - continue; - } - - let other_refs = table - .list_blob_file_references()? - .unwrap_or_default() - .into_iter() - .filter(|x| linked_blob_files.iter().any(|bf| bf.id() == x.blob_file_id)) - .collect::>(); - - for additional_ref in other_refs { - linked_blob_files.retain(|x| x.id() != additional_ref.blob_file_id); - } - } - - Ok(linked_blob_files.into_iter().cloned().collect::>()) -} - fn hidden_guard( payload: &CompactionPayload, opts: &Options, @@ -365,8 +260,6 @@ fn merge_tables( return Ok(()); }; - let mut blob_frag_map = FragmentationMap::default(); - let Some(mut merge_iter) = create_compaction_stream( ¤t_super_version.version, &payload.table_ids.iter().copied().collect::>(), @@ -390,8 +283,6 @@ fn merge_tables( .evict_tombstones(is_last_level) .zero_seqnos(false); - let blobs_folder = opts.config.path.join(BLOBS_FOLDER); - let filter_ctx = Context { is_last_level }; // Construct the compaction filter @@ -400,78 +291,15 @@ fn merge_tables( f.make_filter(&filter_ctx) }); - // This is used by the compaction filter if it wants to write new blobs - // TODO: the filter should really pipe new blobs into the compaction stream directly, - // TODO: but that will probably require to change the protocol between filter <-> compaction stream a bit - let mut filter_blob_writer = None; - let mut merge_iter = merge_iter.with_filter(StreamFilterAdapter::new( + let merge_iter = merge_iter.with_filter(StreamFilterAdapter::new( compaction_filter.as_deref_mut(), - opts, - ¤t_super_version.version, - &blobs_folder, - &mut filter_blob_writer, &filter_ctx, )); let table_writer = super::flavour::prepare_table_writer(¤t_super_version.version, opts, payload)?; - let start = Instant::now(); - - let mut compactor = match &opts.config.kv_separation_opts { - Some(blob_opts) => { - merge_iter = merge_iter.with_drop_callback(&mut blob_frag_map); - - let blob_files_to_rewrite = pick_blob_files_to_rewrite( - &payload.table_ids, - ¤t_super_version.version, - blob_opts, - )?; - - if blob_files_to_rewrite.is_empty() { - log::debug!("No blob relocation needed"); - - Box::new(StandardCompaction::new(table_writer, tables)) - as Box - } else { - log::debug!( - "Relocate blob files: {:?}", - blob_files_to_rewrite - .iter() - .map(BlobFile::id) - .collect::>(), - ); - - let scanner = BlobFileMergeScanner::new( - blob_files_to_rewrite - .iter() - .map(|bf| BlobFileScanner::new(&bf.0.path, bf.id())) - .collect::>>()?, - ); - - let writer = BlobFileWriter::new( - opts.blob_file_id_generator.clone(), - &blobs_folder, - opts.tree_id, - opts.config.descriptor_table.clone(), - )? - .use_target_size(blob_opts.file_target_size) - .use_passthrough_compression(blob_opts.compression); - - let inner = StandardCompaction::new(table_writer, tables); - - Box::new(RelocatingCompaction::new( - inner, - scanner.peekable(), - writer, - blob_files_to_rewrite, - )) - } - } - None => Box::new(StandardCompaction::new(table_writer, tables)), - }; - - log::trace!("Blob file GC preparation done in {:?}", start.elapsed()); + let mut compactor = StandardCompaction::new(table_writer, tables); drop(version_history_lock); @@ -511,31 +339,8 @@ fn merge_tables( let mut version_history_lock = opts.version_history.write().expect("lock is poisoned"); log::trace!("Acquired super version write lock"); - log::trace!("Blob fragmentation diff: {blob_frag_map:#?}"); - - let extra_blob_files = filter_blob_writer - .map(BlobFileWriter::finish) - .transpose() - .inspect_err(|e| { - // NOTE: We cannot use hidden_guard here because we already locked the compaction state - - log::error!("Compaction failed while finishing filter blob writer: {e:?}"); - - compaction_state - .hidden_set_mut() - .show(payload.table_ids.iter().copied()); - })? - .unwrap_or_default(); - compactor - .finish( - &mut version_history_lock, - opts, - payload, - dst_lvl, - blob_frag_map, - extra_blob_files, - ) + .finish(&mut version_history_lock, opts, payload, dst_lvl) .inspect_err(|e| { // NOTE: We cannot use hidden_guard here because we already locked the compaction state @@ -564,100 +369,10 @@ fn merge_tables( Ok(()) } -fn drop_tables( - compaction_state: MutexGuard<'_, CompactionState>, - opts: &Options, - ids_to_drop: &[TableId], -) -> crate::Result<()> { - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - let mut version_history_lock = opts.version_history.write().expect("lock is poisoned"); - - // Fail-safe for buggy compaction strategies - if compaction_state - .hidden_set() - .should_decline_compaction(ids_to_drop.iter().copied()) - { - log::warn!( - "Compaction task created by {:?} contained hidden tables, declining to run it - please report this at https://github.com/fjall-rs/lsm-tree/issues/new?template=bug_report.md", - opts.strategy.get_name(), - ); - return Ok(()); - } - - let Some(tables) = ids_to_drop - .iter() - .map(|&id| { - version_history_lock - .latest_version() - .version - .get_table(id) - .cloned() - }) - .collect::>>() - else { - log::warn!( - "Compaction task created by {:?} contained tables not referenced in the level manifest", - opts.strategy.get_name(), - ); - return Ok(()); - }; - - log::debug!("Dropping tables: {ids_to_drop:?}"); - - let mut dropped_blob_files = vec![]; - - // IMPORTANT: Write the manifest with the removed tables first - // Otherwise the table files are deleted, but are still referenced! - version_history_lock.upgrade_version( - &opts.config.path, - |current| { - let mut copy = current.clone(); - - copy.version = copy - .version - .with_dropped(ids_to_drop, &mut dropped_blob_files)?; - - Ok(copy) - }, - &opts.global_seqno, - &opts.visible_seqno, - )?; - - if let Err(e) = version_history_lock.maintenance(&opts.config.path, opts.mvcc_gc_watermark) { - log::error!("Manifest maintenance failed: {e:?}"); - return Err(e); - } - - drop(version_history_lock); - - // NOTE: If the application were to crash >here< it's fine - // The tables are not referenced anymore, and will be - // cleaned up upon recovery - for table in tables { - table.mark_as_deleted(); - } - - for blob_file in dropped_blob_files { - blob_file.mark_as_deleted(); - } - - drop(compaction_state); - - log::trace!("Dropped {} tables", ids_to_drop.len()); - - Ok(()) -} - #[cfg(test)] mod tests { use super::{create_compaction_stream, pick_run_indexes}; - use crate::{ - compaction::{state::CompactionState, Choice, CompactionStrategy, Input}, - config::BlockSizePolicy, - version::Version, - AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter, TableId, - }; - use std::sync::Arc; + use crate::{AbstractTree, SequenceNumberCounter}; use test_log::test; #[test] @@ -826,135 +541,4 @@ mod tests { Ok(()) } - - #[test] - fn compaction_drop_tables() -> crate::Result<()> { - let folder = tempfile::tempdir()?; - - let tree = crate::Config::new( - folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - tree.insert("a", "a", 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.approximate_len()); - assert_eq!(0, tree.sealed_memtable_count()); - - tree.insert("b", "a", 1); - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.approximate_len()); - assert_eq!(0, tree.sealed_memtable_count()); - - tree.insert("c", "a", 2); - tree.flush_active_memtable(0)?; - assert_eq!(3, tree.approximate_len()); - assert_eq!(0, tree.sealed_memtable_count()); - - tree.compact(Arc::new(crate::compaction::Fifo::new(1, None)), 3)?; - - assert_eq!(0, tree.table_count()); - - Ok(()) - } - - #[test] - fn blob_file_picking_simple() -> crate::Result<()> { - struct InPlaceStrategy(Vec); - - impl CompactionStrategy for InPlaceStrategy { - fn get_name(&self) -> &'static str { - "InPlaceCompaction" - } - - fn choose(&self, _: &Version, _: &Config, _: &CompactionState) -> Choice { - Choice::Merge(Input { - table_ids: self.0.iter().copied().collect(), - dest_level: 6, - target_size: 64_000_000, - canonical_level: 6, // We don't really care - this compaction is only used for very specific unit tests - }) - } - } - - let folder = tempfile::tempdir()?; - - let tree = crate::Config::new( - folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .data_block_size_policy(BlockSizePolicy::all(1)) - .with_kv_separation(Some( - KvSeparationOptions::default() - .separation_threshold(1) - .age_cutoff(1.0) - .staleness_threshold(0.01) - .compression(crate::CompressionType::None), - )) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - tree.flush_active_memtable(1_000)?; - assert_eq!(0, tree.sealed_memtable_count()); - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(1, 1_000)?; - assert_eq!(3, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - // We now have tables [1, 2, 3] pointing into blob file 0 - - tree.drop_range("a"..="a")?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - assert_eq!( - &{ - let mut map = crate::HashMap::default(); - map.insert(0, crate::blob_tree::FragmentationEntry::new(1, 1, 1)); - map - }, - &**tree.current_version().gc_stats(), - ); - } - - // Even though we are compacting table #2, blob file is not rewritten - // because table #3 still points into it - tree.compact(Arc::new(InPlaceStrategy(vec![2])), 1_000)?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - assert_eq!( - &{ - let mut map = crate::HashMap::default(); - map.insert(0, crate::blob_tree::FragmentationEntry::new(1, 1, 1)); - map - }, - &**tree.current_version().gc_stats(), - ); - } - - // Because tables #3 & #4 both point into the blob file - // Only selecting both for compaction will actually rewrite the file - tree.compact(Arc::new(InPlaceStrategy(vec![3, 4])), 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - // Fragmentation is cleared up because blob file was relocated - { - assert_eq!( - crate::HashMap::default(), - **tree.current_version().gc_stats(), - ); - } - - Ok(()) - } } diff --git a/crates/lsm-tree/src/config/mod.rs b/crates/lsm-tree/src/config/mod.rs index 93c6b64da..07a64eec6 100644 --- a/crates/lsm-tree/src/config/mod.rs +++ b/crates/lsm-tree/src/config/mod.rs @@ -20,144 +20,16 @@ pub use restart_interval::RestartIntervalPolicy; pub type PartitioningPolicy = PinningPolicy; use crate::{ - compaction::filter::Factory, path::absolute_path, version::DEFAULT_LEVEL_COUNT, AnyTree, - BlobTree, Cache, CompressionType, DescriptorTable, SequenceNumberCounter, Tree, + Cache, CompressionType, DescriptorTable, SequenceNumberCounter, Tree, + compaction::filter::Factory, path::absolute_path, version::DEFAULT_LEVEL_COUNT, }; use std::{ path::{Path, PathBuf}, sync::Arc, }; -/// LSM-tree type -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TreeType { - /// Standard LSM-tree, see [`Tree`] - Standard, - - /// Key-value separated LSM-tree, see [`BlobTree`] - Blob, -} - -impl From for u8 { - fn from(val: TreeType) -> Self { - match val { - TreeType::Standard => 0, - TreeType::Blob => 1, - } - } -} - -impl TryFrom for TreeType { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Standard), - 1 => Ok(Self::Blob), - _ => Err(()), - } - } -} - const DEFAULT_FILE_FOLDER: &str = ".lsm.data"; -/// Options for key-value separation -#[derive(Clone, Debug, PartialEq)] -pub struct KvSeparationOptions { - /// What type of compression is used for blobs - #[doc(hidden)] - pub compression: CompressionType, - - /// Blob file target size in bytes - #[doc(hidden)] - pub file_target_size: u64, - - /// Key-value separation threshold in bytes - #[doc(hidden)] - pub separation_threshold: u32, - - #[doc(hidden)] - pub staleness_threshold: f32, - - #[doc(hidden)] - pub age_cutoff: f32, -} - -impl Default for KvSeparationOptions { - fn default() -> Self { - Self { - #[cfg(feature="lz4")] - compression: CompressionType::Lz4, - - #[cfg(not(feature="lz4"))] - compression: CompressionType::None, - - file_target_size: /* 64 MiB */ 64 * 1_024 * 1_024, - separation_threshold: /* 1 KiB */ 1_024, - - staleness_threshold: 0.25, - age_cutoff: 0.25, - } - } -} - -impl KvSeparationOptions { - /// Sets the blob compression method. - #[must_use] - pub fn compression(mut self, compression: CompressionType) -> Self { - self.compression = compression; - self - } - - /// Sets the target size of blob files. - /// - /// Smaller blob files allow more granular garbage collection - /// which allows lower space amp for lower write I/O cost. - /// - /// Larger blob files decrease the number of files on disk and maintenance - /// overhead. - /// - /// Defaults to 64 MiB. - #[must_use] - pub fn file_target_size(mut self, bytes: u64) -> Self { - self.file_target_size = bytes; - self - } - - /// Sets the key-value separation threshold in bytes. - /// - /// Smaller value will reduce compaction overhead and thus write amplification, - /// at the cost of lower read performance. - /// - /// Defaults to 1 KiB. - #[must_use] - pub fn separation_threshold(mut self, bytes: u32) -> Self { - self.separation_threshold = bytes; - self - } - - /// Sets the staleness threshold percentage. - /// - /// The staleness percentage determines how much a blob file needs to be fragmented to be - /// picked up by the garbage collection. - /// - /// Defaults to 33%. - #[must_use] - pub fn staleness_threshold(mut self, ratio: f32) -> Self { - self.staleness_threshold = ratio; - self - } - - /// Sets the age cutoff threshold. - /// - /// Defaults to 20%. - #[must_use] - pub fn age_cutoff(mut self, ratio: f32) -> Self { - self.age_cutoff = ratio; - self - } -} - /// Tree configuration builder pub struct Config { /// Folder path @@ -225,9 +97,6 @@ pub struct Config { /// Compaction filter factory pub compaction_filter_factory: Option>, - #[doc(hidden)] - pub kv_separation_opts: Option, - /// The global sequence number generator /// /// Should be shared between multple trees of a database @@ -282,8 +151,6 @@ impl Default for Config { compaction_filter_factory: None, expect_point_read_hits: false, - - kv_separation_opts: None, } } } @@ -443,13 +310,6 @@ impl Config { self } - /// Toggles key-value separation. - #[must_use] - pub fn with_kv_separation(mut self, opts: Option) -> Self { - self.kv_separation_opts = opts; - self - } - /// Installs a custom compaction filter. #[must_use] pub fn with_compaction_filter_factory(mut self, factory: Option>) -> Self { @@ -462,11 +322,7 @@ impl Config { /// # Errors /// /// Will return `Err` if an IO error occurs. - pub fn open(self) -> crate::Result { - Ok(if self.kv_separation_opts.is_some() { - AnyTree::Blob(BlobTree::open(self)?) - } else { - AnyTree::Standard(Tree::open(self)?) - }) + pub fn open(self) -> crate::Result { + Tree::open(self) } } diff --git a/crates/lsm-tree/src/descriptor_table.rs b/crates/lsm-tree/src/descriptor_table.rs index 12df8db2b..f55b1c1de 100644 --- a/crates/lsm-tree/src/descriptor_table.rs +++ b/crates/lsm-tree/src/descriptor_table.rs @@ -3,20 +3,14 @@ // (found in the LICENSE-* files in the repository) use crate::GlobalTableId; -use quick_cache::{sync::Cache as QuickCache, UnitWeighter}; +use quick_cache::{UnitWeighter, sync::Cache as QuickCache}; use std::{fs::File, sync::Arc}; -const TAG_BLOCK: u8 = 0; -const TAG_BLOB: u8 = 1; - type Item = Arc; -#[derive(Eq, std::hash::Hash, PartialEq)] -struct CacheKey(u8, u64, u64); - -/// Caches file descriptors to tables and blob files +/// Caches file descriptors to tables pub struct DescriptorTable { - inner: QuickCache, + inner: QuickCache, } impl DescriptorTable { @@ -41,33 +35,14 @@ impl DescriptorTable { #[must_use] pub fn access_for_table(&self, id: &GlobalTableId) -> Option> { - let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id()); - self.inner.get(&key) + self.inner.get(id) } pub fn insert_for_table(&self, id: GlobalTableId, item: Item) { - let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id()); - self.inner.insert(key, item); - } - - #[must_use] - pub fn access_for_blob_file(&self, id: &GlobalTableId) -> Option> { - let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id()); - self.inner.get(&key) - } - - pub fn insert_for_blob_file(&self, id: GlobalTableId, item: Item) { - let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id()); - self.inner.insert(key, item); + self.inner.insert(id, item); } pub fn remove_for_table(&self, id: &GlobalTableId) { - let key = CacheKey(TAG_BLOCK, id.tree_id(), id.table_id()); - self.inner.remove(&key); - } - - pub fn remove_for_blob_file(&self, id: &GlobalTableId) { - let key = CacheKey(TAG_BLOB, id.tree_id(), id.table_id()); - self.inner.remove(&key); + self.inner.remove(id); } } diff --git a/crates/lsm-tree/src/file.rs b/crates/lsm-tree/src/file.rs index 2c006ddef..8472f16fe 100644 --- a/crates/lsm-tree/src/file.rs +++ b/crates/lsm-tree/src/file.rs @@ -8,7 +8,6 @@ use std::{fs::File, io::Write, path::Path}; pub const MAGIC_BYTES: [u8; 4] = [b'L', b'S', b'M', 3]; pub const TABLES_FOLDER: &str = "tables"; -pub const BLOBS_FOLDER: &str = "blobs"; pub const CURRENT_VERSION_FILE: &str = "current"; /// Reads bytes from a file using `pread`. @@ -50,7 +49,10 @@ pub fn read_exact(file: &File, offset: u64, size: usize) -> std::io::Result Option> { - match self { - Self::File(fd) => Some(fd.clone()), - Self::DescriptorTable(descriptor_table) => { - descriptor_table.access_for_blob_file(table_id) - } - } - } - - pub fn insert_for_blob_file(&self, table_id: GlobalTableId, fd: Arc) { - if let Self::DescriptorTable(descriptor_table) = self { - descriptor_table.insert_for_blob_file(table_id, fd); - } - } } impl std::fmt::Debug for FileAccessor { diff --git a/crates/lsm-tree/src/format_version.rs b/crates/lsm-tree/src/format_version.rs index 37174af40..8773a31a5 100644 --- a/crates/lsm-tree/src/format_version.rs +++ b/crates/lsm-tree/src/format_version.rs @@ -16,6 +16,9 @@ pub enum FormatVersion { /// Fixed-width key/value lengths in table data blocks V4, + + /// 32-bit tree and table IDs + V5, } impl std::fmt::Display for FormatVersion { @@ -31,6 +34,7 @@ impl From for u8 { FormatVersion::V2 => 2, FormatVersion::V3 => 3, FormatVersion::V4 => 4, + FormatVersion::V5 => 5, } } } @@ -44,6 +48,7 @@ impl TryFrom for FormatVersion { 2 => Ok(Self::V2), 3 => Ok(Self::V3), 4 => Ok(Self::V4), + 5 => Ok(Self::V5), _ => Err(()), } } diff --git a/crates/lsm-tree/src/ingestion.rs b/crates/lsm-tree/src/ingestion.rs deleted file mode 100644 index b829690f9..000000000 --- a/crates/lsm-tree/src/ingestion.rs +++ /dev/null @@ -1,126 +0,0 @@ -// 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::{ - blob_tree::ingest::BlobIngestion, tree::ingest::Ingestion, AnyTree, UserKey, UserValue, -}; - -/// Unified ingestion builder over `AnyTree` -// Keep zero allocations and direct dispatch; boxing introduces heap indirection and `dyn` adds virtual dispatch. -// Ingestion calls use `&mut self` in tight loops; the active variant is stable and branch prediction makes the match cheap. -// Allowing this lint preserves hot-path performance at the cost of a larger enum size. -#[expect(clippy::large_enum_variant)] -pub enum AnyIngestion<'a> { - /// Ingestion for a standard LSM-tree - Standard(Ingestion<'a>), - - /// Ingestion for a [`BlobTree`] with KV separation - Blob(BlobIngestion<'a>), -} - -impl AnyIngestion<'_> { - /// Writes a key-value pair. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write, V: Into>( - &mut self, - key: K, - value: V, - ) -> crate::Result<()> { - match self { - Self::Standard(i) => i.write(key.into(), value.into()), - Self::Blob(b) => b.write(key.into(), value.into()), - } - } - - #[doc(hidden)] - pub fn write_prevalidated, V: Into>( - &mut self, - key: K, - value: V, - ) -> crate::Result<()> { - match self { - Self::Standard(i) => i.write_prevalidated(key.into(), value.into()), - Self::Blob(b) => b.write(key.into(), value.into()), - } - } - - /// Writes a tombstone for a key. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write_tombstone>(&mut self, key: K) -> crate::Result<()> { - match self { - Self::Standard(i) => i.write_tombstone(key.into()), - Self::Blob(b) => b.write_tombstone(key.into()), - } - } - - /// Writes a weak tombstone for a key. - /// - /// # Examples - /// - /// ``` - /// # use lsm_tree::Config; - /// # let folder = tempfile::tempdir()?; - /// # let tree = Config::new(folder, Default::default(), Default::default()).open()?; - /// # - /// let mut ingestion = tree.ingestion()?; - /// ingestion.write("a", "abc")?; - /// ingestion.write_weak_tombstone("b")?; - /// ingestion.finish()?; - /// # - /// # Ok::<(), lsm_tree::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write_weak_tombstone>(&mut self, key: K) -> crate::Result<()> { - match self { - Self::Standard(i) => i.write_weak_tombstone(key.into()), - Self::Blob(b) => b.write_weak_tombstone(key.into()), - } - } - - #[doc(hidden)] - pub fn write_prevalidated_weak_tombstone>( - &mut self, - key: K, - ) -> crate::Result<()> { - match self { - Self::Standard(i) => i.write_prevalidated_weak_tombstone(key.into()), - Self::Blob(b) => b.write_weak_tombstone(key.into()), - } - } - - /// Finalizes ingestion and registers created tables (and blob files if present). - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn finish(self) -> crate::Result<()> { - match self { - Self::Standard(i) => i.finish(), - Self::Blob(b) => b.finish(), - } - } -} - -impl AnyTree { - /// Starts an ingestion for any tree type (standard or blob). - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn ingestion(&self) -> crate::Result> { - match self { - Self::Standard(t) => Ok(AnyIngestion::Standard(Ingestion::new(t)?)), - Self::Blob(b) => Ok(AnyIngestion::Blob(BlobIngestion::new(b)?)), - } - } -} diff --git a/crates/lsm-tree/src/iter_guard.rs b/crates/lsm-tree/src/iter_guard.rs index 63fb9312e..6ee2d9211 100644 --- a/crates/lsm-tree/src/iter_guard.rs +++ b/crates/lsm-tree/src/iter_guard.rs @@ -1,10 +1,6 @@ -use crate::{ - blob_tree::Guard as BlobGuard, tree::Guard as StandardGuard, KvPair, UserKey, UserValue, -}; -use enum_dispatch::enum_dispatch; +use crate::{KvPair, UserKey, UserValue, tree::Guard as StandardGuard}; /// Guard to access key-value pairs -#[enum_dispatch] pub trait IterGuard { /// Accesses the key-value pair if the predicate returns `true`. /// @@ -53,12 +49,5 @@ pub trait IterGuard { } } -/// Generic iterator value -#[enum_dispatch(IterGuard)] -pub enum IterGuardImpl { - /// Iterator value of a standard LSM-tree - Standard(StandardGuard), - - /// Iterator value of a key-value separated tree - Blob(BlobGuard), -} +/// Iterator value. +pub type IterGuardImpl = StandardGuard; diff --git a/crates/lsm-tree/src/key.rs b/crates/lsm-tree/src/key.rs index 799897bbe..d1e0f2b71 100644 --- a/crates/lsm-tree/src/key.rs +++ b/crates/lsm-tree/src/key.rs @@ -29,7 +29,6 @@ impl std::fmt::Debug for InternalKey { ValueType::Value => "V", ValueType::Tombstone => "T", ValueType::WeakTombstone => "W", - ValueType::Indirection => "Vb", }, ) } diff --git a/crates/lsm-tree/src/lib.rs b/crates/lsm-tree/src/lib.rs index d429d71c5..c564d6a3b 100644 --- a/crates/lsm-tree/src/lib.rs +++ b/crates/lsm-tree/src/lib.rs @@ -60,18 +60,11 @@ macro_rules! fail_iter { } macro_rules! unwrap { - ($x:expr) => {{ - $x.expect("should read") - }}; + ($x:expr) => {{ $x.expect("should read") }}; } -mod any_tree; - mod abstract_tree; -#[doc(hidden)] -pub mod blob_tree; - #[doc(hidden)] mod cache; @@ -100,7 +93,6 @@ mod error; pub mod file; mod hash; -mod ingestion; mod iter_guard; mod key; mod key_range; @@ -112,11 +104,6 @@ mod run_scanner; #[doc(hidden)] pub mod merge; -#[cfg(feature = "metrics")] -pub(crate) mod metrics; - -// mod multi_reader; - #[doc(hidden)] pub mod mvcc_stream; @@ -145,8 +132,6 @@ pub mod util; mod value; mod value_type; mod version; -mod vlog; - /// User defined key (byte array) pub type UserKey = Slice; @@ -158,42 +143,34 @@ pub type KvPair = (UserKey, UserValue); #[doc(hidden)] pub use { - blob_tree::{handle::BlobIndirection, Guard as BlobGuard}, checksum::Checksum, iter_guard::IterGuardImpl, key_range::KeyRange, merge::BoxedIterator, slice::Builder, table::{GlobalTableId, Table, TableId}, - tree::inner::TreeId, tree::Guard as StandardGuard, + tree::inner::TreeId, value::InternalValue, }; pub use { abstract_tree::AbstractTree, - any_tree::AnyTree, - blob_tree::BlobTree, cache::Cache, compression::CompressionType, - config::{Config, KvSeparationOptions, TreeType}, + config::Config, descriptor_table::DescriptorTable, error::{Error, Result}, format_version::FormatVersion, - ingestion::AnyIngestion, iter_guard::IterGuard as Guard, memtable::{Memtable, MemtableId}, seqno::SequenceNumberCounter, slice::Slice, - tree::Tree, + tree::{Tree, ingest::Ingestion}, value::SeqNo, value_type::ValueType, - vlog::BlobFile, }; -#[cfg(feature = "metrics")] -pub use metrics::Metrics; - #[doc(hidden)] #[must_use] #[allow(missing_docs, clippy::missing_errors_doc, clippy::unwrap_used)] diff --git a/crates/lsm-tree/src/manifest.rs b/crates/lsm-tree/src/manifest.rs index 84e70431b..81057a6da 100644 --- a/crates/lsm-tree/src/manifest.rs +++ b/crates/lsm-tree/src/manifest.rs @@ -2,12 +2,11 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::{checksum::ChecksumType, FormatVersion, TreeType}; +use crate::{FormatVersion, checksum::ChecksumType}; use byteorder::ReadBytesExt; use std::{io::Read, path::Path}; pub struct Manifest { - pub version: FormatVersion, pub level_count: u8, } @@ -29,7 +28,11 @@ impl Manifest { FormatVersion::try_from(version).map_err(|()| crate::Error::InvalidVersion(version))? }; - let _tree_type: TreeType = { + if version != FormatVersion::V5 { + return Err(crate::Error::InvalidVersion(version.into())); + } + + { #[expect( clippy::expect_used, reason = "tree_type section must exist in manifest" @@ -40,10 +43,10 @@ impl Manifest { let mut reader = section.buf_reader(path)?; let tree_type = reader.read_u8()?; - tree_type - .try_into() - .map_err(|()| crate::Error::InvalidTag(("TreeType", tree_type)))? - }; + if tree_type != 0 { + return Err(crate::Error::InvalidTag(("TreeType", tree_type))); + } + } let level_count = { #[expect( @@ -85,9 +88,6 @@ impl Manifest { ); } - Ok(Self { - version, - level_count, - }) + Ok(Self { level_count }) } } diff --git a/crates/lsm-tree/src/memtable/mod.rs b/crates/lsm-tree/src/memtable/mod.rs index ced9c12b7..ea5f3364a 100644 --- a/crates/lsm-tree/src/memtable/mod.rs +++ b/crates/lsm-tree/src/memtable/mod.rs @@ -4,8 +4,8 @@ use crate::key::InternalKey; use crate::{ - value::{InternalValue, SeqNo, UserValue}, ValueType, + value::{InternalValue, SeqNo, UserValue}, }; use crossbeam_skiplist::SkipMap; use std::ops::RangeBounds; diff --git a/crates/lsm-tree/src/metrics.rs b/crates/lsm-tree/src/metrics.rs deleted file mode 100644 index fac3baa3e..000000000 --- a/crates/lsm-tree/src/metrics.rs +++ /dev/null @@ -1,212 +0,0 @@ -// 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::{AtomicU64, AtomicUsize}; - -/// Runtime metrics -/// -/// Are not stored durably, so metrics will reset after a restart/crash. -#[derive(Debug, Default)] -pub struct Metrics { - /// Number of times a table file was opened using `fopen()` - pub(crate) table_file_opened_uncached: AtomicUsize, - - /// Number of times a table file was retrieved from descriptor cache - pub(crate) table_file_opened_cached: AtomicUsize, - - /// Number of index blocks that were actually read from disk - pub(crate) index_block_load_io: AtomicUsize, - - /// Number of filter blocks that were actually read from disk - pub(crate) filter_block_load_io: AtomicUsize, - - /// Number of blocks that were actually read from disk - pub(crate) data_block_load_io: AtomicUsize, - - /// Number of blocks that were read from block cache - pub(crate) index_block_load_cached: AtomicUsize, - - /// Number of blocks that were read from block cache - pub(crate) filter_block_load_cached: AtomicUsize, - - /// Number of blocks that were read from block cache - pub(crate) data_block_load_cached: AtomicUsize, - - /// Number of filter queries that were performed - pub(crate) filter_queries: AtomicUsize, - - /// Number of IOs that were skipped due to filter - pub(crate) io_skipped_by_filter: AtomicUsize, - - /// Number of data block bytes that were requested from OS or disk - pub(crate) data_block_io_requested: AtomicU64, - - /// Number of index block bytes that were requested from OS or disk - pub(crate) index_block_io_requested: AtomicU64, - - /// Number of filter block bytes that were requested from OS or disk - pub(crate) filter_block_io_requested: AtomicU64, -} - -#[expect( - clippy::cast_precision_loss, - reason = "metrics can accept precision loss" -)] -impl Metrics { - /// Returns the cache hit rate for file descriptors in percent (0.0 - 1.0). - pub fn table_file_cache_hit_rate(&self) -> f64 { - let uncached = self.table_file_opened_uncached.load(Relaxed) as f64; - let cached = self.table_file_opened_cached.load(Relaxed) as f64; - - if cached + uncached == 0.0 { - 1.0 - } else { - cached / (cached + uncached) - } - } - - /// Number of I/O data block bytes transferred from disk or OS page cache. - pub fn data_block_io(&self) -> u64 { - self.data_block_io_requested.load(Relaxed) - } - - /// Number of I/O index block bytes transferred from disk or OS page cache. - pub fn index_block_io(&self) -> u64 { - self.index_block_io_requested.load(Relaxed) - } - - /// Number of I/O filter block bytes transferred from disk or OS page cache. - pub fn filter_block_io(&self) -> u64 { - self.filter_block_io_requested.load(Relaxed) - } - - /// Number of I/O block bytes transferred from disk or OS page cache. - pub fn block_io(&self) -> u64 { - self.data_block_io_requested.load(Relaxed) - + self.index_block_io_requested.load(Relaxed) - + self.filter_block_io_requested.load(Relaxed) - } - - /// Number of data blocks that were accessed. - pub fn data_block_load_count(&self) -> usize { - self.data_block_load_cached.load(Relaxed) + self.data_block_load_io.load(Relaxed) - } - - /// Number of index blocks that were accessed. - pub fn index_block_load_count(&self) -> usize { - self.index_block_load_cached.load(Relaxed) + self.index_block_load_io.load(Relaxed) - } - - /// Number of filter blocks that were accessed. - pub fn filter_block_load_count(&self) -> usize { - self.filter_block_load_cached.load(Relaxed) + self.filter_block_load_io.load(Relaxed) - } - - /// Number of blocks that were loaded from disk or OS page cache. - pub fn block_load_io_count(&self) -> usize { - self.data_block_load_io.load(Relaxed) - + self.index_block_load_io.load(Relaxed) - + self.filter_block_load_io.load(Relaxed) - } - - /// Number of data blocks that were loaded from disk or OS page cache. - pub fn data_block_load_cached_count(&self) -> usize { - self.data_block_load_cached.load(Relaxed) - } - - /// Number of index blocks that were loaded from disk or OS page cache. - pub fn index_block_load_cached_count(&self) -> usize { - self.index_block_load_cached.load(Relaxed) - } - - /// Number of filter blocks that were loaded from disk or OS page cache. - pub fn filter_block_load_cached_count(&self) -> usize { - self.filter_block_load_cached.load(Relaxed) - } - - /// Number of blocks that were loaded from disk or OS page cache. - pub fn block_load_cached_count(&self) -> usize { - self.data_block_load_cached.load(Relaxed) - + self.index_block_load_cached.load(Relaxed) - + self.filter_block_load_cached.load(Relaxed) - } - - /// Number of blocks that were accessed. - pub fn block_loads(&self) -> usize { - self.block_load_io_count() + self.block_load_cached_count() - } - - /// Data block cache efficiency in percent (0.0 - 1.0). - pub fn data_block_cache_hit_rate(&self) -> f64 { - let queries = self.data_block_load_count() as f64; - let hits = self.data_block_load_cached_count() as f64; - - if queries == 0.0 { - 1.0 - } else { - hits / queries - } - } - - /// Filter block cache efficiency in percent (0.0 - 1.0). - pub fn filter_block_cache_hit_rate(&self) -> f64 { - let queries = self.filter_block_load_count() as f64; - let hits = self.filter_block_load_cached_count() as f64; - - if queries == 0.0 { - 1.0 - } else { - hits / queries - } - } - - /// Index block cache efficiency in percent (0.0 - 1.0). - pub fn index_block_cache_hit_rate(&self) -> f64 { - let queries = self.index_block_load_count() as f64; - let hits = self.index_block_load_cached_count() as f64; - - if queries == 0.0 { - 1.0 - } else { - hits / queries - } - } - - /// Block cache efficiency in percent (0.0 - 1.0). - pub fn block_cache_hit_rate(&self) -> f64 { - let queries = self.block_loads() as f64; - let hits = self.block_load_cached_count() as f64; - - if queries == 0.0 { - 1.0 - } else { - hits / queries - } - } - - /// Filter efficiency in percent (0.0 - 1.0). - /// - /// Represents the ratio of I/O operations avoided due to filter. - pub fn filter_efficiency(&self) -> f64 { - let queries = self.filter_queries.load(Relaxed) as f64; - let io_skipped = self.io_skipped_by_filter.load(Relaxed) as f64; - - if queries == 0.0 { - 1.0 - } else { - io_skipped / queries - } - } - - /// Number of filter queries performed. - pub fn filter_queries(&self) -> usize { - self.filter_queries.load(Relaxed) - } - - /// Number of I/O operations skipped by filter. - pub fn io_skipped_by_filter(&self) -> usize { - self.io_skipped_by_filter.load(Relaxed) - } -} diff --git a/crates/lsm-tree/src/mvcc_stream.rs b/crates/lsm-tree/src/mvcc_stream.rs index e23246e35..59b93da16 100644 --- a/crates/lsm-tree/src/mvcc_stream.rs +++ b/crates/lsm-tree/src/mvcc_stream.rs @@ -88,7 +88,7 @@ impl>> DoubleEndedIte #[expect(clippy::string_lit_as_bytes)] mod tests { use super::*; - use crate::{value::InternalValue, ValueType}; + use crate::{ValueType, value::InternalValue}; use test_log::test; macro_rules! stream { diff --git a/crates/lsm-tree/src/range.rs b/crates/lsm-tree/src/range.rs index c0cd5df94..5afc8190d 100644 --- a/crates/lsm-tree/src/range.rs +++ b/crates/lsm-tree/src/range.rs @@ -3,6 +3,7 @@ // (found in the LICENSE-* files in the repository) use crate::{ + BoxedIterator, InternalValue, key::InternalKey, memtable::Memtable, merge::Merger, @@ -10,7 +11,6 @@ use crate::{ run_reader::RunReader, value::{SeqNo, UserKey}, version::SuperVersion, - BoxedIterator, InternalValue, }; use self_cell::self_cell; use std::{ diff --git a/crates/lsm-tree/src/run_reader.rs b/crates/lsm-tree/src/run_reader.rs index 96384e66b..2df3e01a1 100644 --- a/crates/lsm-tree/src/run_reader.rs +++ b/crates/lsm-tree/src/run_reader.rs @@ -2,7 +2,7 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::{version::Run, BoxedIterator, InternalValue, Table, UserKey}; +use crate::{BoxedIterator, InternalValue, Table, UserKey, version::Run}; use std::{ ops::{Deref, RangeBounds}, sync::Arc, diff --git a/crates/lsm-tree/src/run_scanner.rs b/crates/lsm-tree/src/run_scanner.rs index 90d2ed702..0de423b30 100644 --- a/crates/lsm-tree/src/run_scanner.rs +++ b/crates/lsm-tree/src/run_scanner.rs @@ -2,7 +2,7 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::{table::Scanner, version::Run, InternalValue, Table}; +use crate::{InternalValue, Table, table::Scanner, version::Run}; use std::sync::Arc; /// Scans through a disjoint run diff --git a/crates/lsm-tree/src/seqno.rs b/crates/lsm-tree/src/seqno.rs index 1fd8fb790..124696806 100644 --- a/crates/lsm-tree/src/seqno.rs +++ b/crates/lsm-tree/src/seqno.rs @@ -4,11 +4,11 @@ use crate::SeqNo; use std::sync::{ + Arc, atomic::{ AtomicU64, Ordering::{AcqRel, Acquire, Release}, }, - Arc, }; /// Thread-safe sequence number generator diff --git a/crates/lsm-tree/src/slice/mod.rs b/crates/lsm-tree/src/slice/mod.rs index 63f9c3745..24e0c6161 100644 --- a/crates/lsm-tree/src/slice/mod.rs +++ b/crates/lsm-tree/src/slice/mod.rs @@ -2,12 +2,6 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -// Using tokio bytes -#[cfg(feature = "bytes_1")] -mod slice_bytes; - -// Using byteview -#[cfg(not(feature = "bytes_1"))] mod slice_default; use std::{ @@ -15,12 +9,8 @@ use std::{ sync::Arc, }; -#[cfg(not(feature = "bytes_1"))] pub use slice_default::{Builder, Slice}; -#[cfg(feature = "bytes_1")] -pub use slice_bytes::{Builder, Slice}; - impl AsRef<[u8]> for Slice { fn as_ref(&self) -> &[u8] { &self.0 @@ -29,15 +19,7 @@ impl AsRef<[u8]> for Slice { impl From<&[u8]> for Slice { fn from(value: &[u8]) -> Self { - #[cfg(not(feature = "bytes_1"))] - { - Self(byteview::ByteView::new(value)) - } - - #[cfg(feature = "bytes_1")] - { - Self(bytes::Bytes::from(value.to_vec())) - } + Self(byteview::ByteView::new(value)) } } diff --git a/crates/lsm-tree/src/slice/slice_bytes/mod.rs b/crates/lsm-tree/src/slice/slice_bytes/mod.rs deleted file mode 100644 index 936fe423d..000000000 --- a/crates/lsm-tree/src/slice/slice_bytes/mod.rs +++ /dev/null @@ -1,115 +0,0 @@ -// 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 bytes::Bytes; - -pub use bytes::BytesMut as Builder; - -/// An immutable byte slice that can be cloned without additional heap allocation -/// -/// There is no guarantee of any sort of alignment for zero-copy (de)serialization. -#[derive(Debug, Default, Clone, Eq, Hash, Ord)] -pub struct Slice(pub(super) Bytes); - -impl Slice { - /// Construct a [`Slice`] from a byte slice. - #[must_use] - pub fn new(bytes: &[u8]) -> Self { - Self(Bytes::copy_from_slice(bytes)) - } - - /// Returns the bytes slice containing the entire data. - #[must_use] - pub fn as_slice(&self) -> &[u8] { - self - } - - #[doc(hidden)] - #[must_use] - pub fn empty() -> Self { - Self::default() - } - - #[doc(hidden)] - pub unsafe fn builder_unzeroed(len: usize) -> Builder { - // Use `with_capacity` & `set_len`` to avoid zeroing the buffer - let mut builder = Builder::with_capacity(len); - - // SAFETY: we just allocated `len` bytes, and `read_exact` will fail if - // it doesn't fill the buffer, subsequently dropping the uninitialized - // BytesMut object - #[expect(unsafe_code, reason = "see safety")] - unsafe { - builder.set_len(len); - } - - builder - } - - #[doc(hidden)] - #[must_use] - pub fn slice(&self, range: impl std::ops::RangeBounds) -> Self { - Self(self.0.slice(range)) - } - - #[doc(hidden)] - #[must_use] - pub fn fused(left: &[u8], right: &[u8]) -> Self { - use std::io::Write; - - let len = left.len() + right.len(); - let mut builder = unsafe { Self::builder_unzeroed(len) }; - { - let mut writer = &mut builder[..]; - - writer.write_all(left).expect("should write"); - writer.write_all(right).expect("should write"); - } - - Self(builder.freeze()) - } - - /// Constructs a [`Slice`] from an I/O reader by pulling in `len` bytes. - /// - /// The reader may not read the existing buffer. - #[doc(hidden)] - pub fn from_reader(reader: &mut R, len: usize) -> std::io::Result { - let mut builder = unsafe { Self::builder_unzeroed(len) }; - - // SAFETY: Normally, read_exact over an uninitialized buffer is UB, - // however we know that in lsm-tree etc. only I/O readers or cursors over Vecs are used - // so it's safe - // - // The safe API is unstable: https://github.com/rust-lang/rust/issues/78485 - reader.read_exact(&mut builder)?; - - Ok(Self(builder.freeze())) - } -} - -impl From for Slice { - fn from(value: Bytes) -> Self { - Self(value) - } -} - -impl From for Bytes { - fn from(value: Slice) -> Self { - value.0 - } -} - -// Bytes::from> is zero-copy optimized -impl From> for Slice { - fn from(value: Vec) -> Self { - Self(Bytes::from(value)) - } -} - -// Bytes::from is zero-copy optimized -impl From for Slice { - fn from(value: String) -> Self { - Self(Bytes::from(value)) - } -} diff --git a/crates/lsm-tree/src/stop_signal.rs b/crates/lsm-tree/src/stop_signal.rs index 8d83cfaab..5eaa4c91a 100644 --- a/crates/lsm-tree/src/stop_signal.rs +++ b/crates/lsm-tree/src/stop_signal.rs @@ -2,7 +2,7 @@ // 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::AtomicBool, Arc}; +use std::sync::{Arc, atomic::AtomicBool}; #[derive(Clone, Debug, Default)] pub struct StopSignal(Arc); diff --git a/crates/lsm-tree/src/table/block_index/two_level.rs b/crates/lsm-tree/src/table/block_index/two_level.rs index 3d0e3b5fe..c30b8583a 100644 --- a/crates/lsm-tree/src/table/block_index/two_level.rs +++ b/crates/lsm-tree/src/table/block_index/two_level.rs @@ -15,9 +15,6 @@ use crate::{ }; use std::{path::PathBuf, sync::Arc}; -#[cfg(feature = "metrics")] -use crate::Metrics; - /// Index that translates item keys to data block handles /// /// Only the top-level index is loaded into memory. @@ -28,9 +25,6 @@ pub struct TwoLevelBlockIndex { pub(crate) file_accessor: FileAccessor, pub(crate) cache: Arc, pub(crate) compression: CompressionType, - - #[cfg(feature = "metrics")] - pub(crate) metrics: Arc, } impl TwoLevelBlockIndex { @@ -47,9 +41,6 @@ impl TwoLevelBlockIndex { file_accessor: self.file_accessor.clone(), cache: self.cache.clone(), compression: self.compression, - - #[cfg(feature = "metrics")] - metrics: self.metrics.clone(), } } } @@ -69,9 +60,6 @@ pub struct Iter { file_accessor: FileAccessor, cache: Arc, compression: CompressionType, - - #[cfg(feature = "metrics")] - metrics: Arc, } impl Iter { @@ -133,8 +121,6 @@ impl Iterator for Iter { &handle.into_inner(), BlockType::Index, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )); let index_block = IndexBlock::new(block); @@ -196,8 +182,6 @@ impl DoubleEndedIterator for Iter { &handle.into_inner(), BlockType::Index, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )); let index_block = IndexBlock::new(block); diff --git a/crates/lsm-tree/src/table/block_index/volatile.rs b/crates/lsm-tree/src/table/block_index/volatile.rs index ad957d3ec..0e7fce1f6 100644 --- a/crates/lsm-tree/src/table/block_index/volatile.rs +++ b/crates/lsm-tree/src/table/block_index/volatile.rs @@ -15,9 +15,6 @@ use crate::{ }; use std::{path::PathBuf, sync::Arc}; -#[cfg(feature = "metrics")] -use crate::Metrics; - /// Index that translates item keys to data block handles /// /// The index is loaded on demand. @@ -28,9 +25,6 @@ pub struct VolatileBlockIndex { pub(crate) cache: Arc, pub(crate) handle: BlockHandle, pub(crate) compression: CompressionType, - - #[cfg(feature = "metrics")] - pub(crate) metrics: Arc, } impl VolatileBlockIndex { @@ -56,9 +50,6 @@ pub struct Iter { lo: Option<(UserKey, SeqNo)>, hi: Option<(UserKey, SeqNo)>, - - #[cfg(feature = "metrics")] - pub(crate) metrics: Arc, } impl Iter { @@ -74,9 +65,6 @@ impl Iter { lo: None, hi: None, - - #[cfg(feature = "metrics")] - metrics: index.metrics.clone(), } } } @@ -108,8 +96,6 @@ impl Iterator for Iter { &self.handle, BlockType::Index, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )); let index_block = IndexBlock::new(block); @@ -148,8 +134,6 @@ impl DoubleEndedIterator for Iter { &self.handle, BlockType::Index, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )); let index_block = IndexBlock::new(block); diff --git a/crates/lsm-tree/src/table/data_block/mod.rs b/crates/lsm-tree/src/table/data_block/mod.rs index 777f3b9dd..ad0074f52 100644 --- a/crates/lsm-tree/src/table/data_block/mod.rs +++ b/crates/lsm-tree/src/table/data_block/mod.rs @@ -851,39 +851,6 @@ mod tests { Ok(()) } - #[test] - fn data_block_vhandle() -> crate::Result<()> { - let items = [InternalValue::from_components( - "abc", - "world", - 1, - crate::ValueType::Indirection, - )]; - - for restart_interval in 1..=16 { - let bytes = DataBlock::encode_into_vec(&items, restart_interval, 0.0)?; - let serialized_len = bytes.len(); - - let data_block = DataBlock::new(Block { - data: bytes.into(), - header: Header { - block_type: BlockType::Data, - checksum: Checksum::from_raw(0), - data_length: 0, - uncompressed_length: 0, - }, - }); - - assert_eq!(data_block.len(), items.len()); - assert_eq!(data_block.inner.size(), serialized_len); - - assert_eq!(Some(items[0].clone()), data_block.point_read(b"abc", 777)); - assert!(data_block.point_read(b"abc", 1).is_none()); - } - - Ok(()) - } - #[test] fn data_block_mvcc_read_first() -> crate::Result<()> { let items = [InternalValue::from_components( diff --git a/crates/lsm-tree/src/table/filter/blocked_bloom/builder._rs b/crates/lsm-tree/src/table/filter/blocked_bloom/builder._rs deleted file mode 100644 index 4aac77c8b..000000000 --- a/crates/lsm-tree/src/table/filter/blocked_bloom/builder._rs +++ /dev/null @@ -1,169 +0,0 @@ -// 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::super::bit_array::Builder as BitArrayBuilder; -use super::super::standard_bloom::builder::secondary_hash; -use crate::{ - file::MAGIC_BYTES, - table::filter::{blocked_bloom::CACHE_LINE_BYTES, FilterType}, -}; -use byteorder::{LittleEndian, WriteBytesExt}; -use std::io::Write; - -#[derive(Debug)] -pub struct Builder { - /// Raw bytes exposed as bit array - inner: BitArrayBuilder, - - /// Number of hash functions - pub(crate) k: usize, - - /// Number of blocks in the blocked bloom filter - pub(crate) num_blocks: usize, -} - -impl Builder { - #[expect( - clippy::expect_used, - reason = "we write into a Vec, so no I/O error can happen" - )] - #[must_use] - pub fn build(&self) -> Vec { - let mut v = vec![]; - - // Write header - v.write_all(&MAGIC_BYTES).expect("should not fail"); - - // NOTE: Filter type - v.write_u8(FilterType::BlockedBloom.into()) - .expect("should not fail"); - - // NOTE: Hash type (unused) - v.write_u8(0).expect("should not fail"); - - v.write_u64::(self.num_blocks as u64) - .expect("should not fail"); - v.write_u64::(self.k as u64) - .expect("should not fail"); - v.write_all(self.inner.bytes()).expect("should not fail"); - - v - } - - /// Constructs a bloom filter that can hold `n` items - /// while maintaining a certain false positive rate `fpr`. - #[must_use] - pub fn with_fp_rate(n: usize, fpr: f32) -> Self { - use std::f32::consts::LN_2; - - assert!(n > 0); - - // NOTE: Some sensible minimum - let fpr = fpr.max(0.000_000_1); - - // NOTE: We add ~5-25% more bits to account for blocked bloom filters being a bit less accurate - // See https://dl.acm.org/doi/10.1145/1498698.1594230 - let bonus = match fpr { - _ if fpr <= 0.001 => 1.25, - _ if fpr <= 0.01 => 1.2, - _ if fpr <= 0.1 => 1.1, - _ => 1.05, - }; - - let m = ((Self::calculate_m(n, fpr)) as f32 * bonus) as usize; - let bpk = m / n; - let k = (((bpk as f32) * LN_2) as usize).max(1); - - let num_blocks = m.div_ceil(CACHE_LINE_BYTES * 8); - - Self { - inner: BitArrayBuilder::with_capacity(num_blocks * CACHE_LINE_BYTES), - k, - num_blocks, - } - } - - /// Constructs a bloom filter that can hold `n` items - /// with `bpk` bits per key. - /// - /// 10 bits per key is a sensible default. - #[must_use] - pub fn with_bpk(n: usize, bpk: u8) -> Self { - use std::f32::consts::LN_2; - - assert!(bpk > 0); - assert!(n > 0); - - let bpk = bpk as usize; - - let m = n * bpk; - let k = (((bpk as f32) * LN_2) as usize).max(1); - - let num_blocks = m.div_ceil(CACHE_LINE_BYTES * 8); - - Self { - inner: BitArrayBuilder::with_capacity(num_blocks * CACHE_LINE_BYTES), - k, - num_blocks, - } - } - - fn calculate_m(n: usize, fp_rate: f32) -> usize { - use std::f32::consts::LN_2; - - let n = n as f32; - let ln2_squared = LN_2.powi(2); - - let numerator = n * fp_rate.ln(); - let m = -(numerator / ln2_squared); - - // Round up to next byte - ((m / 8.0).ceil() * 8.0) as usize - } - - /// Adds the key to the filter. - pub fn set_with_hash(&mut self, mut h1: u64) { - let mut h2 = secondary_hash(h1); - - let block_idx = h1 % (self.num_blocks as u64); - - for i in 1..(self.k as u64) { - let idx = h1 % (CACHE_LINE_BYTES as u64 * 8); - - #[expect( - clippy::cast_possible_truncation, - reason = "filters tend to be pretty small, definitely less than 4 GiB, even for large tables" - )] - self.inner - .enable_bit(Self::get_bit_idx(block_idx as usize, idx as usize)); - - h1 = h1.wrapping_add(h2); - h2 = h2.wrapping_mul(i); - } - } - - #[must_use] - pub fn get_bit_idx(block_idx: usize, idx_in_block: usize) -> usize { - block_idx * CACHE_LINE_BYTES * 8 + idx_in_block - } - - /// Gets the hash of a key. - #[must_use] - pub fn get_hash(key: &[u8]) -> u64 { - super::super::standard_bloom::Builder::get_hash(key) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn bloom_calculate_m() { - assert_eq!(9_592, Builder::calculate_m(1_000, 0.01)); - assert_eq!(4_800, Builder::calculate_m(1_000, 0.1)); - assert_eq!(4_792_536, Builder::calculate_m(1_000_000, 0.1)); - } -} diff --git a/crates/lsm-tree/src/table/filter/blocked_bloom/mod._rs b/crates/lsm-tree/src/table/filter/blocked_bloom/mod._rs deleted file mode 100644 index da0fb689a..000000000 --- a/crates/lsm-tree/src/table/filter/blocked_bloom/mod._rs +++ /dev/null @@ -1,193 +0,0 @@ -// 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 builder; - -pub use builder::Builder; - -use super::bit_array::BitArrayReader; -use crate::{ - file::MAGIC_BYTES, - table::filter::{standard_bloom::builder::secondary_hash, FilterType}, -}; -use byteorder::{LittleEndian, ReadBytesExt}; -use std::io::{Cursor, Read}; - -const CACHE_LINE_BYTES: usize = 64; - -/// A blocked bloom filter -/// -/// Allows buffering the key hashes before actual filter construction -/// which is needed to properly calculate the filter size, as the number of items -/// are unknown during table construction. -/// -/// The filter uses double hashing instead of `k` hash functions, see: -/// -#[derive(Debug)] -pub struct BlockedBloomFilterReader<'a> { - /// Raw bytes exposed as bit array - inner: BitArrayReader<'a>, - - /// Number of hash functions - pub(crate) k: usize, - - /// Number of blocks in the blocked bloom filter - pub(crate) num_blocks: usize, -} - -impl<'a> BlockedBloomFilterReader<'a> { - pub fn new(slice: &'a [u8]) -> crate::Result { - let mut reader = Cursor::new(slice); - - // Check header - let mut magic = [0u8; MAGIC_BYTES.len()]; - reader.read_exact(&mut magic)?; - - if magic != MAGIC_BYTES { - return Err(crate::Error::InvalidHeader("BloomFilter")); - } - - // NOTE: Filter type - let filter_type = reader.read_u8()?; - let filter_type = FilterType::try_from(filter_type)?; - assert_eq!( - FilterType::BlockedBloom, - filter_type, - "Invalid filter type, got={filter_type:?}, expected={:?}", - FilterType::BlockedBloom, - ); - - // NOTE: Hash type (unused) - let hash_type = reader.read_u8()?; - assert_eq!(0, hash_type, "Invalid bloom hash type"); - - let num_blocks = reader.read_u64::()? as usize; - let k = reader.read_u64::()? as usize; - - let offset = reader.position() as usize; - - Ok(Self { - k, - num_blocks, - inner: BitArrayReader::new(slice.get(offset..).expect("should be in bounds")), - }) - } - - fn bytes(&self) -> &[u8] { - self.inner.bytes() - } - - /// Size of bloom filter in bytes - #[must_use] - fn len(&self) -> usize { - self.inner.bytes().len() - } - - /// Returns `true` if the hash may be contained. - /// - /// Will never have a false negative. - #[must_use] - pub fn contains_hash(&self, mut h1: u64) -> bool { - let mut h2 = secondary_hash(h1); - - let block_idx = h1 % (self.num_blocks as u64); - - for i in 1..(self.k as u64) { - let bit_idx = h1 % (CACHE_LINE_BYTES as u64 * 8); - - if !self.has_bit(block_idx as usize, bit_idx as usize) { - return false; - } - - h1 = h1.wrapping_add(h2); - h2 = h2.wrapping_mul(i); - } - - true - } - - /// Returns `true` if the bit at `idx` is `1`. - fn has_bit(&self, block_idx: usize, idx_in_block: usize) -> bool { - self.inner - .get(Builder::get_bit_idx(block_idx, idx_in_block)) - } - - /// Gets the hash of a key. - #[must_use] - pub fn get_hash(key: &[u8]) -> u64 { - Builder::get_hash(key) - } - - /// Returns `true` if the item may be contained. - /// - /// Will never have a false negative. - #[must_use] - pub fn contains(&self, key: &[u8]) -> bool { - self.contains_hash(Self::get_hash(key)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn filter_bloom_blocked_serde_round_trip() -> crate::Result<()> { - let mut filter = Builder::with_fp_rate(10, 0.0001); - - let keys = &[ - b"item0", b"item1", b"item2", b"item3", b"item4", b"item5", b"item6", b"item7", - b"item8", b"item9", - ]; - - for key in keys { - filter.set_with_hash(BlockedBloomFilterReader::get_hash(*key)); - } - - let filter_bytes = filter.build(); - let filter_copy = BlockedBloomFilterReader::new(&filter_bytes)?; - - assert_eq!(filter.k, filter_copy.k); - assert_eq!(filter.num_blocks, filter_copy.num_blocks); - assert!(!filter_copy.contains(b"asdasads")); - assert!(!filter_copy.contains(b"item10")); - assert!(!filter_copy.contains(b"cxycxycxy")); - - Ok(()) - } - - #[test] - fn filter_bloom_blocked_basic() -> crate::Result<()> { - let mut filter = Builder::with_fp_rate(10, 0.0001); - - let keys = [ - b"item0" as &[u8], - b"item1", - b"item2", - b"item3", - b"item4", - b"item5", - b"item6", - b"item7", - b"item8", - b"item9", - ]; - - for key in &keys { - filter.set_with_hash(Builder::get_hash(key)); - } - - let filter_bytes = filter.build(); - let filter = BlockedBloomFilterReader::new(&filter_bytes)?; - - for key in &keys { - assert!(filter.contains(key)); - } - - assert!(!filter.contains(b"asdasdasdasdasdasdasd")); - - Ok(()) - } -} diff --git a/crates/lsm-tree/src/table/filter/mod.rs b/crates/lsm-tree/src/table/filter/mod.rs index 012d9ce3f..f4ee4d9cd 100644 --- a/crates/lsm-tree/src/table/filter/mod.rs +++ b/crates/lsm-tree/src/table/filter/mod.rs @@ -4,7 +4,6 @@ pub mod bit_array; pub mod block; -// pub mod blocked_bloom; pub mod standard_bloom; use standard_bloom::Builder as StandardBloomFilterBuilder; diff --git a/crates/lsm-tree/src/table/id.rs b/crates/lsm-tree/src/table/id.rs index 309c5e3d8..05b952f5f 100644 --- a/crates/lsm-tree/src/table/id.rs +++ b/crates/lsm-tree/src/table/id.rs @@ -2,9 +2,9 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::tree::inner::TreeId; +use crate::{SequenceNumberCounter, tree::inner::TreeId}; -pub type TableId = u64; +pub type TableId = u32; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GlobalTableId(TreeId, TableId); @@ -22,11 +22,15 @@ impl GlobalTableId { } impl From<(TreeId, TableId)> for GlobalTableId { - fn from((tid, sid): (TreeId, TableId)) -> Self { - Self(tid, sid) + fn from((tree_id, table_id): (TreeId, TableId)) -> Self { + Self(tree_id, table_id) } } +pub(crate) fn next_table_id(counter: &SequenceNumberCounter) -> TableId { + counter.next().try_into().expect("ran out of table IDs") +} + #[cfg(test)] mod tests { use super::*; @@ -40,5 +44,6 @@ mod tests { assert_eq!(global_table_id.tree_id(), 42); assert_eq!(global_table_id.table_id(), 7); + assert_eq!(size_of::(), 8); } } diff --git a/crates/lsm-tree/src/table/inner.rs b/crates/lsm-tree/src/table/inner.rs index 749768e48..5c0336198 100644 --- a/crates/lsm-tree/src/table/inner.rs +++ b/crates/lsm-tree/src/table/inner.rs @@ -2,9 +2,6 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - use super::{block_index::BlockIndexImpl, meta::ParsedMeta, regions::ParsedRegions}; use crate::{ Checksum, GlobalTableId, SeqNo, @@ -15,7 +12,7 @@ use crate::{ }; use std::{ path::PathBuf, - sync::{Arc, OnceLock, atomic::AtomicBool}, + sync::{Arc, atomic::AtomicBool}, }; pub struct Inner { @@ -58,13 +55,6 @@ pub struct Inner { pub(super) checksum: Checksum, pub(super) global_seqno: SeqNo, - - #[cfg(feature = "metrics")] - pub(crate) metrics: Arc, - - /// Cached sum of referenced blob file bytes for this table. - /// Lazily computed on first access to avoid repeated I/O in compaction decisions. - pub(crate) cached_blob_bytes: OnceLock, } impl Inner { diff --git a/crates/lsm-tree/src/table/iter.rs b/crates/lsm-tree/src/table/iter.rs index 1eb9c0193..9cdf509d4 100644 --- a/crates/lsm-tree/src/table/iter.rs +++ b/crates/lsm-tree/src/table/iter.rs @@ -16,9 +16,6 @@ use crate::{ use self_cell::self_cell; use std::{path::PathBuf, sync::Arc}; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - type InnerIter<'a> = DataBlockIter<'a>; pub enum Bound { @@ -113,9 +110,6 @@ pub struct Iter { hi_data_block: Option, range: Bounds, - - #[cfg(feature = "metrics")] - metrics: Arc, } impl Iter { @@ -127,7 +121,6 @@ impl Iter { file_accessor: FileAccessor, cache: Arc, compression: CompressionType, - #[cfg(feature = "metrics")] metrics: Arc, ) -> Self { Self { table_id, @@ -149,9 +142,6 @@ impl Iter { hi_data_block: None, range: (None, None), - - #[cfg(feature = "metrics")] - metrics, } } @@ -257,8 +247,6 @@ impl Iterator for Iter { &BlockHandle::new(handle.offset(), handle.size()), crate::table::block::BlockType::Data, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )) } }; @@ -378,8 +366,6 @@ impl DoubleEndedIterator for Iter { &BlockHandle::new(handle.offset(), handle.size()), crate::table::block::BlockType::Data, self.compression, - #[cfg(feature = "metrics")] - &self.metrics, )) } }; diff --git a/crates/lsm-tree/src/table/meta.rs b/crates/lsm-tree/src/table/meta.rs index 946c855ef..646cb97f0 100644 --- a/crates/lsm-tree/src/table/meta.rs +++ b/crates/lsm-tree/src/table/meta.rs @@ -74,6 +74,17 @@ macro_rules! read_u64 { }}; } +macro_rules! read_u32 { + ($block:expr, $name:expr) => {{ + let bytes = $block + .point_read($name, SeqNo::MAX) + .unwrap_or_else(|| panic!("meta property {:?} should exist", $name)); + + let mut bytes = &bytes.value[..]; + bytes.read_u32::()? + }}; +} + impl ParsedMeta { #[expect(clippy::expect_used, clippy::too_many_lines)] pub fn load_with_handle(file: &File, handle: &BlockHandle) -> crate::Result { @@ -95,11 +106,7 @@ impl ParsedMeta { .expect("Table version should exist") .value; - assert!( - matches!(&*table_version, [3u8 | 4u8]), - "unsupported table version {}", - table_version[0], - ); + assert_eq!(&*table_version, [5], "unsupported table version"); } { @@ -136,7 +143,7 @@ impl ParsedMeta { "index block restart intervals >1 are not supported for this version", ); - let id = read_u64!(block, b"table_id"); + let id = read_u32!(block, b"table_id"); let item_count = read_u64!(block, b"item_count"); let tombstone_count = read_u64!(block, b"tombstone_count"); let data_block_count = read_u64!(block, b"block_count#data"); diff --git a/crates/lsm-tree/src/table/mod.rs b/crates/lsm-tree/src/table/mod.rs index 38da2de39..553b4c46b 100644 --- a/crates/lsm-tree/src/table/mod.rs +++ b/crates/lsm-tree/src/table/mod.rs @@ -22,6 +22,7 @@ mod tests; pub use block::{Block, BlockOffset}; pub use data_block::DataBlock; +pub(crate) use id::next_table_id; pub use id::{GlobalTableId, TableId}; pub use index_block::{BlockHandle, IndexBlock, KeyedBlockHandle}; pub use scanner::Scanner; @@ -37,7 +38,6 @@ use crate::{ block_index::{BlockIndex, FullBlockIndex, TwoLevelBlockIndex, VolatileBlockIndex}, filter::block::FilterBlock, regions::ParsedRegions, - writer::LinkedFile, }, value::PointReadValue, }; @@ -53,9 +53,6 @@ use std::{ }; use util::load_block; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - pub type TableInner = Inner; /// A disk segment (a.k.a. `Table`, `SSTable`, `SST`, `sorted string table`) that is located on disk @@ -91,70 +88,6 @@ impl Table { self.0.global_seqno } - pub fn referenced_blob_bytes(&self) -> crate::Result { - if let Some(v) = self.0.cached_blob_bytes.get() { - return Ok(*v); - } - - let sum = self - .list_blob_file_references()? - .map(|bf| bf.iter().map(|f| f.on_disk_bytes).sum::()) - .unwrap_or_default(); - - let _ = self.0.cached_blob_bytes.set(sum); - Ok(sum) - } - - pub fn list_blob_file_references(&self) -> crate::Result>> { - use byteorder::{LE, ReadBytesExt}; - - Ok(if let Some(handle) = &self.regions.linked_blob_files { - let table_id = self.global_id(); - - let (fd, fd_cache_miss) = - if let Some(fd) = self.file_accessor.access_for_table(&table_id) { - (fd, false) - } else { - (Arc::new(File::open(&*self.path)?), true) - }; - - // Read the exact region using pread-style helper - let buf = crate::file::read_exact(&fd, *handle.offset(), handle.size() as usize)?; - - // If we opened the file here, cache the FD for future accesses - if fd_cache_miss { - self.file_accessor.insert_for_table(table_id, fd); - } - - // Parse the buffer - let mut reader = &buf[..]; - let len = reader.read_u32::()?; - let mut blob_files = Vec::with_capacity(len as usize); - - for _ in 0..len { - let blob_file_id = reader.read_u64::()?; - let len = reader.read_u64::()?; - let bytes = reader.read_u64::()?; - let on_disk_bytes = reader.read_u64::()?; - - #[expect( - clippy::cast_possible_truncation, - reason = "truncation is not expected to happen" - )] - blob_files.push(LinkedFile { - blob_file_id, - bytes, - len: len as usize, - on_disk_bytes, - }); - } - - Some(blob_files) - } else { - None - }) - } - /// Gets the global table ID. #[must_use] fn global_id(&self) -> GlobalTableId { @@ -208,8 +141,6 @@ impl Table { handle, block_type, compression, - #[cfg(feature = "metrics")] - &self.metrics, ) } @@ -252,9 +183,6 @@ impl Table { key_hash: u64, point_read: impl FnOnce(&Self, &[u8], SeqNo) -> crate::Result>, ) -> crate::Result> { - #[cfg(feature = "metrics")] - use std::sync::atomic::Ordering::Relaxed; - // Translate seqno to "our" seqno let seqno = seqno.saturating_sub(self.global_seqno()); @@ -299,35 +227,11 @@ impl Table { if let Some(filter_block) = &filter_block { if !filter_block.maybe_contains_hash(key_hash)? { - #[cfg(feature = "metrics")] - { - self.metrics.filter_queries.fetch_add(1, Relaxed); - self.metrics.io_skipped_by_filter.fetch_add(1, Relaxed); - } - return Ok(None); } } - let item = point_read(self, key, seqno); - - #[cfg(not(feature = "metrics"))] - { - item - } - - #[cfg(feature = "metrics")] - { - // NOTE: Only increment the filter queries when the filter reported a miss - // and we actually waste an I/O for a non-existing item. - // Otherwise, the filter efficiency decreases whenever an item is hit. - // https://github.com/fjall-rs/lsm-tree/issues/246 - item.inspect(|maybe_kv| { - if maybe_kv.is_none() && filter_block.is_some() { - self.metrics.filter_queries.fetch_add(1, Relaxed); - } - }) - } + point_read(self, key, seqno) } fn point_read(&self, key: &[u8], seqno: SeqNo) -> crate::Result> { @@ -432,8 +336,6 @@ impl Table { self.file_accessor.clone(), self.cache.clone(), self.metadata.data_block_compression, - #[cfg(feature = "metrics")] - self.metrics.clone(), ); match range.start_bound() { @@ -485,7 +387,6 @@ impl Table { descriptor_table: Option>, pin_filter: bool, pin_index: bool, - #[cfg(feature = "metrics")] metrics: Arc, ) -> crate::Result { use meta::ParsedMeta; use regions::ParsedRegions; @@ -495,12 +396,20 @@ impl Table { let mut file = std::fs::File::open(&file_path)?; let file_path = Arc::new(file_path); - #[cfg(feature = "metrics")] - metrics - .table_file_opened_uncached - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let trailer = sfa::Reader::from_reader(&mut file)?; + + let table_version = trailer + .toc() + .section(b"table_version") + .ok_or(crate::Error::Unrecoverable)?; + if table_version.len() != 1 { + return Err(crate::Error::Unrecoverable); + } + let version = byteorder::ReadBytesExt::read_u8(&mut table_version.buf_reader(&file_path)?)?; + if version != 5 { + return Err(crate::Error::InvalidVersion(version)); + } + let regions = ParsedRegions::parse_from_toc(trailer.toc())?; log::trace!("Reading meta block, with meta_ptr={:?}", regions.metadata); @@ -529,9 +438,6 @@ impl Table { path: Arc::clone(&file_path), file_accessor: file_accessor.clone(), table_id: (tree_id, metadata.id).into(), - - #[cfg(feature = "metrics")] - metrics: metrics.clone(), }) } else if pin_index { log::trace!( @@ -551,9 +457,6 @@ impl Table { handle: regions.tli, path: Arc::clone(&file_path), table_id: (tree_id, metadata.id).into(), - - #[cfg(feature = "metrics")] - metrics: metrics.clone(), }) }; @@ -624,11 +527,6 @@ impl Table { checksum, global_seqno, - - #[cfg(feature = "metrics")] - metrics, - - cached_blob_bytes: std::sync::OnceLock::new(), }))) } diff --git a/crates/lsm-tree/src/table/multi_writer.rs b/crates/lsm-tree/src/table/multi_writer.rs index 1464028e7..44a31309d 100644 --- a/crates/lsm-tree/src/table/multi_writer.rs +++ b/crates/lsm-tree/src/table/multi_writer.rs @@ -2,11 +2,9 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use super::{filter::BloomConstructionPolicy, writer::Writer}; +use super::{filter::BloomConstructionPolicy, id::next_table_id, writer::Writer}; use crate::{ - Checksum, CompressionType, HashMap, SequenceNumberCounter, TableId, UserKey, - blob_tree::handle::BlobIndirection, table::writer::LinkedFile, value::InternalValue, - vlog::BlobFileId, + Checksum, CompressionType, SequenceNumberCounter, TableId, UserKey, value::InternalValue, }; use std::path::PathBuf; @@ -44,9 +42,6 @@ pub struct MultiWriter { bloom_policy: BloomConstructionPolicy, current_key: Option, - has_items: bool, - - linked_blobs: HashMap, /// Level the tables are written to initial_level: u8, @@ -60,7 +55,7 @@ impl MultiWriter { target_size: u64, initial_level: u8, ) -> crate::Result { - let current_table_id = table_id_generator.next(); + let current_table_id = next_table_id(&table_id_generator); let path = base_path.join(current_table_id.to_string()); let writer = Writer::new(path, current_table_id, initial_level)?; @@ -91,28 +86,9 @@ impl MultiWriter { bloom_policy: BloomConstructionPolicy::default(), current_key: None, - has_items: false, - - linked_blobs: HashMap::default(), }) } - pub fn register_blob(&mut self, indirection: BlobIndirection) { - self.linked_blobs - .entry(indirection.vhandle.blob_file_id) - .and_modify(|entry| { - entry.bytes += u64::from(indirection.size); - entry.on_disk_bytes += u64::from(indirection.vhandle.on_disk_size); - entry.len += 1; - }) - .or_insert_with(|| LinkedFile { - blob_file_id: indirection.vhandle.blob_file_id, - bytes: u64::from(indirection.size), - on_disk_bytes: u64::from(indirection.vhandle.on_disk_size), - len: 1, - }); - } - #[must_use] pub fn use_partitioned_index(mut self) -> Self { self.use_partitioned_index = true; @@ -184,7 +160,7 @@ impl MultiWriter { fn rotate(&mut self) -> crate::Result<()> { log::debug!("Rotating table writer"); - let new_table_id = self.table_id_generator.next(); + let new_table_id = next_table_id(&self.table_id_generator); let path = self.base_path.join(new_table_id.to_string()); let mut new_writer = Writer::new(path, new_table_id, self.initial_level)? @@ -203,17 +179,7 @@ impl MultiWriter { new_writer = new_writer.use_partitioned_filter(); } - let mut old_writer = std::mem::replace(&mut self.writer, new_writer); - - for linked in self.linked_blobs.values() { - old_writer.link_blob_file( - linked.blob_file_id, - linked.len, - linked.bytes, - linked.on_disk_bytes, - ); - } - self.linked_blobs.clear(); + let old_writer = std::mem::replace(&mut self.writer, new_writer); if let Some((table_id, checksum)) = old_writer.finish()? { self.results.push((table_id, checksum)); @@ -235,7 +201,6 @@ impl MultiWriter { } self.writer.write(item)?; - self.has_items = true; Ok(()) } @@ -246,32 +211,22 @@ impl MultiWriter { } self.writer.write_distinct(item)?; - self.has_items = true; Ok(()) } pub(crate) fn is_empty(&self) -> bool { - !self.has_items + self.results.is_empty() && self.writer.meta.first_key.is_none() } /// Finishes the last table, making sure all data is written durably /// /// Returns the metadata of created tables - pub fn finish(mut self) -> crate::Result> { - for linked in self.linked_blobs.values() { - self.writer.link_blob_file( - linked.blob_file_id, - linked.len, - linked.bytes, - linked.on_disk_bytes, - ); - } - + pub fn finish(mut self) -> crate::Result<(PathBuf, Vec<(TableId, Checksum)>)> { if let Some((table_id, checksum)) = self.writer.finish()? { self.results.push((table_id, checksum)); } - Ok(self.results) + Ok((self.base_path, self.results)) } } diff --git a/crates/lsm-tree/src/table/regions.rs b/crates/lsm-tree/src/table/regions.rs index 5644d2f2b..1ee27de94 100644 --- a/crates/lsm-tree/src/table/regions.rs +++ b/crates/lsm-tree/src/table/regions.rs @@ -31,10 +31,6 @@ fn toc_entry_to_handle(entry: &TocEntry) -> BlockHandle { /// |--------------| /// | filter | <- may not exist /// |--------------| -/// | ... | -/// |--------------| -/// | linked blobs | <- may not exist -/// |--------------| /// | meta | /// |--------------| /// | toc | @@ -47,7 +43,6 @@ pub struct ParsedRegions { pub index: Option, pub filter_tli: Option, pub filter: Option, - pub linked_blob_files: Option, pub metadata: BlockHandle, } @@ -64,7 +59,6 @@ impl ParsedRegions { })?, index: toc.section(b"index").map(toc_entry_to_handle), filter: toc.section(b"filter").map(toc_entry_to_handle), - linked_blob_files: toc.section(b"linked_blob_files").map(toc_entry_to_handle), metadata: toc .section(b"meta") .map(toc_entry_to_handle) diff --git a/crates/lsm-tree/src/table/tests.rs b/crates/lsm-tree/src/table/tests.rs index 622564936..37e37bea5 100644 --- a/crates/lsm-tree/src/table/tests.rs +++ b/crates/lsm-tree/src/table/tests.rs @@ -42,9 +42,6 @@ fn test_with_table( let (_, checksum) = writer.finish()?.unwrap(); { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -54,8 +51,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), false, false, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -72,9 +67,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -84,8 +76,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), true, false, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -102,9 +92,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -114,8 +101,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), false, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -132,9 +117,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -144,8 +126,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), true, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -162,9 +142,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -174,8 +151,6 @@ fn test_with_table( None, true, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -210,9 +185,6 @@ fn test_with_table( let (_, checksum) = writer.finish()?.unwrap(); { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -222,8 +194,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), false, false, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -239,9 +209,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -251,8 +218,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), true, false, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -268,9 +233,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -280,8 +242,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), false, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -298,9 +258,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file.clone(), checksum, @@ -310,8 +267,6 @@ fn test_with_table( Some(Arc::new(DescriptorTable::new(10))), true, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -328,9 +283,6 @@ fn test_with_table( } { - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - let table = Table::recover( file, checksum, @@ -340,8 +292,6 @@ fn test_with_table( None, true, true, - #[cfg(feature = "metrics")] - metrics, )?; assert_eq!(0, table.id()); @@ -850,7 +800,6 @@ fn table_zero_bpk() -> crate::Result<()> { clippy::indexing_slicing, clippy::cast_possible_truncation )] -#[cfg(not(feature = "metrics"))] fn table_read_fuzz_1() -> crate::Result<()> { use crate::Slice; use crate::ValueType::{Tombstone, Value}; @@ -1299,8 +1248,6 @@ fn table_partitioned_index() -> crate::Result<()> { Some(Arc::new(crate::DescriptorTable::new(10))), true, true, - #[cfg(feature = "metrics")] - Default::default(), ) .unwrap(); @@ -1408,8 +1355,6 @@ fn table_global_seqno() -> crate::Result<()> { Some(Arc::new(crate::DescriptorTable::new(10))), true, true, - #[cfg(feature = "metrics")] - Default::default(), )?; // global seqno is 7, so a1 is = 8 -> can not be read by snapshot=8 @@ -1459,8 +1404,6 @@ fn table_return_global_seqno() -> crate::Result<()> { Some(Arc::new(crate::DescriptorTable::new(10))), true, true, - #[cfg(feature = "metrics")] - Default::default(), )?; assert_eq!( diff --git a/crates/lsm-tree/src/table/util.rs b/crates/lsm-tree/src/table/util.rs index 95dd8f6a7..3758a70c8 100644 --- a/crates/lsm-tree/src/table/util.rs +++ b/crates/lsm-tree/src/table/util.rs @@ -9,9 +9,6 @@ use crate::{ }; use std::{path::Path, sync::Arc}; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - #[must_use] pub fn aggregate_run_key_range(tables: &[Table]) -> KeyRange { #[expect(clippy::expect_used, reason = "runs are never empty by definition")] @@ -37,41 +34,18 @@ pub fn load_block( handle: &BlockHandle, block_type: BlockType, compression: CompressionType, - #[cfg(feature = "metrics")] metrics: &Metrics, ) -> crate::Result { - #[cfg(feature = "metrics")] - use std::sync::atomic::Ordering::Relaxed; - log::trace!("load {block_type:?} block {handle:?}"); if let Some(block) = cache.get_block(table_id, handle.offset()) { - #[cfg(feature = "metrics")] - match block_type { - BlockType::Filter => { - metrics.filter_block_load_cached.fetch_add(1, Relaxed); - } - BlockType::Index => { - metrics.index_block_load_cached.fetch_add(1, Relaxed); - } - BlockType::Data | BlockType::Meta => { - metrics.data_block_load_cached.fetch_add(1, Relaxed); - } - } - return Ok(block); } let (fd, fd_cache_miss) = if let Some(cached_fd) = file_accessor.access_for_table(&table_id) { - #[cfg(feature = "metrics")] - metrics.table_file_opened_cached.fetch_add(1, Relaxed); - (cached_fd, false) } else { let fd = std::fs::File::open(path)?; - #[cfg(feature = "metrics")] - metrics.table_file_opened_uncached.fetch_add(1, Relaxed); - (Arc::new(fd), true) }; @@ -84,31 +58,6 @@ pub fn load_block( ))); } - #[cfg(feature = "metrics")] - match block_type { - BlockType::Filter => { - metrics.filter_block_load_io.fetch_add(1, Relaxed); - - metrics - .filter_block_io_requested - .fetch_add(handle.size().into(), Relaxed); - } - BlockType::Index => { - metrics.index_block_load_io.fetch_add(1, Relaxed); - - metrics - .index_block_io_requested - .fetch_add(handle.size().into(), Relaxed); - } - BlockType::Data | BlockType::Meta => { - metrics.data_block_load_io.fetch_add(1, Relaxed); - - metrics - .data_block_io_requested - .fetch_add(handle.size().into(), Relaxed); - } - } - // Cache FD if fd_cache_miss { file_accessor.insert_for_table(table_id, fd); diff --git a/crates/lsm-tree/src/table/writer/mod.rs b/crates/lsm-tree/src/table/writer/mod.rs index f7636c5aa..8c8804c27 100644 --- a/crates/lsm-tree/src/table/writer/mod.rs +++ b/crates/lsm-tree/src/table/writer/mod.rs @@ -23,19 +23,10 @@ use crate::{ }, }, time::unix_timestamp, - vlog::BlobFileId, }; use index::BlockIndexWriter; use std::{fs::File, io::BufWriter, path::PathBuf}; -#[derive(Copy, Clone, PartialEq, Eq, Debug, std::hash::Hash)] -pub struct LinkedFile { - pub blob_file_id: BlobFileId, - pub bytes: u64, - pub on_disk_bytes: u64, - pub len: usize, -} - #[derive(Default)] struct FixedLen { len: Option, @@ -115,8 +106,6 @@ pub struct Writer { /// Tracks the previously written item to detect weak tombstone/value pairs previous_item: Option<(UserKey, ValueType)>, - linked_blob_files: Vec, - initial_level: u8, } @@ -166,26 +155,9 @@ impl Writer { bloom_policy: BloomConstructionPolicy::default(), previous_item: None, - - linked_blob_files: Vec::new(), }) } - pub fn link_blob_file( - &mut self, - blob_file_id: BlobFileId, - len: usize, - bytes: u64, - on_disk_bytes: u64, - ) { - self.linked_blob_files.push(LinkedFile { - blob_file_id, - bytes, - on_disk_bytes, - len, - }); - } - #[must_use] pub fn use_partitioned_filter(mut self) -> Self { self.filter_writer = Box::new(filter::PartitionedFilterWriter::new(self.bloom_policy)) @@ -477,28 +449,8 @@ impl Writer { log::trace!("Finishing filter writer"); let filter_block_count = self.filter_writer.finish(&mut self.file_writer)?; - if !self.linked_blob_files.is_empty() { - use byteorder::{LE, WriteBytesExt}; - - self.file_writer.start("linked_blob_files")?; - - #[expect( - clippy::cast_possible_truncation, - reason = "there are never 4 billion blob files linked to a single table" - )] - self.file_writer - .write_u32::(self.linked_blob_files.len() as u32)?; - - for file in self.linked_blob_files { - self.file_writer.write_u64::(file.blob_file_id)?; - self.file_writer.write_u64::(file.len as u64)?; - self.file_writer.write_u64::(file.bytes)?; - self.file_writer.write_u64::(file.on_disk_bytes)?; - } - } - self.file_writer.start("table_version")?; - self.file_writer.write_all(&[0x4])?; + self.file_writer.write_all(&[0x5])?; // Write metadata self.file_writer.start("meta")?; @@ -567,7 +519,7 @@ impl Writer { meta("seqno#max", &self.meta.highest_seqno.to_le_bytes()), meta("seqno#min", &self.meta.lowest_seqno.to_le_bytes()), meta("table_id", &self.table_id.to_le_bytes()), - meta("table_version", &[4u8]), + meta("table_version", &[5u8]), meta( "tombstone_count", &(self.meta.tombstone_count as u64).to_le_bytes(), diff --git a/crates/lsm-tree/src/time.rs b/crates/lsm-tree/src/time.rs index 4cb0f3845..ee1686029 100644 --- a/crates/lsm-tree/src/time.rs +++ b/crates/lsm-tree/src/time.rs @@ -4,32 +4,9 @@ /// Gets the unix timestamp as a duration pub fn unix_timestamp() -> std::time::Duration { - #[cfg(test)] - #[allow(clippy::significant_drop_in_scrutinee, clippy::expect_used)] - { - if let Some(cell) = NOW_OVERRIDE.get() { - if let Some(override_val) = *cell.lock().expect("lock is poisoned") { - return override_val; - } - } - } - let now = std::time::SystemTime::now(); #[expect(clippy::expect_used, reason = "trivial")] now.duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("time went backwards") } - -#[cfg(test)] -use std::sync::{Mutex, OnceLock}; - -#[cfg(test)] -static NOW_OVERRIDE: OnceLock>> = OnceLock::new(); - -#[cfg(test)] -#[allow(clippy::expect_used)] -pub fn set_unix_timestamp_for_test(value: Option) { - let cell = NOW_OVERRIDE.get_or_init(|| Mutex::new(None)); - *cell.lock().expect("lock is poisoned") = value; -} diff --git a/crates/lsm-tree/src/tree/ingest.rs b/crates/lsm-tree/src/tree/ingest.rs index 5dc5a9f37..ff066d2af 100644 --- a/crates/lsm-tree/src/tree/ingest.rs +++ b/crates/lsm-tree/src/tree/ingest.rs @@ -3,11 +3,7 @@ // (found in the LICENSE-* files in the repository) use super::Tree; -use crate::{ - BlobIndirection, SeqNo, UserKey, UserValue, config::FilterPolicyEntry, - table::multi_writer::MultiWriter, -}; -use std::path::PathBuf; +use crate::{UserKey, UserValue, config::FilterPolicyEntry, table::multi_writer::MultiWriter}; pub const INITIAL_CANONICAL_LEVEL: usize = 1; @@ -18,10 +14,9 @@ pub const INITIAL_CANONICAL_LEVEL: usize = 1; /// Ingested data bypasses memtables and is written directly into new tables, /// using the same table writer configuration that is used for flush and compaction. pub struct Ingestion<'a> { - folder: PathBuf, tree: &'a Tree, pub(crate) writer: MultiWriter, - seqno: SeqNo, + #[cfg(debug_assertions)] last_key: Option, } @@ -46,53 +41,49 @@ impl<'a> Ingestion<'a> { .get(INITIAL_CANONICAL_LEVEL); // TODO: maybe create a PrepareMultiWriter that can be used by flush, ingest and compaction worker - let mut writer = MultiWriter::new( - folder.clone(), - tree.table_id_counter.clone(), - 64 * 1_024 * 1_024, - 6, - )? - .use_bloom_policy({ - if tree.config.expect_point_read_hits { - crate::config::BloomConstructionPolicy::BitsPerKey(0.0) - } else if let FilterPolicyEntry::Bloom(p) = - tree.config.filter_policy.get(INITIAL_CANONICAL_LEVEL) - { - p - } else { - crate::config::BloomConstructionPolicy::BitsPerKey(0.0) - } - }) - .use_data_block_size( - tree.config - .data_block_size_policy - .get(INITIAL_CANONICAL_LEVEL), - ) - .use_data_block_hash_ratio( - tree.config - .data_block_hash_ratio_policy - .get(INITIAL_CANONICAL_LEVEL), - ) - .use_data_block_compression( - tree.config - .data_block_compression_policy - .get(INITIAL_CANONICAL_LEVEL), - ) - .use_index_block_compression( - tree.config - .index_block_compression_policy - .get(INITIAL_CANONICAL_LEVEL), - ) - .use_data_block_restart_interval( - tree.config - .data_block_restart_interval_policy - .get(INITIAL_CANONICAL_LEVEL), - ) - .use_index_block_restart_interval( - tree.config - .index_block_restart_interval_policy - .get(INITIAL_CANONICAL_LEVEL), - ); + let mut writer = + MultiWriter::new(folder, tree.table_id_counter.clone(), 64 * 1_024 * 1_024, 6)? + .use_bloom_policy({ + if tree.config.expect_point_read_hits { + crate::config::BloomConstructionPolicy::BitsPerKey(0.0) + } else if let FilterPolicyEntry::Bloom(p) = + tree.config.filter_policy.get(INITIAL_CANONICAL_LEVEL) + { + p + } else { + crate::config::BloomConstructionPolicy::BitsPerKey(0.0) + } + }) + .use_data_block_size( + tree.config + .data_block_size_policy + .get(INITIAL_CANONICAL_LEVEL), + ) + .use_data_block_hash_ratio( + tree.config + .data_block_hash_ratio_policy + .get(INITIAL_CANONICAL_LEVEL), + ) + .use_data_block_compression( + tree.config + .data_block_compression_policy + .get(INITIAL_CANONICAL_LEVEL), + ) + .use_index_block_compression( + tree.config + .index_block_compression_policy + .get(INITIAL_CANONICAL_LEVEL), + ) + .use_data_block_restart_interval( + tree.config + .data_block_restart_interval_policy + .get(INITIAL_CANONICAL_LEVEL), + ) + .use_index_block_restart_interval( + tree.config + .index_block_restart_interval_policy + .get(INITIAL_CANONICAL_LEVEL), + ); if index_partitioning { writer = writer.use_partitioned_index(); @@ -102,97 +93,45 @@ impl<'a> Ingestion<'a> { } Ok(Self { - folder, tree, writer, - seqno: 0, + #[cfg(debug_assertions)] last_key: None, }) } + #[cfg(debug_assertions)] + fn validate_key(&mut self, key: &UserKey) { + if let Some(previous) = &self.last_key { + debug_assert!( + key > previous, + "next key in ingestion must be greater than last key" + ); + } + self.last_key = Some(key.clone()); + } + /// Writes a key-value pair. /// /// # Errors /// /// Will return `Err` if an IO error occurs. - pub(crate) fn write_indirection( + pub fn write, V: Into>( &mut self, - key: UserKey, - indirection: BlobIndirection, + key: K, + value: V, ) -> crate::Result<()> { - use crate::coding::Encode; + let key = key.into(); + let value = value.into(); - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - - let cloned_key = key.clone(); - self.writer - .write_distinct(crate::InternalValue::from_components( - key, - indirection.encode_into_vec(), - self.seqno, - crate::ValueType::Indirection, - ))?; - - self.writer.register_blob(indirection); - - // Remember the last user key to validate the next call's ordering - self.last_key = Some(cloned_key); - - Ok(()) - } - - /// Writes a key-value pair. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write(&mut self, key: UserKey, value: UserValue) -> crate::Result<()> { - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - - self.writer - .write_distinct(crate::InternalValue::from_components( - key.clone(), - value, - self.seqno, - crate::ValueType::Value, - ))?; - - // Remember the last user key to validate the next call's ordering - self.last_key = Some(key); - - Ok(()) - } - - /// Writes a key that the caller has already proven is greater than every - /// preceding key. Debug builds retain the ordering assertion. - #[doc(hidden)] - pub fn write_prevalidated(&mut self, key: UserKey, value: UserValue) -> crate::Result<()> { #[cfg(debug_assertions)] - { - if let Some(prev) = &self.last_key { - debug_assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - self.last_key = Some(key.clone()); - } + self.validate_key(&key); self.writer .write_distinct(crate::InternalValue::from_components( key, value, - self.seqno, + 0, crate::ValueType::Value, )) } @@ -202,26 +141,19 @@ impl<'a> Ingestion<'a> { /// # Errors /// /// Will return `Err` if an IO error occurs. - pub fn write_tombstone(&mut self, key: UserKey) -> crate::Result<()> { - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } + pub fn write_tombstone>(&mut self, key: K) -> crate::Result<()> { + let key = key.into(); + + #[cfg(debug_assertions)] + self.validate_key(&key); self.writer .write_distinct(crate::InternalValue::from_components( - key.clone(), + key, crate::UserValue::empty(), - self.seqno, + 0, crate::ValueType::Tombstone, - ))?; - - // Remember the last user key to validate the next call's ordering - self.last_key = Some(key); - - Ok(()) + )) } /// Writes a weak tombstone for a key. @@ -229,48 +161,17 @@ impl<'a> Ingestion<'a> { /// # Errors /// /// Will return `Err` if an IO error occurs. - pub fn write_weak_tombstone(&mut self, key: UserKey) -> crate::Result<()> { - if let Some(prev) = &self.last_key { - assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } + pub fn write_weak_tombstone>(&mut self, key: K) -> crate::Result<()> { + let key = key.into(); - self.writer - .write_distinct(crate::InternalValue::from_components( - key.clone(), - crate::UserValue::empty(), - self.seqno, - crate::ValueType::WeakTombstone, - ))?; - - // Remember the last user key to validate the next call's ordering - self.last_key = Some(key); - - Ok(()) - } - - /// Writes a weak tombstone whose key the caller has already proven is - /// greater than every preceding key. Debug builds retain the assertion. - #[doc(hidden)] - pub fn write_prevalidated_weak_tombstone(&mut self, key: UserKey) -> crate::Result<()> { #[cfg(debug_assertions)] - { - if let Some(prev) = &self.last_key { - debug_assert!( - key > *prev, - "next key in ingestion must be greater than last key" - ); - } - self.last_key = Some(key.clone()); - } + self.validate_key(&key); self.writer .write_distinct(crate::InternalValue::from_components( key, crate::UserValue::empty(), - self.seqno, + 0, crate::ValueType::WeakTombstone, )) } @@ -282,6 +183,43 @@ impl<'a> Ingestion<'a> { /// Will return `Err` if an IO error occurs. #[allow(clippy::significant_drop_tightening)] pub fn finish(self) -> crate::Result<()> { + self.finish_inner(true) + } + + /// Finishes ingestion without checking or flushing memtables. + /// + /// The caller must ensure that no memtable writes can occur before or + /// during completion. + /// + /// # Errors + /// + /// Will return `Err` if an IO error occurs. + #[allow(clippy::significant_drop_tightening)] + pub fn finish_exclusive(self) -> crate::Result<()> { + #[cfg(debug_assertions)] + { + #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] + let super_version = self + .tree + .version_history + .read() + .expect("lock is poisoned") + .latest_version(); + + debug_assert!( + super_version.active_memtable.is_empty() + && super_version + .sealed_memtables + .iter() + .all(|memtable| memtable.is_empty()), + "exclusive ingestion requires empty memtables", + ); + } + + self.finish_inner(false) + } + + fn finish_inner(self, flush_memtables: bool) -> crate::Result<()> { use crate::{AbstractTree, Table}; if self.writer.is_empty() { @@ -289,7 +227,9 @@ impl<'a> Ingestion<'a> { return Ok(()); } - // CRITICAL SECTION: Atomic flush + seqno allocation + registration + // General path critical section: atomic flush + seqno allocation + + // registration. The exclusive path skips this because its caller + // guarantees that memtable writes cannot occur. // // We must ensure no concurrent writes interfere between flushing the // active memtable and registering the ingested tables. The sequence is: @@ -306,16 +246,18 @@ impl<'a> Ingestion<'a> { // The seqno would be disconnected from the flush, violating MVCC. // // By holding the flush lock throughout, we guarantee atomicity. - let flush_lock = self.tree.get_flush_lock(); + let flush_lock = flush_memtables.then(|| self.tree.get_flush_lock()); - // Flush any pending memtable writes to ensure ingestion sees a - // consistent snapshot and lookup order remains correct. - // We call rotate + flush directly because we already hold the lock. - self.tree.rotate_memtable(); - self.tree.flush(&flush_lock, 0)?; + if let Some(flush_lock) = flush_lock.as_ref() { + // Flush any pending memtable writes to ensure ingestion sees a + // consistent snapshot and lookup order remains correct. + // We call rotate + flush directly because we already hold the lock. + self.tree.rotate_memtable(); + self.tree.flush(flush_lock, 0)?; + } // Finalize the ingestion writer, writing all buffered data to disk. - let results = self.writer.finish()?; + let (folder, results) = self.writer.finish()?; log::info!("Finished ingestion writer"); @@ -344,7 +286,7 @@ impl<'a> Ingestion<'a> { .into_iter() .map(|(table_id, checksum)| -> crate::Result
{ Table::recover( - self.folder.join(table_id.to_string()), + folder.join(table_id.to_string()), checksum, global_seqno, self.tree.id, @@ -352,8 +294,6 @@ impl<'a> Ingestion<'a> { self.tree.config.descriptor_table.clone(), false, false, - #[cfg(feature = "metrics")] - self.tree.metrics.clone(), ) }) .collect::>>()?; @@ -369,7 +309,7 @@ impl<'a> Ingestion<'a> { &self.tree.config.path, |current| { let mut copy = current.clone(); - copy.version = copy.version.with_new_l0_run(&created_tables, None, None); + copy.version = copy.version.with_new_l0_run(&created_tables); Ok(copy) }, global_seqno, @@ -385,3 +325,14 @@ impl<'a> Ingestion<'a> { Ok(()) } } + +impl Tree { + /// Starts a bulk ingestion into this tree. + /// + /// # Errors + /// + /// Will return `Err` if an IO error occurs. + pub fn ingestion(&self) -> crate::Result> { + Ingestion::new(self) + } +} diff --git a/crates/lsm-tree/src/tree/inner.rs b/crates/lsm-tree/src/tree/inner.rs index 85c8f9d0e..9acf8b3c3 100644 --- a/crates/lsm-tree/src/tree/inner.rs +++ b/crates/lsm-tree/src/tree/inner.rs @@ -7,17 +7,15 @@ use crate::{ compaction::state::CompactionState, config::Config, stop_signal::StopSignal, + table::next_table_id, version::{SuperVersions, Version, persist_version}, }; use std::sync::{Arc, Mutex, RwLock, atomic::AtomicU64}; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - /// Unique tree ID /// /// Tree IDs are monotonically increasing integers. -pub type TreeId = u64; +pub type TreeId = u32; /// Unique memtable ID /// @@ -27,7 +25,10 @@ pub type MemtableId = u64; /// Hands out a unique (monotonically increasing) tree ID. pub fn get_next_tree_id() -> TreeId { static TREE_ID_COUNTER: AtomicU64 = AtomicU64::new(0); - TREE_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + TREE_ID_COUNTER + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + .try_into() + .expect("ran out of tree IDs") } pub struct TreeInner { @@ -42,10 +43,6 @@ pub struct TreeInner { #[doc(hidden)] pub table_id_counter: SequenceNumberCounter, - // This is not really used in the normal tree, but we need it in the blob tree - /// Hands out a unique (monotonically increasing) blob file ID - pub(crate) blob_file_id_counter: SequenceNumberCounter, - pub(crate) version_history: Arc>, pub(crate) compaction_state: Arc>, @@ -65,43 +62,28 @@ pub struct TreeInner { /// Serializes flush operations. pub(crate) flush_lock: Mutex<()>, - - #[doc(hidden)] - #[cfg(feature = "metrics")] - pub metrics: Arc, } impl TreeInner { pub(crate) fn create_new(config: Config) -> crate::Result { - let version = Version::new( - 0, - if config.kv_separation_opts.is_some() { - crate::TreeType::Blob - } else { - crate::TreeType::Standard - }, - ); + let version = Version::new(0); persist_version(&config.path, &version)?; Ok(Self { id: get_next_tree_id(), memtable_id_counter: SequenceNumberCounter::new(1), table_id_counter: SequenceNumberCounter::default(), - blob_file_id_counter: SequenceNumberCounter::default(), config: Arc::new(config), version_history: Arc::new(RwLock::new(SuperVersions::new(version))), stop_signal: StopSignal::default(), major_compaction_lock: RwLock::default(), flush_lock: Mutex::default(), compaction_state: Arc::new(Mutex::new(CompactionState::default())), - - #[cfg(feature = "metrics")] - metrics: Metrics::default().into(), }) } pub fn get_next_table_id(&self) -> TableId { - self.table_id_counter.next() + next_table_id(&self.table_id_counter) } } diff --git a/crates/lsm-tree/src/tree/mod.rs b/crates/lsm-tree/src/tree/mod.rs index c517131ee..217c77074 100644 --- a/crates/lsm-tree/src/tree/mod.rs +++ b/crates/lsm-tree/src/tree/mod.rs @@ -9,7 +9,7 @@ pub mod sealed; use crate::{ AbstractTree, Checksum, KvPair, SeqNo, SequenceNumberCounter, TableId, UserKey, UserValue, ValueType, - compaction::{CompactionStrategy, drop_range::OwnedBounds, state::CompactionState}, + compaction::{CompactionStrategy, state::CompactionState}, config::Config, file::CURRENT_VERSION_FILE, format_version::FormatVersion, @@ -20,19 +20,18 @@ use crate::{ slice::Slice, table::Table, value::InternalValue, - version::{SuperVersion, SuperVersions, Version, recovery::recover}, - vlog::BlobFile, + version::{ + SuperVersion, SuperVersions, Version, + recovery::{get_current_version, recover}, + }, }; use inner::{TreeId, TreeInner}; use std::{ - ops::{Bound, RangeBounds}, + ops::RangeBounds, path::Path, sync::{Arc, Mutex, RwLock}, }; -#[cfg(feature = "metrics")] -use crate::metrics::Metrics; - /// Iterator value guard pub struct Guard(crate::Result<(UserKey, UserValue)>); @@ -100,17 +99,17 @@ impl AbstractTree for Tree { } fn next_table_id(&self) -> TableId { - self.0.table_id_counter.get() + self.0 + .table_id_counter + .get() + .try_into() + .expect("ran out of table IDs") } fn id(&self) -> TreeId { self.id } - fn blob_file_count(&self) -> usize { - 0 - } - fn print_trace(&self, key: &[u8]) -> crate::Result<()> { #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] let super_version = self @@ -179,11 +178,6 @@ impl AbstractTree for Tree { self.flush_lock.lock().expect("lock is poisoned") } - #[cfg(feature = "metrics")] - fn metrics(&self) -> &Arc { - &self.0.metrics - } - fn version_free_list_len(&self) -> usize { #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] self.version_history @@ -198,10 +192,7 @@ impl AbstractTree for Tree { seqno: SeqNo, index: Option<(Arc, SeqNo)>, ) -> Box + Send + 'static> { - Box::new( - self.create_prefix(&prefix, seqno, index) - .map(|kv| IterGuardImpl::Standard(Guard(kv))), - ) + Box::new(self.create_prefix(&prefix, seqno, index).map(Guard)) } fn range, R: RangeBounds>( @@ -210,10 +201,7 @@ impl AbstractTree for Tree { seqno: SeqNo, index: Option<(Arc, SeqNo)>, ) -> Box + Send + 'static> { - Box::new( - self.create_range(&range, seqno, index) - .map(|kv| IterGuardImpl::Standard(Guard(kv))), - ) + Box::new(self.create_range(&range, seqno, index).map(Guard)) } /// Returns the number of tombstones in the tree. @@ -240,27 +228,6 @@ impl AbstractTree for Tree { .sum() } - fn drop_range, R: RangeBounds>(&self, range: R) -> crate::Result<()> { - let (bounds, is_empty) = Self::range_bounds_to_owned_bounds(&range); - - if is_empty { - return Ok(()); - } - - let strategy = Arc::new(crate::compaction::drop_range::Strategy::new(bounds)); - - // IMPORTANT: Write lock so we can be the only compaction going on - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - let _lock = self - .0 - .major_compaction_lock - .write() - .expect("lock is poisoned"); - - log::info!("Starting drop_range compaction"); - self.inner_compact(strategy, 0) - } - fn clear(&self) -> crate::Result<()> { let config = self.tree_config(); let mut versions = self.get_version_history_lock(); @@ -271,7 +238,7 @@ impl AbstractTree for Tree { let mut copy = v.clone(); copy.active_memtable = Arc::new(Memtable::new(self.memtable_id_counter.next())); copy.sealed_memtables = Arc::default(); - copy.version = Version::new(v.version.id() + 1, self.tree_type()); + copy.version = Version::new(v.version.id() + 1); Ok(copy) }, &config.seqno, @@ -342,7 +309,7 @@ impl AbstractTree for Tree { fn flush_to_tables( &self, stream: impl Iterator>, - ) -> crate::Result, Option>)>> { + ) -> crate::Result>> { use crate::{file::TABLES_FOLDER, table::multi_writer::MultiWriter}; use std::time::Instant; @@ -368,27 +335,23 @@ impl AbstractTree for Tree { folder.display(), ); - let mut table_writer = MultiWriter::new( - folder.clone(), - self.table_id_counter.clone(), - 64 * 1_024 * 1_024, - 0, - )? - .use_data_block_restart_interval(data_block_restart_interval) - .use_index_block_restart_interval(index_block_restart_interval) - .use_data_block_compression(data_block_compression) - .use_index_block_compression(index_block_compression) - .use_data_block_size(data_block_size) - .use_data_block_hash_ratio(data_block_hash_ratio) - .use_bloom_policy({ - use crate::config::FilterPolicyEntry::{Bloom, None}; - use crate::table::filter::BloomConstructionPolicy; + let mut table_writer = + MultiWriter::new(folder, self.table_id_counter.clone(), 64 * 1_024 * 1_024, 0)? + .use_data_block_restart_interval(data_block_restart_interval) + .use_index_block_restart_interval(index_block_restart_interval) + .use_data_block_compression(data_block_compression) + .use_index_block_compression(index_block_compression) + .use_data_block_size(data_block_size) + .use_data_block_hash_ratio(data_block_hash_ratio) + .use_bloom_policy({ + use crate::config::FilterPolicyEntry::{Bloom, None}; + use crate::table::filter::BloomConstructionPolicy; - match self.config.filter_policy.get(0) { - Bloom(policy) => policy, - None => BloomConstructionPolicy::BitsPerKey(0.0), - } - }); + match self.config.filter_policy.get(0) { + Bloom(policy) => policy, + None => BloomConstructionPolicy::BitsPerKey(0.0), + } + }); if index_partitioning { table_writer = table_writer.use_partitioned_index(); @@ -401,7 +364,7 @@ impl AbstractTree for Tree { table_writer.write(item?)?; } - let result = table_writer.finish()?; + let (folder, result) = table_writer.finish()?; log::debug!("Flushed memtable(s) in {:?}", start.elapsed()); @@ -421,29 +384,21 @@ impl AbstractTree for Tree { self.config.descriptor_table.clone(), pin_filter, pin_index, - #[cfg(feature = "metrics")] - self.metrics.clone(), ) }) .collect::>>()?; - Ok(Some((tables, None))) + Ok(Some(tables)) } #[expect(clippy::significant_drop_tightening)] fn register_tables( &self, tables: &[Table], - blob_files: Option<&[BlobFile]>, - frag_map: Option, sealed_memtables_to_delete: &[crate::tree::inner::MemtableId], gc_watermark: SeqNo, ) -> crate::Result<()> { - log::trace!( - "Registering {} tables, {} blob files", - tables.len(), - blob_files.map(<[BlobFile]>::len).unwrap_or_default(), - ); + log::trace!("Registering {} tables", tables.len()); #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] let mut _compaction_state = self.compaction_state.lock().expect("lock is poisoned"); @@ -467,11 +422,7 @@ impl AbstractTree for Tree { |current| { let mut copy = current.clone(); - copy.version = copy.version.with_new_l0_run( - tables, - blob_files, - frag_map.filter(|x| !x.is_empty()), - ); + copy.version = copy.version.with_new_l0_run(tables); for &table_id in sealed_memtables_to_delete { log::trace!("releasing sealed memtable #{table_id}"); @@ -789,51 +740,6 @@ impl Tree { None } - pub(crate) fn get_version_for_snapshot(&self, seqno: SeqNo) -> SuperVersion { - #[expect(clippy::expect_used, reason = "lock is expected to not be poisoned")] - self.version_history - .read() - .expect("lock is poisoned") - .get_version_for_snapshot(seqno) - } - - /// Normalizes a user-provided range into owned `Bound` values. - /// - /// Returns a tuple containing: - /// - the `OwnedBounds` that mirror the original bounds semantics (including - /// inclusive/exclusive markers and unbounded endpoints), and - /// - a `bool` flag indicating whether the normalized range is logically - /// empty (e.g., when the lower bound is greater than the upper bound). - /// - /// Callers can use the flag to detect empty ranges and skip further work - /// while still having access to the normalized bounds for non-empty cases. - fn range_bounds_to_owned_bounds, R: RangeBounds>( - range: &R, - ) -> (OwnedBounds, bool) { - use Bound::{Excluded, Included, Unbounded}; - - let start = match range.start_bound() { - Included(key) => Included(Slice::from(key.as_ref())), - Excluded(key) => Excluded(Slice::from(key.as_ref())), - Unbounded => Unbounded, - }; - - let end = match range.end_bound() { - Included(key) => Included(Slice::from(key.as_ref())), - Excluded(key) => Excluded(Slice::from(key.as_ref())), - Unbounded => Unbounded, - }; - - let is_empty = - if let (Included(lo) | Excluded(lo), Included(hi) | Excluded(hi)) = (&start, &end) { - lo.as_ref() > hi.as_ref() - } else { - false - }; - - (OwnedBounds { start, end }, is_empty) - } - /// Opens an LSM-tree in the given directory. /// /// Will recover previous state if the folder was previously @@ -954,7 +860,7 @@ impl Tree { .insert(value) } - /// Recovers previous state, by loading the level manifest, tables and blob files. + /// Recovers previous state by loading the level manifest and tables. /// /// # Errors /// @@ -967,63 +873,33 @@ impl Tree { let tree_id = get_next_tree_id(); - #[cfg(feature = "metrics")] - let metrics = Arc::new(Metrics::default()); - - let version = Self::recover_levels( - &config.path, - tree_id, - &config, - #[cfg(feature = "metrics")] - &metrics, - )?; - { - let manifest_path = config.path.join(format!("v{}", version.id())); + let current_version = get_current_version(&config.path)?; + let manifest_path = config.path.join(format!("v{current_version}")); let reader = sfa::Reader::new(&manifest_path)?; let manifest = Manifest::decode_from(&manifest_path, &reader)?; - if manifest.version != FormatVersion::V4 { - return Err(crate::Error::InvalidVersion(manifest.version.into())); - } - - let requested_tree_type = match config.kv_separation_opts { - Some(_) => crate::TreeType::Blob, - None => crate::TreeType::Standard, - }; - - if version.tree_type() != requested_tree_type { - log::error!( - "Tried to open a {requested_tree_type:?}Tree, but the existing tree is of type {:?}Tree. This indicates a misconfiguration or corruption.", - version.tree_type(), - ); - return Err(crate::Error::Unrecoverable); - } - // IMPORTANT: Restore persisted config config.level_count = manifest.level_count; } + let version = Self::recover_levels(&config.path, tree_id, &config)?; + let highest_table_id = version .iter_tables() .map(Table::id) .max() .unwrap_or_default(); - let inner = TreeInner { id: tree_id, memtable_id_counter: SequenceNumberCounter::new(1), - table_id_counter: SequenceNumberCounter::new(highest_table_id + 1), - blob_file_id_counter: SequenceNumberCounter::default(), + table_id_counter: SequenceNumberCounter::new(u64::from(highest_table_id) + 1), version_history: Arc::new(RwLock::new(SuperVersions::new(version))), stop_signal: StopSignal::default(), config: Arc::new(config), major_compaction_lock: RwLock::default(), flush_lock: Mutex::default(), compaction_state: Arc::new(Mutex::new(CompactionState::default())), - - #[cfg(feature = "metrics")] - metrics, }; Ok(Self(Arc::new(inner))) @@ -1055,7 +931,6 @@ impl Tree { tree_path: P, tree_id: TreeId, config: &Config, - #[cfg(feature = "metrics")] metrics: &Arc, ) -> crate::Result { use crate::{TableId, file::TABLES_FOLDER, file::fsync_directory}; @@ -1152,8 +1027,6 @@ impl Tree { config.descriptor_table.clone(), pin_filter, pin_index, - #[cfg(feature = "metrics")] - metrics.clone(), )?; tables.push(table); @@ -1176,14 +1049,7 @@ impl Tree { log::debug!("Successfully recovered {} tables", tables.len()); - let (blob_files, orphaned_blob_files) = crate::vlog::recover_blob_files( - &tree_path.join(crate::file::BLOBS_FOLDER), - &recovery.blob_file_ids, - tree_id, - config.descriptor_table.as_ref(), - )?; - - let version = Version::from_recovery(recovery, &tables, &blob_files)?; + let version = Version::from_recovery(recovery, &tables)?; // NOTE: Cleanup old versions // But only after we definitely recovered the latest version @@ -1194,11 +1060,6 @@ impl Tree { std::fs::remove_file(&table_path)?; } - for blob_file_path in orphaned_blob_files { - log::debug!("Deleting orphaned blob file {}", blob_file_path.display()); - std::fs::remove_file(&blob_file_path)?; - } - Ok(version) } diff --git a/crates/lsm-tree/src/util.rs b/crates/lsm-tree/src/util.rs index 444426286..f6931df5d 100644 --- a/crates/lsm-tree/src/util.rs +++ b/crates/lsm-tree/src/util.rs @@ -2,8 +2,8 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::range::prefix_upper_range; use crate::UserKey; +use crate::range::prefix_upper_range; use std::ops::RangeBounds; pub use crate::range::prefix_to_range; diff --git a/crates/lsm-tree/src/value.rs b/crates/lsm-tree/src/value.rs index e872160e4..c9b228eb6 100644 --- a/crates/lsm-tree/src/value.rs +++ b/crates/lsm-tree/src/value.rs @@ -2,7 +2,7 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::{key::InternalKey, Slice, ValueType}; +use crate::{Slice, ValueType, key::InternalKey}; /// User defined key pub type UserKey = Slice; diff --git a/crates/lsm-tree/src/value_type.rs b/crates/lsm-tree/src/value_type.rs index fa4774999..114264e1b 100644 --- a/crates/lsm-tree/src/value_type.rs +++ b/crates/lsm-tree/src/value_type.rs @@ -14,11 +14,6 @@ pub enum ValueType { /// "Weak" deletion (a.k.a. `SingleDelete` in `RocksDB`) WeakTombstone, - - /// Value pointer - /// - /// Points to a blob in a blob file. - Indirection = 4, } impl ValueType { @@ -27,10 +22,6 @@ impl ValueType { pub fn is_tombstone(self) -> bool { self == Self::Tombstone || self == Self::WeakTombstone } - - pub(crate) fn is_indirection(self) -> bool { - self == Self::Indirection - } } impl TryFrom for ValueType { @@ -41,7 +32,6 @@ impl TryFrom for ValueType { 0 => Ok(Self::Value), 1 => Ok(Self::Tombstone), 2 => Ok(Self::WeakTombstone), - 4 => Ok(Self::Indirection), _ => Err(()), } } @@ -53,7 +43,6 @@ impl From for u8 { ValueType::Value => 0, ValueType::Tombstone => 1, ValueType::WeakTombstone => 2, - ValueType::Indirection => 4, } } } diff --git a/crates/lsm-tree/src/version/blob_file_list.rs b/crates/lsm-tree/src/version/blob_file_list.rs deleted file mode 100644 index 1c595126a..000000000 --- a/crates/lsm-tree/src/version/blob_file_list.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{ - blob_tree::FragmentationMap, - vlog::{BlobFile, BlobFileId}, - HashMap, -}; - -// TODO: MSRV: [1.91] Replace with BTreeMap + extract_if -#[derive(Clone, Default)] -pub struct BlobFileList(HashMap); - -impl BlobFileList { - pub fn new(blob_files: HashMap) -> Self { - Self(blob_files) - } - - /// On-disk size - pub fn on_disk_size(&self) -> u64 { - self.iter().map(|bf| bf.0.meta.total_compressed_bytes).sum() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn extend>(&mut self, iter: I) { - self.0.extend(iter); - } - - pub fn contains_key(&self, key: BlobFileId) -> bool { - self.0.contains_key(&key) - } - - pub fn prune_dead(&mut self, gc_stats: &FragmentationMap) -> Vec { - self.0 - .extract_if(|_, blob_file| blob_file.is_dead(gc_stats)) - .map(|(_, v)| v) - .collect() - } - - pub fn insert(&mut self, key: BlobFileId, value: BlobFile) { - self.0.insert(key, value); - } - - pub fn remove(&mut self, key: BlobFileId) -> Option { - self.0.remove(&key) - } - - /// Visits all blob files in no particular order. - pub fn iter(&self) -> impl Iterator { - self.0.values() - } - - pub fn list_ids(&self) -> impl Iterator { - self.0.keys() - } - - pub fn get(&self, key: BlobFileId) -> Option<&BlobFile> { - self.0.get(&key) - } -} diff --git a/crates/lsm-tree/src/version/mod.rs b/crates/lsm-tree/src/version/mod.rs index 2126ff1c5..2611942d1 100644 --- a/crates/lsm-tree/src/version/mod.rs +++ b/crates/lsm-tree/src/version/mod.rs @@ -2,28 +2,20 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -mod blob_file_list; mod optimize; mod persist; pub mod recovery; pub mod run; mod super_version; -pub use blob_file_list::BlobFileList; pub use persist::persist_version; pub use run::Run; pub use super_version::{SuperVersion, SuperVersions}; -use crate::blob_tree::{FragmentationEntry, FragmentationMap}; use crate::checksum::ChecksumType; -use crate::coding::Encode; use crate::compaction::state::hidden_set::HiddenSet; use crate::version::recovery::Recovery; -use crate::TreeType; -use crate::{ - vlog::{BlobFile, BlobFileId}, - HashSet, KeyRange, Table, TableId, -}; +use crate::{HashSet, KeyRange, Table, TableId}; use optimize::optimize_runs; use run::Ranged; use std::{ops::Deref, sync::Arc}; @@ -142,22 +134,8 @@ pub struct VersionInner { /// The version's ID id: VersionId, - tree_type: TreeType, - /// The individual LSM-tree levels which consist of runs of tables levels: Vec, - - // NOTE: We purposefully use Arc<_> to avoid deep cloning the blob files again and again - // - // Changing the value log tends to happen way less often than other modifications to the - // LSM-tree - // - /// Blob files for large values (value log) - #[doc(hidden)] - pub blob_files: Arc, - - /// Blob file fragmentation - gc_stats: Arc, } /// A version is an immutable, point-in-time view of a tree's structure @@ -178,20 +156,11 @@ impl std::ops::Deref for Version { // TODO: impl using generics so we can easily unit test Version transformation functions impl Version { - /// Returns the initial tree type. - pub fn tree_type(&self) -> TreeType { - self.tree_type - } - /// Returns the version ID. pub fn id(&self) -> VersionId { self.id } - pub fn gc_stats(&self) -> &FragmentationMap { - &self.gc_stats - } - pub fn l0(&self) -> &Level { #[expect(clippy::expect_used)] self.levels.first().expect("L0 should exist") @@ -208,25 +177,15 @@ impl Version { } /// Creates a new empty version. - pub fn new(id: VersionId, tree_type: TreeType) -> Self { + pub fn new(id: VersionId) -> Self { let levels = (0..DEFAULT_LEVEL_COUNT).map(|_| Level::empty()).collect(); Self { - inner: Arc::new(VersionInner { - id, - tree_type, - levels, - blob_files: Arc::default(), - gc_stats: Arc::default(), - }), + inner: Arc::new(VersionInner { id, levels }), } } - pub(crate) fn from_recovery( - recovery: Recovery, - tables: &[Table], - blob_files: &[BlobFile], - ) -> crate::Result { + pub(crate) fn from_recovery(recovery: Recovery, tables: &[Table]) -> crate::Result { let version_levels = recovery .table_ids .iter() @@ -259,31 +218,13 @@ impl Version { }) .collect::>>()?; - Ok(Self::from_levels( - recovery.curr_version_id, - recovery.tree_type, - version_levels, - BlobFileList::new(blob_files.iter().cloned().map(|bf| (bf.id(), bf)).collect()), - recovery.gc_stats, - )) + Ok(Self::from_levels(recovery.curr_version_id, version_levels)) } /// Creates a new pre-populated version. - pub fn from_levels( - id: VersionId, - tree_type: TreeType, - levels: Vec, - blob_files: BlobFileList, - gc_stats: FragmentationMap, - ) -> Self { + pub fn from_levels(id: VersionId, levels: Vec) -> Self { Self { - inner: Arc::new(VersionInner { - id, - tree_type, - levels, - blob_files: Arc::new(blob_files), - gc_stats: Arc::new(gc_stats), - }), + inner: Arc::new(VersionInner { id, levels }), } } @@ -302,10 +243,6 @@ impl Version { self.iter_levels().map(|x| x.table_count()).sum() } - pub fn blob_file_count(&self) -> usize { - self.blob_files.len() - } - /// Returns an iterator over all tables. pub fn iter_tables(&self) -> impl Iterator { self.levels @@ -324,12 +261,7 @@ impl Version { } /// Creates a new version with the additional run added to the "top" of L0. - pub fn with_new_l0_run( - &self, - run: &[Table], - blob_files: Option<&[BlobFile]>, - diff: Option, - ) -> Self { + pub fn with_new_l0_run(&self, run: &[Table]) -> Self { let id = self.id + 1; let mut levels = vec![]; @@ -366,49 +298,19 @@ impl Version { // L1+ levels.extend(self.levels.iter().skip(1).cloned()); - // Value log - let value_log = if let Some(blob_files) = blob_files { - let mut copy = self.blob_files.deref().clone(); - copy.extend(blob_files.iter().cloned().map(|bf| (bf.id(), bf))); - copy.into() - } else { - self.blob_files.clone() - }; - - let gc_stats = if let Some(diff) = diff { - let mut copy = self.gc_stats.deref().clone(); - diff.merge_into(&mut copy); - copy.prune(&value_log); - Arc::new(copy) - } else { - self.gc_stats.clone() - }; - Self { - inner: Arc::new(VersionInner { - id, - tree_type: self.tree_type, - levels, - blob_files: value_log, - gc_stats, - }), + inner: Arc::new(VersionInner { id, levels }), } } /// Returns a new version with a list of tables removed. /// /// The table files are not immediately deleted, this is handled by the version system's free list. - pub fn with_dropped( - &self, - ids: &[TableId], - dropped_blob_files: &mut Vec, - ) -> crate::Result { + pub fn with_dropped(&self, ids: &[TableId]) -> crate::Result { let id = self.id + 1; let mut levels = vec![]; - let mut dropped_tables: Vec
= vec![]; - for level in &self.levels { let runs = level .runs @@ -417,12 +319,7 @@ impl Version { // TODO: don't clone Arc inner if we don't need to modify let mut run: Run<_> = run.deref().clone(); - let removed_tables = run - .inner_mut() - .extract_if(.., |x| ids.contains(&x.metadata.id)); - - dropped_tables.extend(removed_tables); - + run.retain(|x| !ids.contains(&x.metadata.id)); run }) .filter(|x| !x.is_empty()) @@ -433,61 +330,12 @@ impl Version { levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect())); } - let gc_stats = if dropped_tables.is_empty() { - self.gc_stats.clone() - } else { - let mut copy = self.gc_stats.deref().clone(); - - for table in &dropped_tables { - let linked_blob_files = table.list_blob_file_references()?.unwrap_or_default(); - - for blob_file in linked_blob_files { - copy.entry(blob_file.blob_file_id) - .and_modify(|counter| { - counter.bytes += blob_file.bytes; - counter.len += blob_file.len; - }) - .or_insert_with(|| { - FragmentationEntry::new( - blob_file.len, - blob_file.bytes, - blob_file.on_disk_bytes, - ) - }); - } - } - - Arc::new(copy) - }; - - let value_log = if dropped_tables.is_empty() { - self.blob_files.clone() - } else { - let mut copy = self.blob_files.deref().clone(); - dropped_blob_files.extend(copy.prune_dead(&gc_stats)); - Arc::new(copy) - }; - Ok(Self { - inner: Arc::new(VersionInner { - id, - tree_type: self.tree_type, - levels, - blob_files: value_log, - gc_stats, - }), + inner: Arc::new(VersionInner { id, levels }), }) } - pub fn with_merge( - &self, - old_ids: &[TableId], - new_tables: &[Table], - dest_level: usize, - diff: Option, - new_blob_files: Vec, - blob_files_to_drop: &HashSet, - ) -> Self { + pub fn with_merge(&self, old_ids: &[TableId], new_tables: &[Table], dest_level: usize) -> Self { let id = self.id + 1; let mut levels = vec![]; @@ -516,47 +364,8 @@ impl Version { levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect())); } - let has_diff = diff.is_some(); - - let value_log = if has_diff || !new_blob_files.is_empty() || !blob_files_to_drop.is_empty() - { - let mut copy = self.blob_files.deref().clone(); - - for blob_file in new_blob_files { - copy.insert(blob_file.id(), blob_file); - } - - for &id in blob_files_to_drop { - copy.remove(id); - } - - Arc::new(copy) - } else { - self.blob_files.clone() - }; - - let gc_stats = if has_diff || !blob_files_to_drop.is_empty() { - let mut copy = self.gc_stats.deref().clone(); - - if let Some(diff) = diff { - diff.merge_into(&mut copy); - } - - copy.prune(&value_log); - - Arc::new(copy) - } else { - self.gc_stats.clone() - }; - Self { - inner: Arc::new(VersionInner { - id, - tree_type: self.tree_type, - levels, - blob_files: value_log, - gc_stats, - }), + inner: Arc::new(VersionInner { id, levels }), } } @@ -598,13 +407,7 @@ impl Version { } Self { - inner: Arc::new(VersionInner { - id, - tree_type: self.tree_type, - levels, - blob_files: self.blob_files.clone(), - gc_stats: self.gc_stats.clone(), - }), + inner: Arc::new(VersionInner { id, levels }), } } } @@ -623,13 +426,13 @@ impl Version { // writer.start("format_version")?; - writer.write_u8(FormatVersion::V4.into())?; + writer.write_u8(FormatVersion::V5.into())?; writer.start("crate_version")?; writer.write_all(env!("CARGO_PKG_VERSION").as_bytes())?; writer.start("tree_type")?; - writer.write_u8(self.tree_type.into())?; + writer.write_u8(0)?; writer.start("level_count")?; #[expect( @@ -672,7 +475,7 @@ impl Version { // Tables for table in run.iter() { - writer.write_u64::(table.id())?; + writer.write_u32::(table.id())?; writer.write_u8(0)?; // Checksum type, 0 = XXH3 writer.write_u128::(table.checksum().into_u128())?; writer.write_u64::(table.global_seqno())?; @@ -680,25 +483,6 @@ impl Version { } } - writer.start("blob_files")?; - - // Blob file count - #[expect( - clippy::cast_possible_truncation, - reason = "there are always less than 4 billion blob files" - )] - writer.write_u32::(self.blob_files.len() as u32)?; - - for file in self.blob_files.iter() { - writer.write_u64::(file.id())?; - writer.write_u8(0)?; // Checksum type, 0 = XXH3 - writer.write_u128::(file.0.checksum.into_u128())?; - } - - writer.start("blob_gc_stats")?; - - self.gc_stats.encode_into(writer)?; - Ok(()) } } diff --git a/crates/lsm-tree/src/version/persist.rs b/crates/lsm-tree/src/version/persist.rs index 299460eeb..60eda893c 100644 --- a/crates/lsm-tree/src/version/persist.rs +++ b/crates/lsm-tree/src/version/persist.rs @@ -1,6 +1,6 @@ use crate::{ checksum::ChecksummedWriter, - file::{fsync_directory, retry_transient_io, rewrite_atomic, CURRENT_VERSION_FILE}, + file::{CURRENT_VERSION_FILE, fsync_directory, retry_transient_io, rewrite_atomic}, version::Version, }; use byteorder::{LittleEndian, WriteBytesExt}; @@ -56,13 +56,12 @@ pub fn persist_version(folder: &Path, version: &Version) -> crate::Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::TreeType; use test_log::test; #[test] fn version_persist_replaces_orphaned_file() -> crate::Result<()> { let dir = tempfile::tempdir()?; - let version = Version::new(0, TreeType::Standard); + let version = Version::new(0); // Simulates the leftover of a persist that failed midway std::fs::write(dir.path().join("v0"), b"partial")?; diff --git a/crates/lsm-tree/src/version/recovery.rs b/crates/lsm-tree/src/version/recovery.rs index 515d5c9be..f414a1094 100644 --- a/crates/lsm-tree/src/version/recovery.rs +++ b/crates/lsm-tree/src/version/recovery.rs @@ -2,10 +2,7 @@ // This source code is licensed under both the Apache 2.0 and MIT License // (found in the LICENSE-* files in the repository) -use crate::{ - coding::Decode, file::CURRENT_VERSION_FILE, version::VersionId, vlog::BlobFileId, Checksum, - SeqNo, TableId, TreeType, -}; +use crate::{Checksum, SeqNo, TableId, file::CURRENT_VERSION_FILE, version::VersionId}; use byteorder::{LittleEndian, ReadBytesExt}; use std::path::Path; @@ -24,11 +21,8 @@ pub struct RecoveredTable { } pub struct Recovery { - pub tree_type: TreeType, pub curr_version_id: VersionId, pub table_ids: Vec>>, - pub blob_file_ids: Vec<(BlobFileId, Checksum)>, - pub gc_stats: crate::blob_tree::FragmentationMap, } pub fn recover(folder: &Path) -> crate::Result { @@ -68,7 +62,7 @@ pub fn recover(folder: &Path) -> crate::Result { let table_count = reader.read_u32::()?; for _ in 0..table_count { - let id = reader.read_u64::()?; + let id = reader.read_u32::()?; let checksum_type = reader.read_u8()?; if checksum_type != 0 { @@ -94,67 +88,26 @@ pub fn recover(folder: &Path) -> crate::Result { } } - let blob_file_ids = { - let mut reader = toc - .section(b"blob_files") + let tree_type = { + let byte = toc + .section(b"tree_type") .ok_or(crate::Error::Unrecoverable) .inspect_err(|_| { - log::error!("blob_files section not found in version #{curr_version_id} - maybe the file is corrupted?"); - })? - .buf_reader(&version_file_path)?; - - let blob_file_count = reader.read_u32::()?; - let mut blob_file_ids = Vec::with_capacity(blob_file_count as usize); - - for _ in 0..blob_file_count { - let id = reader.read_u64::()?; - - let checksum_type = reader.read_u8()?; - - if checksum_type != 0 { - return Err(crate::Error::InvalidTag(("ChecksumType", checksum_type))); - } - - let checksum = reader.read_u128::()?; - let checksum = Checksum::from_raw(checksum); - - blob_file_ids.push((id, checksum)); - } - - blob_file_ids.sort_by_key(|(id, _)| *id); - blob_file_ids - }; - - debug_assert!(blob_file_ids.is_sorted_by_key(|(id, _)| id)); - - let gc_stats = { - let mut reader = toc - .section(b"blob_gc_stats") - .ok_or(crate::Error::Unrecoverable) - .inspect_err(|_| { - log::error!("blob_gc_stats section not found in version #{curr_version_id} - maybe the file is corrupted?"); - })? - .buf_reader(&version_file_path)?; - - crate::blob_tree::FragmentationMap::decode_from(&mut reader)? - }; - - Ok(Recovery { - tree_type: { - let byte = toc.section(b"tree_type").ok_or(crate::Error::Unrecoverable) - .inspect_err(|_|{ log::error!("tree_type section not found in version #{curr_version_id} - maybe the file is corrupted?"); })? - .buf_reader( - &version_file_path - )? + .buf_reader(&version_file_path)? .read_u8()?; - TreeType::try_from(byte).map_err(|()| crate::Error::InvalidHeader("TreeType"))? - }, + byte + }; + + if tree_type != 0 { + log::error!("Blob trees are not supported by this build"); + return Err(crate::Error::Unrecoverable); + } + + Ok(Recovery { curr_version_id, table_ids: levels, - blob_file_ids, - gc_stats, }) } diff --git a/crates/lsm-tree/src/version/run.rs b/crates/lsm-tree/src/version/run.rs index 686416ec9..fbf8ca651 100644 --- a/crates/lsm-tree/src/version/run.rs +++ b/crates/lsm-tree/src/version/run.rs @@ -174,11 +174,12 @@ impl Run { #[expect(clippy::unwrap_used)] mod tests { use super::*; + use crate::TableId; use test_log::test; #[derive(Clone)] struct FakeTable { - id: u64, + id: TableId, key_range: KeyRange, } @@ -188,7 +189,7 @@ mod tests { } } - fn s(id: u64, min: &str, max: &str) -> FakeTable { + fn s(id: TableId, min: &str, max: &str) -> FakeTable { FakeTable { id, key_range: KeyRange::new((min.as_bytes().into(), max.as_bytes().into())), @@ -283,15 +284,14 @@ mod tests { run.range_overlap_indexes(&(b"z" as &[u8]..=b"zzz")) ); assert_eq!(Some((3, 3)), run.range_overlap_indexes(&(b"z" as &[u8]..))); - assert!(run - .range_overlap_indexes(&(b"zzz" as &[u8]..=b"zzzzzzz")) - .is_none()); + assert!( + run.range_overlap_indexes(&(b"zzz" as &[u8]..=b"zzzzzzz")) + .is_none() + ); } #[test] fn run_range_contained() { - use crate::TableId; - let items = vec![ s(0, "a", "d"), s(1, "e", "j"), @@ -411,7 +411,7 @@ mod tests { ); assert_eq!( - &[] as &[u64], + &[] as &[TableId], &*run .get_overlapping(&KeyRange::new((b"zzz".into(), b"zzzz".into()))) .iter() diff --git a/crates/lsm-tree/src/version/super_version.rs b/crates/lsm-tree/src/version/super_version.rs index 2398a45ab..a76d219e9 100644 --- a/crates/lsm-tree/src/version/super_version.rs +++ b/crates/lsm-tree/src/version/super_version.rs @@ -3,10 +3,10 @@ // (found in the LICENSE-* files in the repository) use crate::{ + SeqNo, SequenceNumberCounter, memtable::Memtable, tree::sealed::SealedMemtables, - version::{persist_version, Version}, - SeqNo, SequenceNumberCounter, + version::{Version, persist_version}, }; use std::{collections::VecDeque, path::Path, sync::Arc}; @@ -216,19 +216,19 @@ mod tests { SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 0, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 1, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 2, }, ] @@ -249,19 +249,19 @@ mod tests { SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 0, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 1, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 2, }, ] @@ -282,25 +282,25 @@ mod tests { SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 0, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 1, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 2, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 8, }, ] @@ -321,13 +321,13 @@ mod tests { SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 0, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 8, }, ] @@ -348,13 +348,13 @@ mod tests { SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 0, }, SuperVersion { active_memtable: Arc::new(Memtable::new(0)), sealed_memtables: Arc::default(), - version: Version::new(0, crate::TreeType::Standard), + version: Version::new(0), seqno: 2, }, ] diff --git a/crates/lsm-tree/src/vlog/accessor.rs b/crates/lsm-tree/src/vlog/accessor.rs deleted file mode 100644 index f246ff7d7..000000000 --- a/crates/lsm-tree/src/vlog/accessor.rs +++ /dev/null @@ -1,56 +0,0 @@ -// 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::{ - version::BlobFileList, - vlog::{blob_file::reader::Reader, ValueHandle}, - Cache, GlobalTableId, TreeId, UserValue, -}; -use std::{fs::File, path::Path, sync::Arc}; - -pub struct Accessor<'a>(&'a BlobFileList); - -impl<'a> Accessor<'a> { - pub fn new(blob_files: &'a BlobFileList) -> Self { - Self(blob_files) - } - - pub fn get( - &self, - tree_id: TreeId, - base_path: &Path, - key: &[u8], - vhandle: &ValueHandle, - cache: &Cache, - ) -> crate::Result> { - if let Some(value) = cache.get_blob(tree_id, vhandle) { - return Ok(Some(value)); - } - - let Some(blob_file) = self.0.get(vhandle.blob_file_id) else { - return Ok(None); - }; - - let bf_id = GlobalTableId::from((tree_id, blob_file.id())); - - let (file, fd_cache_miss) = - if let Some(cached_fd) = blob_file.file_accessor().access_for_blob_file(&bf_id) { - (cached_fd, false) - } else { - let file = Arc::new(File::open( - base_path.join(vhandle.blob_file_id.to_string()), - )?); - (file, true) - }; - - let value = Reader::new(blob_file, &file).get(key, vhandle)?; - cache.insert_blob(tree_id, vhandle, value.clone()); - - if fd_cache_miss { - blob_file.file_accessor().insert_for_blob_file(bf_id, file); - } - - Ok(Some(value)) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/merge.rs b/crates/lsm-tree/src/vlog/blob_file/merge.rs deleted file mode 100644 index 9a87a420c..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/merge.rs +++ /dev/null @@ -1,203 +0,0 @@ -// 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::scanner::Scanner as BlobFileScanner; -use crate::vlog::{blob_file::scanner::ScanEntry, BlobFileId}; -use interval_heap::IntervalHeap; -use std::cmp::Reverse; - -type IteratorIndex = usize; - -#[derive(Debug)] -struct IteratorValue { - index: IteratorIndex, - scan_entry: ScanEntry, - blob_file_id: BlobFileId, -} - -impl PartialEq for IteratorValue { - fn eq(&self, other: &Self) -> bool { - self.scan_entry.key == other.scan_entry.key - } -} -impl Eq for IteratorValue {} - -impl PartialOrd for IteratorValue { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for IteratorValue { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - (&self.scan_entry.key, Reverse(&self.scan_entry.seqno)) - .cmp(&(&other.scan_entry.key, Reverse(&other.scan_entry.seqno))) - } -} - -/// Interleaves multiple blob file readers into a single, sorted stream -pub struct MergeScanner { - readers: Vec, - heap: IntervalHeap, -} - -impl MergeScanner { - /// Initializes a new merging reader - pub fn new(readers: Vec) -> Self { - let heap = IntervalHeap::with_capacity(readers.len()); - Self { readers, heap } - } - - fn advance_reader(&mut self, idx: usize) -> crate::Result<()> { - #[expect(clippy::indexing_slicing, reason = "we trust the caller")] - let reader = &mut self.readers[idx]; - - if let Some(value) = reader.next() { - let scan_entry = value?; - let blob_file_id = reader.blob_file_id; - - self.heap.push(IteratorValue { - index: idx, - blob_file_id, - scan_entry, - }); - } - - Ok(()) - } - - fn push_next(&mut self) -> crate::Result<()> { - for idx in 0..self.readers.len() { - self.advance_reader(idx)?; - } - - Ok(()) - } -} - -impl Iterator for MergeScanner { - type Item = crate::Result<(ScanEntry, BlobFileId)>; - - fn next(&mut self) -> Option { - if self.heap.is_empty() { - fail_iter!(self.push_next()); - } - - if let Some(head) = self.heap.pop_min() { - fail_iter!(self.advance_reader(head.index)); - return Some(Ok((head.scan_entry, head.blob_file_id))); - } - - None - } -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::super::scanner::Scanner; - use super::*; - use crate::{vlog::blob_file::writer::Writer as BlobFileWriter, Slice}; - use tempfile::tempdir; - use test_log::test; - - #[test] - fn blob_file_merger_seqno() -> crate::Result<()> { - let dir = tempdir()?; - - let blob_file_path = dir.path().join("0"); - { - { - let mut writer = BlobFileWriter::new(&blob_file_path, 0, 0)?; - - writer.write(b"a", 1, &b"1".repeat(100))?; - writer.write(b"a", 0, &b"0".repeat(100))?; - - writer.finish()?; - } - } - - { - let mut merger = MergeScanner::new(vec![Scanner::new(&blob_file_path, 0)?]); - - assert_eq!( - (Slice::from(b"a"), Slice::from(b"1".repeat(100))), - merger - .next() - .map(|result| result.map(|(entry, _)| (entry.key, entry.value))) - .unwrap()?, - ); - assert_eq!( - (Slice::from(b"a"), Slice::from(b"0".repeat(100))), - merger - .next() - .map(|result| result.map(|(entry, _)| (entry.key, entry.value))) - .unwrap()?, - ); - - assert!(merger.next().is_none()); - } - - Ok(()) - } - - #[test] - fn blob_file_merger() -> crate::Result<()> { - let dir = tempdir()?; - - let blob_file_0_path = dir.path().join("0"); - let blob_file_1_path = dir.path().join("1"); - - { - let keys = [b"a", b"c", b"e"]; - - { - let mut writer = BlobFileWriter::new(&blob_file_0_path, 0, 0)?; - - for key in keys { - writer.write(key, 0, &key.repeat(100))?; - } - - writer.finish()?; - } - } - - { - let keys = [b"b", b"d"]; - - { - let mut writer = BlobFileWriter::new(&blob_file_1_path, 1, 0)?; - - for key in keys { - writer.write(key, 1, &key.repeat(100))?; - } - - writer.finish()?; - } - } - - { - let mut merger = MergeScanner::new(vec![ - Scanner::new(&blob_file_0_path, 0)?, - Scanner::new(&blob_file_1_path, 1)?, - ]); - - let merged_keys = [b"a", b"b", b"c", b"d", b"e"]; - - for key in merged_keys { - assert_eq!( - (Slice::from(key), Slice::from(key.repeat(100))), - merger - .next() - .map(|result| result.map(|(entry, _)| (entry.key, entry.value))) - .unwrap()?, - ); - } - - assert!(merger.next().is_none()); - } - - Ok(()) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/meta.rs b/crates/lsm-tree/src/vlog/blob_file/meta.rs deleted file mode 100644 index 821e77c37..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/meta.rs +++ /dev/null @@ -1,186 +0,0 @@ -// 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::{ - checksum::ChecksumType, - coding::{Decode, Encode}, - table::{Block, DataBlock}, - vlog::BlobFileId, - CompressionType, InternalValue, KeyRange, SeqNo, Slice, -}; -use byteorder::{LittleEndian, ReadBytesExt}; -use std::io::{Read, Write}; - -macro_rules! read_u64 { - ($block:expr, $name:expr) => {{ - let bytes = $block - .point_read($name, SeqNo::MAX) - .unwrap_or_else(|| panic!("meta property {:?} should exist", $name)); - - let mut bytes = &bytes.value[..]; - bytes.read_u64::()? - }}; -} - -macro_rules! read_u128 { - ($block:expr, $name:expr) => {{ - let bytes = $block - .point_read($name, SeqNo::MAX) - .unwrap_or_else(|| panic!("meta property {:?} should exist", $name)); - - let mut bytes = &bytes.value[..]; - bytes.read_u128::()? - }}; -} - -pub const METADATA_HEADER_MAGIC: &[u8] = b"META"; - -#[derive(Debug, PartialEq, Eq)] -pub struct Metadata { - pub id: BlobFileId, - - pub created_at: u128, - - /// Number of KV-pairs in the blob file - pub item_count: u64, - - /// compressed size in bytes (on disk) (without metadata or trailer) - pub total_compressed_bytes: u64, - - /// true size in bytes (if no compression were used) - pub total_uncompressed_bytes: u64, - - /// Key range - pub key_range: KeyRange, - - /// Compression type used for all blobs in this file - pub compression: CompressionType, -} - -impl Metadata { - pub fn encode_into(&self, writer: &mut W) -> crate::Result<()> { - fn meta(key: &str, value: &[u8]) -> InternalValue { - InternalValue::from_components(key, value, 0, crate::ValueType::Value) - } - - // Write header - writer.write_all(METADATA_HEADER_MAGIC)?; - - #[rustfmt::skip] - let meta_items = [ - meta("blob_file_version", &[0x3]), - meta("checksum_type", &[u8::from(ChecksumType::Xxh3)]), - meta("compression", &self.compression.encode_into_vec()), - meta("crate_version", env!("CARGO_PKG_VERSION").as_bytes()), - meta("created_at", &self.created_at.to_le_bytes()), - meta("file_size", &self.total_compressed_bytes.to_le_bytes()), - meta("id", &self.id.to_le_bytes()), - meta("item_count", &self.item_count.to_le_bytes()), - meta("key#max", self.key_range.max()), - meta("key#min", self.key_range.min()), - meta("uncompressed_size", &self.total_uncompressed_bytes.to_le_bytes()), - ]; - - // NOTE: Just to make sure the items are definitely sorted - #[cfg(debug_assertions)] - { - let is_sorted = meta_items.iter().is_sorted_by_key(|kv| &kv.key); - assert!(is_sorted, "meta items not sorted correctly"); - } - - // TODO: no binary index - let buf = DataBlock::encode_into_vec(&meta_items, 1, 0.0)?; - - Block::write_into( - writer, - &buf, - crate::table::block::BlockType::Meta, - CompressionType::None, - )?; - - Ok(()) - } - - pub fn from_slice(slice: &Slice) -> crate::Result { - let reader = &mut &slice[..]; - - // Check header - let mut magic = [0u8; METADATA_HEADER_MAGIC.len()]; - reader.read_exact(&mut magic)?; - - if magic != METADATA_HEADER_MAGIC { - return Err(crate::Error::InvalidHeader("BlobFileMeta")); - } - - // TODO: Block::from_slice - let block = Block::from_reader(reader, CompressionType::None)?; - let block = DataBlock::new(block); - - let id = read_u64!(block, b"id"); - let created_at = read_u128!(block, b"created_at"); - let item_count = read_u64!(block, b"item_count"); - let file_size = read_u64!(block, b"file_size"); - let total_uncompressed_bytes = read_u64!(block, b"uncompressed_size"); - - let compression = { - #[expect(clippy::expect_used, reason = "compression is expected to exist")] - let bytes = block - .point_read(b"compression", SeqNo::MAX) - .expect("compression should exist"); - - let mut bytes = &bytes.value[..]; - CompressionType::decode_from(&mut bytes)? - }; - - let key_range = KeyRange::new(( - #[expect(clippy::expect_used, reason = "key min is expected to exist")] - block - .point_read(b"key#min", SeqNo::MAX) - .expect("key min should exist") - .value, - #[expect(clippy::expect_used, reason = "key max is expected to exist")] - block - .point_read(b"key#max", SeqNo::MAX) - .expect("key max should exist") - .value, - )); - - Ok(Self { - id, - created_at, - compression, - item_count, - total_compressed_bytes: file_size, - total_uncompressed_bytes, - key_range, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - #[expect(clippy::unwrap_used)] - fn test_blob_file_meta_roundtrip() { - let meta = Metadata { - id: 0, - created_at: 1_234_567_890, - compression: CompressionType::None, - item_count: 100, - total_compressed_bytes: 1024, - total_uncompressed_bytes: 2048, - key_range: KeyRange::new((b"a".into(), b"z".into())), - }; - - let mut buf = Vec::new(); - meta.encode_into(&mut buf).unwrap(); - let buf = Slice::from(buf); - - let meta2 = Metadata::from_slice(&buf).unwrap(); - assert_eq!(meta, meta2); - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/mod.rs b/crates/lsm-tree/src/vlog/blob_file/mod.rs deleted file mode 100644 index 138e5e142..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/mod.rs +++ /dev/null @@ -1,156 +0,0 @@ -// 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 merge; -pub mod meta; -pub mod multi_writer; -pub mod reader; -pub mod scanner; -pub mod writer; - -use crate::{ - blob_tree::FragmentationMap, file_accessor::FileAccessor, vlog::BlobFileId, Checksum, - GlobalTableId, TreeId, -}; -pub use meta::Metadata; -use std::{ - path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, -}; - -/// A blob file is an immutable, sorted, contiguous file that contains large key-value pairs (blobs) -#[derive(Debug)] -pub struct Inner { - /// Blob file ID - pub id: BlobFileId, - - pub tree_id: TreeId, - - /// File path - pub path: PathBuf, - - /// Statistics - pub meta: Metadata, - - /// Whether this blob file is deleted (logically) - pub is_deleted: AtomicBool, - - pub checksum: Checksum, - - pub(crate) file_accessor: FileAccessor, -} - -impl Inner { - fn global_id(&self) -> GlobalTableId { - GlobalTableId::from((self.tree_id, self.id)) - } -} - -impl Drop for Inner { - fn drop(&mut self) { - if self.is_deleted.load(std::sync::atomic::Ordering::Acquire) { - log::trace!( - "Cleanup deleted blob file {:?} at {}", - self.id, - self.path.display(), - ); - - if let Err(e) = std::fs::remove_file(&*self.path) { - log::warn!( - "Failed to cleanup deleted blob file {:?} at {}: {e:?}", - self.id, - self.path.display(), - ); - } - - self.file_accessor - .as_descriptor_table() - .inspect(|d| d.remove_for_blob_file(&self.global_id())); - } - } -} - -/// A blob file stores large values and is part of the value log -#[derive(Clone)] -pub struct BlobFile(pub(crate) Arc); - -impl Eq for BlobFile {} - -impl PartialEq for BlobFile { - fn eq(&self, other: &Self) -> bool { - self.id().eq(&other.id()) - } -} - -impl std::hash::Hash for BlobFile { - fn hash(&self, state: &mut H) { - self.id().hash(state); - } -} - -impl BlobFile { - pub(crate) fn mark_as_deleted(&self) { - self.0 - .is_deleted - .store(true, std::sync::atomic::Ordering::Release); - } - - /// Returns the blob file ID. - #[must_use] - pub fn id(&self) -> BlobFileId { - self.0.id - } - - /// Returns the full blob file checksum. - #[must_use] - pub fn checksum(&self) -> Checksum { - self.0.checksum - } - - /// Returns the blob file path. - #[must_use] - pub fn path(&self) -> &Path { - &self.0.path - } - - /// Returns the blob file accessor. - #[must_use] - pub(crate) fn file_accessor(&self) -> &FileAccessor { - &self.0.file_accessor - } - - /// Returns the number of items in the blob file. - #[must_use] - #[expect(clippy::len_without_is_empty)] - pub fn len(&self) -> u64 { - self.0.meta.item_count - } - - /// Returns `true` if the blob file is stale (based on the given staleness threshold). - pub(crate) fn is_stale(&self, frag_map: &FragmentationMap, threshold: f32) -> bool { - frag_map.get(&self.id()).is_some_and(|x| { - #[expect( - clippy::cast_precision_loss, - reason = "ok to lose precision as this is an approximate calculation" - )] - let stale_bytes = x.bytes as f32; - #[expect( - clippy::cast_precision_loss, - reason = "ok to lose precision as this is an approximate calculation" - )] - let all_bytes = self.0.meta.total_uncompressed_bytes as f32; - let ratio = stale_bytes / all_bytes; - ratio >= threshold - }) - } - - /// Returns `true` if the blob file has no more incoming references, and can be safely removed from a Version. - pub(crate) fn is_dead(&self, frag_map: &FragmentationMap) -> bool { - frag_map.get(&self.id()).is_some_and(|x| { - let stale_bytes = x.bytes; - let all_bytes = self.0.meta.total_uncompressed_bytes; - stale_bytes == all_bytes - }) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/multi_writer.rs b/crates/lsm-tree/src/vlog/blob_file/multi_writer.rs deleted file mode 100644 index 040a7a382..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/multi_writer.rs +++ /dev/null @@ -1,242 +0,0 @@ -// 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::{ - file_accessor::FileAccessor, - vlog::{ - blob_file::{Inner as BlobFileInner, Metadata}, - ValueHandle, - }, - BlobFile, CompressionType, DescriptorTable, SeqNo, SequenceNumberCounter, TreeId, -}; -use std::{ - fs::File, - path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, -}; - -/// Blob file writer, may write multiple blob files -pub struct MultiWriter { - folder: PathBuf, - target_size: u64, - - active_writer: Writer, - - results: Vec, - - id_generator: SequenceNumberCounter, - - compression: CompressionType, - passthrough_compression: CompressionType, - - tree_id: TreeId, - descriptor_table: Option>, -} - -impl MultiWriter { - /// Initializes a new blob file writer. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - #[doc(hidden)] - pub fn new>( - id_generator: SequenceNumberCounter, - folder: P, - tree_id: TreeId, - descriptor_table: Option>, - ) -> crate::Result { - let folder = folder.as_ref(); - - let blob_file_id = id_generator.next(); - let blob_file_path = folder.join(blob_file_id.to_string()); - - Ok(Self { - id_generator, - folder: folder.into(), - target_size: 64 * 1_024 * 1_024, - - active_writer: Writer::new(blob_file_path, blob_file_id, tree_id)?, - - results: Vec::new(), - - compression: CompressionType::None, - passthrough_compression: CompressionType::None, - - tree_id, - descriptor_table, - }) - } - - /// Sets the blob file target size. - #[must_use] - pub fn use_target_size(mut self, bytes: u64) -> Self { - self.target_size = bytes; - self - } - - /// Sets the compression method in blob file writer metadata, but does not actually compress blobs. - /// - /// This is used in garbage collection to pass through already-compressed blobs, but correctly - /// set the compression type in the metadata. - pub(crate) fn use_passthrough_compression(mut self, compression: CompressionType) -> Self { - assert_eq!(self.compression, CompressionType::None); - self.passthrough_compression = compression; - self.active_writer.passthrough_compression = compression; - self - } - - /// Sets the compression method. - #[must_use] - #[doc(hidden)] - pub fn use_compression(mut self, compression: CompressionType) -> Self { - self.compression.clone_from(&compression); - self.active_writer.compression = compression; - self - } - - /// Sets up a new writer for the next blob file. - fn rotate(&mut self) -> crate::Result<()> { - log::debug!("Rotating blob file writer"); - - let new_blob_file_id = self.id_generator.next(); - let blob_file_path = self.folder.join(new_blob_file_id.to_string()); - - let new_writer = Writer::new(blob_file_path, new_blob_file_id, self.tree_id)? - .use_compression(self.compression) - .use_passthrough_compression(self.passthrough_compression); - - let old_writer = std::mem::replace(&mut self.active_writer, new_writer); - let blob_file = Self::consume_writer(old_writer, self.descriptor_table.clone())?; - self.results.extend(blob_file); - - Ok(()) - } - - fn consume_writer( - writer: Writer, - descriptor_table: Option>, - ) -> crate::Result> { - if writer.item_count > 0 { - let blob_file_id = writer.blob_file_id; - let path = writer.path.clone(); - - log::debug!( - "Created blob file #{blob_file_id:?} ({} items, {} userdata bytes)", - writer.item_count, - writer.uncompressed_bytes, - ); - - let tree_id = writer.tree_id; - - let (metadata, checksum) = writer.finish()?; - - let file = Arc::new(File::open(&path)?); - let file_accessor = descriptor_table.map_or(FileAccessor::File(file.clone()), |dt| { - FileAccessor::DescriptorTable(dt) - }); - file_accessor.insert_for_blob_file((tree_id, blob_file_id).into(), file); - - let blob_file = BlobFile(Arc::new(BlobFileInner { - checksum, - tree_id, - path, - is_deleted: AtomicBool::new(false), - id: blob_file_id, - file_accessor, - meta: Metadata { - id: blob_file_id, - created_at: crate::time::unix_timestamp().as_nanos(), - item_count: metadata.item_count, - total_compressed_bytes: metadata.total_compressed_bytes, - total_uncompressed_bytes: metadata.total_uncompressed_bytes, - key_range: metadata.key_range.clone(), - compression: metadata.compression, - }, - })); - - Ok(Some(blob_file)) - } else { - log::debug!( - "Blob file writer at {} has written no data, deleting empty blob file", - writer.path.display(), - ); - - if let Err(e) = std::fs::remove_file(&writer.path) { - log::warn!( - "Could not delete empty blob file at {}: {e:?}", - writer.path.display(), - ); - } - - Ok(None) - } - } - - /// Writes an item. - /// - /// Returns the [`ValueHandle`] of the written blob. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn write(&mut self, key: &[u8], seqno: SeqNo, value: &[u8]) -> crate::Result { - let target_size = self.target_size; - - // Write actual value into blob file - let writer = &mut self.active_writer; - - let offset = writer.offset(); - let on_disk_value_len = writer.write(key, seqno, value)?; - - let handle = ValueHandle { - blob_file_id: writer.blob_file_id(), - offset, - on_disk_size: on_disk_value_len, - }; - - // Check for blob file size target, maybe rotate to next writer - if writer.offset() >= target_size { - self.rotate()?; - } - - Ok(handle) - } - - pub(crate) fn write_raw( - &mut self, - key: &[u8], - seqno: SeqNo, - value: &[u8], - uncompressed_len: u32, - ) -> crate::Result { - let target_size = self.target_size; - - // Write actual value into blob file - let writer = &mut self.active_writer; - - let offset = writer.offset(); - let on_disk_value_len = writer.write_raw(key, seqno, value, uncompressed_len)?; - - let handle = ValueHandle { - blob_file_id: writer.blob_file_id(), - offset, - on_disk_size: on_disk_value_len, - }; - - // Check for blob file size target, maybe rotate to next writer - if writer.offset() >= target_size { - self.rotate()?; - } - - Ok(handle) - } - - pub(crate) fn finish(mut self) -> crate::Result> { - let blob_file = Self::consume_writer(self.active_writer, self.descriptor_table.clone())?; - self.results.extend(blob_file); - Ok(self.results) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/reader.rs b/crates/lsm-tree/src/vlog/blob_file/reader.rs deleted file mode 100644 index 281b8625b..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/reader.rs +++ /dev/null @@ -1,161 +0,0 @@ -// 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::{ - vlog::{ - blob_file::writer::{BLOB_HEADER_LEN, BLOB_HEADER_MAGIC}, - ValueHandle, - }, - BlobFile, Checksum, CompressionType, UserValue, -}; -use byteorder::{LittleEndian, ReadBytesExt}; -use std::{ - fs::File, - io::{Cursor, Read}, -}; - -/// Reads a single blob from a blob file -pub struct Reader<'a> { - blob_file: &'a BlobFile, - file: &'a File, -} - -impl<'a> Reader<'a> { - pub fn new(blob_file: &'a BlobFile, file: &'a File) -> Self { - Self { blob_file, file } - } - - pub fn get(&self, key: &'a [u8], vhandle: &'a ValueHandle) -> crate::Result { - debug_assert_eq!(vhandle.blob_file_id, self.blob_file.id()); - - let add_size = (BLOB_HEADER_LEN as u64) + (key.len() as u64); - - let value = crate::file::read_exact( - self.file, - vhandle.offset, - (u64::from(vhandle.on_disk_size) + add_size) as usize, - )?; - - let mut reader = Cursor::new(&value[..]); - - let mut magic = [0u8; 4]; - reader.read_exact(&mut magic)?; - - if magic != BLOB_HEADER_MAGIC { - return Err(crate::Error::InvalidHeader("Blob")); - } - - let expected_checksum = reader.read_u128::()?; - - let _seqno = reader.read_u64::()?; - let key_len = reader.read_u16::()?; - - let real_val_len = reader.read_u32::()?; - - let _on_disk_val_len = reader.read_u32::()? as usize; - - let key = crate::UserKey::from_reader(&mut reader, key_len.into())?; - - let raw_data = value.slice((add_size as usize)..); - - { - let checksum = { - let mut hasher = xxhash_rust::xxh3::Xxh3::default(); - hasher.update(&key); - hasher.update(&raw_data); - hasher.digest128() - }; - - if expected_checksum != checksum { - log::error!( - "Checksum mismatch for blob {vhandle:?}, got={checksum}, expected={expected_checksum}", - ); - - return Err(crate::Error::ChecksumMismatch { - got: Checksum::from_raw(checksum), - expected: Checksum::from_raw(expected_checksum), - }); - } - } - - #[warn(clippy::match_single_binding)] - let value = match &self.blob_file.0.meta.compression { - CompressionType::None => raw_data, - - #[cfg(feature = "lz4")] - CompressionType::Lz4 => { - #[warn(unsafe_code)] - let mut builder = unsafe { UserValue::builder_unzeroed(real_val_len as usize) }; - - lz4_flex::decompress_into(&raw_data, &mut builder) - .map_err(|_| crate::Error::Decompress(self.blob_file.0.meta.compression))?; - - builder.freeze().into() - } - }; - - debug_assert_eq!(real_val_len, { - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - { - value.len() as u32 - } - }); - - Ok(value) - } -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::SequenceNumberCounter; - use test_log::test; - - #[test] - fn blob_reader_roundtrip() -> crate::Result<()> { - let id_generator = SequenceNumberCounter::default(); - - let folder = tempfile::tempdir()?; - let mut writer = crate::vlog::BlobFileWriter::new(id_generator, folder.path(), 0, None)? - .use_target_size(u64::MAX); - - let handle = writer.write(b"a", 0, b"abcdef")?; - - let blob_file = writer.finish()?; - let blob_file = blob_file.first().unwrap(); - - let file = File::open(&blob_file.0.path)?; - let reader = Reader::new(blob_file, &file); - - assert_eq!(reader.get(b"a", &handle)?, b"abcdef"); - - Ok(()) - } - - #[test] - #[cfg(feature = "lz4")] - fn blob_reader_roundtrip_lz4() -> crate::Result<()> { - let id_generator = SequenceNumberCounter::default(); - - let folder = tempfile::tempdir()?; - let mut writer = crate::vlog::BlobFileWriter::new(id_generator, folder.path(), 0, None)? - .use_target_size(u64::MAX) - .use_compression(CompressionType::Lz4); - - let handle0 = writer.write(b"a", 0, b"abcdef")?; - let handle1 = writer.write(b"b", 0, b"ghi")?; - - let blob_file = writer.finish()?; - let blob_file = blob_file.first().unwrap(); - - let file = File::open(&blob_file.0.path)?; - let reader = Reader::new(blob_file, &file); - - assert_eq!(reader.get(b"a", &handle0)?, b"abcdef"); - assert_eq!(reader.get(b"b", &handle1)?, b"ghi"); - - Ok(()) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/scanner.rs b/crates/lsm-tree/src/vlog/blob_file/scanner.rs deleted file mode 100644 index a4deb5def..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/scanner.rs +++ /dev/null @@ -1,169 +0,0 @@ -// 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::BLOB_HEADER_MAGIC; -use crate::{ - vlog::{blob_file::meta::METADATA_HEADER_MAGIC, BlobFileId}, - Checksum, SeqNo, UserKey, UserValue, -}; -use byteorder::{LittleEndian, ReadBytesExt}; -use std::{ - fs::File, - io::{BufReader, Read, Seek}, - path::Path, -}; - -/// Reads through a blob file in order -pub struct Scanner { - pub(crate) blob_file_id: BlobFileId, // TODO: remove unused? - inner: BufReader, - is_terminated: bool, -} - -impl Scanner { - /// Initializes a new blob file reader. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - pub fn new>(path: P, blob_file_id: BlobFileId) -> crate::Result { - let file_reader = BufReader::with_capacity(32_000, File::open(path)?); - Ok(Self::with_reader(blob_file_id, file_reader)) - } - - /// Initializes a new blob file reader. - #[must_use] - pub fn with_reader(blob_file_id: BlobFileId, file_reader: BufReader) -> Self { - Self { - blob_file_id, - inner: file_reader, - is_terminated: false, - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ScanEntry { - pub key: UserKey, - pub seqno: SeqNo, - pub value: UserValue, - pub offset: u64, - pub uncompressed_len: u32, -} - -impl Iterator for Scanner { - type Item = crate::Result; - - fn next(&mut self) -> Option { - if self.is_terminated { - return None; - } - - let offset = fail_iter!(self.inner.stream_position()); - - { - let mut buf = [0; BLOB_HEADER_MAGIC.len()]; - fail_iter!(self.inner.read_exact(&mut buf)); - - if buf == METADATA_HEADER_MAGIC { - self.is_terminated = true; - return None; - } - - if buf != BLOB_HEADER_MAGIC { - return Some(Err(crate::Error::InvalidHeader("Blob"))); - } - } - - let expected_checksum = fail_iter!(self.inner.read_u128::()); - let seqno = fail_iter!(self.inner.read_u64::()); - - let key_len = fail_iter!(self.inner.read_u16::()); - - let real_val_len = fail_iter!(self.inner.read_u32::()); - - let on_disk_val_len = fail_iter!(self.inner.read_u32::()); - - let key = fail_iter!(UserKey::from_reader(&mut self.inner, key_len as usize)); - - let value = fail_iter!(UserValue::from_reader( - &mut self.inner, - on_disk_val_len as usize - )); - - { - let checksum = { - let mut hasher = xxhash_rust::xxh3::Xxh3::default(); - hasher.update(&key); - hasher.update(&value); - hasher.digest128() - }; - - if expected_checksum != checksum { - log::error!( - "Checksum mismatch for blob>{}@{offset}, got={checksum}, expected={expected_checksum}", - self.blob_file_id, - ); - - return Some(Err(crate::Error::ChecksumMismatch { - got: Checksum::from_raw(checksum), - expected: Checksum::from_raw(expected_checksum), - })); - } - } - - Some(Ok(ScanEntry { - key, - seqno, - value, - offset, - uncompressed_len: real_val_len, - })) - } -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::{vlog::blob_file::writer::Writer as BlobFileWriter, Slice}; - use tempfile::tempdir; - use test_log::test; - - #[test] - fn blob_scanner() -> crate::Result<()> { - let dir = tempdir()?; - let blob_file_path = dir.path().join("0"); - - let keys = [b"a", b"b", b"c", b"d", b"e"]; - - { - let mut writer = BlobFileWriter::new(&blob_file_path, 0, 0)?; - - for key in keys { - writer.write(key, 0, &key.repeat(100))?; - } - - writer.finish()?; - } - - { - let mut scanner = Scanner::new(&blob_file_path, 0)?; - - for key in keys { - assert_eq!( - (Slice::from(key), Slice::from(key.repeat(100))), - scanner - .next() - .map(|result| result.map(|entry| { (entry.key, entry.value) })) - .unwrap()?, - ); - } - - assert!(scanner.next().is_none()); - } - - Ok(()) - } -} diff --git a/crates/lsm-tree/src/vlog/blob_file/writer.rs b/crates/lsm-tree/src/vlog/blob_file/writer.rs deleted file mode 100644 index 77298c451..000000000 --- a/crates/lsm-tree/src/vlog/blob_file/writer.rs +++ /dev/null @@ -1,247 +0,0 @@ -// 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::meta::Metadata; -use crate::{ - checksum::ChecksummedWriter, time::unix_timestamp, vlog::BlobFileId, Checksum, CompressionType, - KeyRange, SeqNo, TreeId, UserKey, -}; -use byteorder::{LittleEndian, WriteBytesExt}; -use std::{ - fs::File, - io::{BufWriter, Write}, - path::{Path, PathBuf}, -}; - -pub const BLOB_HEADER_MAGIC: &[u8] = b"BLOB"; - -pub const BLOB_HEADER_LEN: usize = BLOB_HEADER_MAGIC.len() - + std::mem::size_of::() // Checksum - + std::mem::size_of::() // SeqNo - + std::mem::size_of::() // Key length - + std::mem::size_of::() // Real value length - + std::mem::size_of::(); // On-disk value length - -/// Blob file writer -pub struct Writer { - pub(crate) tree_id: TreeId, - pub path: PathBuf, - pub(crate) blob_file_id: BlobFileId, - - #[expect(clippy::struct_field_names)] - writer: sfa::Writer>>, - - offset: u64, - - pub(crate) item_count: u64, - pub(crate) written_blob_bytes: u64, - pub(crate) uncompressed_bytes: u64, - - pub(crate) first_key: Option, - pub(crate) last_key: Option, - - pub(crate) compression: CompressionType, - pub(crate) passthrough_compression: CompressionType, -} - -impl Writer { - /// Initializes a new blob file writer. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - #[doc(hidden)] - pub fn new>( - path: P, - blob_file_id: BlobFileId, - tree_id: TreeId, - ) -> crate::Result { - let path = path.as_ref(); - - let writer = BufWriter::new(File::create(path)?); - let writer = ChecksummedWriter::new(writer); - let mut writer = sfa::Writer::from_writer(writer); - writer.start("data")?; - - Ok(Self { - tree_id, - path: path.into(), - blob_file_id, - - writer, - - offset: 0, - item_count: 0, - written_blob_bytes: 0, - uncompressed_bytes: 0, - - first_key: None, - last_key: None, - - compression: CompressionType::None, - passthrough_compression: CompressionType::None, - }) - } - - pub fn use_compression(mut self, compressor: CompressionType) -> Self { - self.compression = compressor; - self - } - - pub(crate) fn use_passthrough_compression(mut self, compressor: CompressionType) -> Self { - self.passthrough_compression = compressor; - self - } - - /// Returns the current offset in the file. - #[must_use] - pub(crate) fn offset(&self) -> u64 { - self.offset - } - - /// Returns the blob file ID. - #[must_use] - pub(crate) fn blob_file_id(&self) -> BlobFileId { - self.blob_file_id - } - - pub(crate) fn write_raw( - &mut self, - key: &[u8], - seqno: SeqNo, - value: &[u8], - uncompressed_len: u32, - ) -> crate::Result { - assert!(!key.is_empty()); - assert!(u16::try_from(key.len()).is_ok()); - assert!(u32::try_from(value.len()).is_ok()); - - if self.first_key.is_none() { - self.first_key = Some(key.into()); - } - self.last_key = Some(key.into()); - - self.uncompressed_bytes += u64::from(uncompressed_len); - - // NOTE: - // BLOB HEADER LAYOUT - // - // [MAGIC_BYTES; 4B] - // [Checksum; 16B] - // [Seqno; 8B] - // [key len; 2B] - // [real val len; 4B] - // [on-disk val len; 4B] - // [...key; ?] - // [...val; ?] - - // Write header - self.writer.write_all(BLOB_HEADER_MAGIC)?; - - let value = match &self.compression { - CompressionType::None => std::borrow::Cow::Borrowed(value), - - #[cfg(feature = "lz4")] - CompressionType::Lz4 => std::borrow::Cow::Owned(lz4_flex::compress(value)), - }; - - let checksum = { - let mut hasher = xxhash_rust::xxh3::Xxh3::default(); - hasher.update(key); - hasher.update(&value); - hasher.digest128() - }; - - // Write checksum - self.writer.write_u128::(checksum)?; - - // Write seqno - self.writer.write_u64::(seqno)?; - - #[expect(clippy::cast_possible_truncation, reason = "keys are u16 length max")] - self.writer.write_u16::(key.len() as u16)?; - - // Write uncompressed value length - self.writer.write_u32::(uncompressed_len)?; - - // Write compressed (on-disk) value length - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - self.writer.write_u32::(value.len() as u32)?; - - self.writer.write_all(key)?; - self.writer.write_all(&value)?; - - // Update offset - self.offset += BLOB_HEADER_MAGIC.len() as u64; - self.offset += std::mem::size_of::() as u64; - self.offset += std::mem::size_of::() as u64; - - self.offset += std::mem::size_of::() as u64; - self.offset += std::mem::size_of::() as u64; - self.offset += std::mem::size_of::() as u64; - - self.offset += key.len() as u64; - self.offset += value.len() as u64; - - // Update metadata - self.written_blob_bytes += value.len() as u64; - self.item_count += 1; - - // TODO: if we store the offset before writing, we can return a vhandle here - // instead of needing to call offset() and blob_file_id() before write() - - #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")] - Ok(value.len() as u32) - } - - /// Writes an item into the file. - /// - /// Items need to be written in key order. - /// - /// # Errors - /// - /// Will return `Err` if an IO error occurs. - /// - /// # Panics - /// - /// Panics if the key length is empty or greater than 2^16, or the value length is greater than 2^32. - pub fn write(&mut self, key: &[u8], seqno: SeqNo, value: &[u8]) -> crate::Result { - #[expect(clippy::cast_possible_truncation, reason = "values are u32 max")] - self.write_raw(key, seqno, value, value.len() as u32) - } - - pub(crate) fn finish(mut self) -> crate::Result<(Metadata, Checksum)> { - self.writer.start("meta")?; - - // Write metadata - let metadata = Metadata { - id: self.blob_file_id, - created_at: unix_timestamp().as_nanos(), - item_count: self.item_count, - total_compressed_bytes: self.written_blob_bytes, - total_uncompressed_bytes: self.uncompressed_bytes, - #[expect(clippy::expect_used, reason = "should have written at least 1 item")] - key_range: KeyRange::new(( - self.first_key - .clone() - .expect("should have written at least 1 item"), - self.last_key - .clone() - .expect("should have written at least 1 item"), - )), - compression: if self.passthrough_compression == CompressionType::None { - self.compression - } else { - self.passthrough_compression - }, - }; - metadata.encode_into(&mut self.writer)?; - - let mut checksum = self.writer.into_inner()?; - checksum.inner_mut().get_mut().sync_all()?; - let checksum = checksum.checksum(); - - Ok((metadata, checksum)) - } -} diff --git a/crates/lsm-tree/src/vlog/handle.rs b/crates/lsm-tree/src/vlog/handle.rs deleted file mode 100644 index d0888147b..000000000 --- a/crates/lsm-tree/src/vlog/handle.rs +++ /dev/null @@ -1,49 +0,0 @@ -// 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::{ - coding::{Decode, Encode}, - vlog::BlobFileId, -}; -use std::{ - hash::Hash, - io::{Read, Write}, -}; -use varint_rs::{VarintReader, VarintWriter}; - -/// A value handle points into the value log -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub struct ValueHandle { - /// Blob file ID - pub blob_file_id: BlobFileId, - - /// Offset in file - pub offset: u64, - - /// On-disk size - pub on_disk_size: u32, -} - -impl Encode for ValueHandle { - fn encode_into(&self, writer: &mut W) -> Result<(), crate::Error> { - writer.write_u64_varint(self.offset)?; - writer.write_u64_varint(self.blob_file_id)?; - writer.write_u32_varint(self.on_disk_size)?; - Ok(()) - } -} - -impl Decode for ValueHandle { - fn decode_from(reader: &mut R) -> Result { - let offset = reader.read_u64_varint()?; - let blob_file_id = reader.read_u64_varint()?; - let on_disk_size = reader.read_u32_varint()?; - - Ok(Self { - blob_file_id, - offset, - on_disk_size, - }) - } -} diff --git a/crates/lsm-tree/src/vlog/mod.rs b/crates/lsm-tree/src/vlog/mod.rs deleted file mode 100644 index 5f59a0a7d..000000000 --- a/crates/lsm-tree/src/vlog/mod.rs +++ /dev/null @@ -1,150 +0,0 @@ -// 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 accessor; -pub mod blob_file; -mod handle; - -pub use { - accessor::Accessor, blob_file::merge::MergeScanner as BlobFileMergeScanner, - blob_file::multi_writer::MultiWriter as BlobFileWriter, - blob_file::scanner::Scanner as BlobFileScanner, blob_file::BlobFile, handle::ValueHandle, -}; - -use crate::{ - file_accessor::FileAccessor, - vlog::blob_file::{Inner as BlobFileInner, Metadata}, - Checksum, DescriptorTable, TreeId, -}; -use std::{ - path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, -}; - -pub fn recover_blob_files( - folder: &Path, - ids: &[(BlobFileId, Checksum)], - tree_id: TreeId, - descriptor_table: Option<&Arc>, -) -> crate::Result<(Vec, Vec)> { - if !folder.try_exists()? { - return Ok((vec![], vec![])); - } - - let cnt = ids.len(); - - let progress_mod = match cnt { - _ if cnt <= 20 => 1, - _ if cnt <= 100 => 10, - _ => 100, - }; - - log::debug!("Recovering {cnt} blob files from {:?}", folder.display()); - - let mut blob_files = Vec::with_capacity(ids.len()); - let mut orphaned_blob_files = vec![]; - - for (idx, dirent) in std::fs::read_dir(folder)?.enumerate() { - let dirent = dirent?; - let file_name = dirent.file_name(); - - // https://en.wikipedia.org/wiki/.DS_Store - if file_name == ".DS_Store" { - continue; - } - - // https://en.wikipedia.org/wiki/AppleSingle_and_AppleDouble_formats - if file_name.to_string_lossy().starts_with("._") { - continue; - } - - let blob_file_name = file_name.to_str().ok_or_else(|| { - log::error!("invalid table file name {}", file_name.display()); - crate::Error::Unrecoverable - })?; - - let blob_file_id = blob_file_name.parse::().map_err(|e| { - log::error!("invalid table file name {blob_file_name:?}: {e:?}"); - crate::Error::Unrecoverable - })?; - - let blob_file_path = dirent.path(); - assert!(!blob_file_path.is_dir()); - - if let Some(&(_, checksum)) = ids.iter().find(|(id, _)| id == &blob_file_id) { - log::trace!( - "Recovering blob file #{blob_file_id:?} from {}", - blob_file_path.display(), - ); - - let file = std::fs::File::open(&blob_file_path)?; - - let meta = { - let reader = sfa::Reader::new(&blob_file_path)?; - let toc = reader.toc(); - - let metadata_section = toc.section(b"meta") - .ok_or(crate::Error::Unrecoverable) - .inspect_err(|_| { - log::error!("meta section in blob file #{blob_file_id} is missing - maybe the file is corrupted?"); - })?; - - let metadata_slice = crate::file::read_exact( - &file, - metadata_section.pos(), - metadata_section.len() as usize, - )?; - - Metadata::from_slice(&metadata_slice)? - }; - - let file_accessor = if let Some(dt) = descriptor_table.cloned() { - FileAccessor::DescriptorTable(dt) - } else { - FileAccessor::File(Arc::new(file)) - }; - - blob_files.push(BlobFile(Arc::new(BlobFileInner { - id: blob_file_id, - path: blob_file_path, - meta, - is_deleted: AtomicBool::new(false), - checksum, - file_accessor, - tree_id, - }))); - - if idx % progress_mod == 0 { - log::debug!("Recovered {idx}/{cnt} blob files"); - } - } else { - orphaned_blob_files.push(blob_file_path.clone()); - } - } - - if blob_files.len() < ids.len() { - return Err(crate::Error::Unrecoverable); - } - - log::debug!("Successfully recovered {} blob files", blob_files.len()); - - Ok((blob_files, orphaned_blob_files)) -} - -/// The unique identifier for a value log blob file. -pub type BlobFileId = u64; - -#[cfg(test)] -mod tests { - use super::*; - use test_log::test; - - #[test] - fn vlog_recovery_missing_blob_file() { - assert!(matches!( - recover_blob_files(Path::new("."), &[(0, Checksum::from_raw(0))], 0, None), - Err(crate::Error::Unrecoverable), - )); - } -} diff --git a/crates/lsm-tree/test_fixture/v1_tree/config b/crates/lsm-tree/test_fixture/v1_tree/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree/config and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree/levels b/crates/lsm-tree/test_fixture/v1_tree/levels deleted file mode 100644 index 67a331d07..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree/levels and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree/segments/0 b/crates/lsm-tree/test_fixture/v1_tree/segments/0 deleted file mode 100644 index 8788a1f2f..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree/segments/0 and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree/version b/crates/lsm-tree/test_fixture/v1_tree/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree/version and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree_corrupt/config b/crates/lsm-tree/test_fixture/v1_tree_corrupt/config deleted file mode 100644 index 61e5bd120..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree_corrupt/config and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree_corrupt/levels b/crates/lsm-tree/test_fixture/v1_tree_corrupt/levels deleted file mode 100644 index 67a331d07..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree_corrupt/levels and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree_corrupt/segments/0 b/crates/lsm-tree/test_fixture/v1_tree_corrupt/segments/0 deleted file mode 100644 index 5faff93e7..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree_corrupt/segments/0 and /dev/null differ diff --git a/crates/lsm-tree/test_fixture/v1_tree_corrupt/version b/crates/lsm-tree/test_fixture/v1_tree_corrupt/version deleted file mode 100644 index 2073e5dd0..000000000 Binary files a/crates/lsm-tree/test_fixture/v1_tree_corrupt/version and /dev/null differ diff --git a/crates/lsm-tree/tests/blob_compression.rs b/crates/lsm-tree/tests/blob_compression.rs deleted file mode 100644 index 3298adafa..000000000 --- a/crates/lsm-tree/tests/blob_compression.rs +++ /dev/null @@ -1,99 +0,0 @@ -#[test_log::test] -#[cfg(feature = "lz4")] -fn blob_tree_compression() -> lsm_tree::Result<()> { - use lsm_tree::{ - blob_tree::FragmentationEntry, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, - }; - - let folder = tempfile::tempdir()?; - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::Lz4) - .separation_threshold(1) - .staleness_threshold(0.0000001) - .age_cutoff(1.0), - )) - .open()?; - - let big_value = b"abc".repeat(50); - - tree.insert("a", &big_value, 0); - tree.insert("b", b"smol", 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - - let value = tree.get("b", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, b"smol"); - } - - tree.remove("b", 1); - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - - assert!(!tree.contains_key("b", SeqNo::MAX)?); - } - - tree.major_compact(u64::MAX, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - map.insert( - 0, - FragmentationEntry::new(1, b"smol".len().try_into().unwrap(), 5), - ); - map - }, - &*gc_stats, - ); - } - - { - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - - assert!(!tree.contains_key("b", SeqNo::MAX)?); - } - - tree.major_compact(u64::MAX, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - { - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - - assert!(!tree.contains_key("b", SeqNo::MAX)?); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_empty_flush.rs b/crates/lsm-tree/tests/blob_empty_flush.rs deleted file mode 100644 index 56a72b053..000000000 --- a/crates/lsm-tree/tests/blob_empty_flush.rs +++ /dev/null @@ -1,30 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_flush_empty() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let medium_value = b"a".repeat(500); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("med", &medium_value, 0); - tree.flush_active_memtable(0)?; - - assert_eq!(1, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - // Blob writer should have cleaned up blob file because it was empty - let blob_file_count_on_disk = std::fs::read_dir(path.join("blobs"))?.flatten().count(); - assert_eq!(0, blob_file_count_on_disk); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_fifo_limit.rs b/crates/lsm-tree/tests/blob_fifo_limit.rs deleted file mode 100644 index 00ae4db59..000000000 --- a/crates/lsm-tree/tests/blob_fifo_limit.rs +++ /dev/null @@ -1,28 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SequenceNumberCounter}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn blob_tree_fifo_limit() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let compaction = Arc::new(lsm_tree::compaction::Fifo::new(10, None)); - - for _ in 0..100 { - tree.insert(nanoid::nanoid!(), "$", 0); - tree.flush_active_memtable(0)?; - tree.compact(compaction.clone(), 0)?; - assert!((0..10).contains(&tree.blob_file_count())); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_file_full_file_checksum.rs b/crates/lsm-tree/tests/blob_file_full_file_checksum.rs deleted file mode 100644 index d5cb8c07f..000000000 --- a/crates/lsm-tree/tests/blob_file_full_file_checksum.rs +++ /dev/null @@ -1,121 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter}; -use test_log::test; -use xxhash_rust::xxh3::xxh3_128; - -#[test] -fn blob_file_full_file_checksum() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for key in ('a'..='z').map(|c| c.to_string()) { - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes(), 0); - } - tree.flush_active_memtable(0)?; - - let version = tree.current_version(); - let blob_file = version.blob_files.iter().next().unwrap(); - - let expected_checksum = blob_file.checksum().into_u128(); - let real_checksum = xxh3_128(&std::fs::read(blob_file.path())?); - assert_eq!( - real_checksum, expected_checksum, - "full file checksum mismatch", - ); - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let version = tree.current_version(); - let blob_file = version.blob_files.iter().next().unwrap(); - - let expected_checksum = blob_file.checksum().into_u128(); - let real_checksum = xxh3_128(&std::fs::read(blob_file.path())?); - assert_eq!( - real_checksum, expected_checksum, - "full file checksum mismatch", - ); - } - - Ok(()) -} - -#[test] -fn blob_file_full_file_detect_corruption() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for key in ('a'..='z').map(|c| c.to_string()) { - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes(), 0); - } - tree.flush_active_memtable(0)?; - - let version = tree.current_version(); - let blob_file = version.blob_files.iter().next().unwrap(); - - let expected_checksum = blob_file.checksum().into_u128(); - let real_checksum = xxh3_128(&std::fs::read(blob_file.path())?); - assert_eq!( - real_checksum, expected_checksum, - "full file checksum mismatch", - ); - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let version = tree.current_version(); - let blob_file = version.blob_files.iter().next().unwrap(); - - { - use std::io::{Seek, Write}; - - let mut f = std::fs::OpenOptions::new() - .write(true) - .open(blob_file.path())?; - - f.seek(std::io::SeekFrom::Start(100))?; - f.write_all(b"!")?; - f.sync_all()?; - } - - let expected_checksum = blob_file.checksum().into_u128(); - let real_checksum = xxh3_128(&std::fs::read(blob_file.path())?); - assert_ne!( - real_checksum, expected_checksum, - "full file checksum should not match", - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_flush_gc_stats.rs b/crates/lsm-tree/tests/blob_flush_gc_stats.rs deleted file mode 100644 index c88489c79..000000000 --- a/crates/lsm-tree/tests/blob_flush_gc_stats.rs +++ /dev/null @@ -1,71 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_flush_gc_stats() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.insert("big", &new_big_value, 1); - - tree.flush_active_memtable(1_000)?; - - // NOTE: The first big_value is dropped, so it never arrives in a blob file - assert_eq!(2, tree.approximate_len()); - } - - Ok(()) -} - -#[test] -fn blob_tree_flush_gc_stats_tombstone() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.remove("big", 1); - - tree.flush_active_memtable(1_000)?; - - // NOTE: The first big_value is dropped, so it never arrives in a blob file - assert_eq!(2, tree.approximate_len()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_gc_watermark.rs b/crates/lsm-tree/tests/blob_gc_watermark.rs deleted file mode 100644 index 5cb2c0282..000000000 --- a/crates/lsm-tree/tests/blob_gc_watermark.rs +++ /dev/null @@ -1,105 +0,0 @@ -use lsm_tree::{ - config::CompressionPolicy, get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -// NOTE: This was a logic/MVCC error in v2 that could drop -// a blob file while it was maybe accessible by a snapshot read -// -// https://github.com/fjall-rs/lsm-tree/commit/79c6ead4b955051cbb4835913e21d08b8aeafba1 -#[test] -fn blob_gc_seqno_watermark() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .data_block_compression_policy(CompressionPolicy::all(lsm_tree::CompressionType::None)) - .with_kv_separation(Some( - KvSeparationOptions::default() - .staleness_threshold(0.01) - .age_cutoff(1.0) - .separation_threshold(50), - )) - .open()?; - - tree.insert("a", "neptune".repeat(50), seqno.next()); - - let snapshot_seqno = seqno.get(); - - assert_eq!( - &*tree.get("a", snapshot_seqno)?.unwrap(), - b"neptune".repeat(50), - ); - assert_eq!(&*tree.get("a", SeqNo::MAX)?.unwrap(), b"neptune".repeat(50),); - - tree.insert("a", "neptune2".repeat(50), seqno.next()); - assert_eq!( - &*tree.get("a", snapshot_seqno)?.unwrap(), - b"neptune".repeat(50), - ); - assert_eq!( - &*tree.get("a", SeqNo::MAX)?.unwrap(), - b"neptune2".repeat(50), - ); - - tree.insert("a", "neptune3".repeat(50), seqno.next()); - assert_eq!( - &*tree.get("a", snapshot_seqno)?.unwrap(), - b"neptune".repeat(50), - ); - assert_eq!( - &*tree.get("a", SeqNo::MAX)?.unwrap(), - b"neptune3".repeat(50), - ); - - tree.flush_active_memtable(0)?; - - assert_eq!( - &*tree.get("a", snapshot_seqno)?.unwrap(), - b"neptune".repeat(50), - ); - assert_eq!( - &*tree.get("a", SeqNo::MAX)?.unwrap(), - b"neptune3".repeat(50), - ); - - tree.major_compact(u64::MAX, 0)?; - tree.major_compact(u64::MAX, 0)?; - - // IMPORTANT: We cannot drop any blobs yet - // because the watermark is too low - // - // This would previously fail - - { - let gc_stats = tree.current_version().gc_stats().clone(); - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - assert_eq!( - &*tree.get("a", snapshot_seqno)?.unwrap(), - b"neptune".repeat(50), - ); - assert_eq!( - &*tree.get("a", SeqNo::MAX)?.unwrap(), - b"neptune3".repeat(50), - ); - - tree.major_compact(u64::MAX, 1_000)?; - - { - let gc_stats = tree.current_version().gc_stats().clone(); - assert!(!gc_stats.is_empty()); - } - - tree.major_compact(u64::MAX, 1_000)?; - - { - let gc_stats = tree.current_version().gc_stats().clone(); - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_ingest.rs b/crates/lsm-tree/tests/blob_ingest.rs deleted file mode 100644 index 93cef156e..000000000 --- a/crates/lsm-tree/tests/blob_ingest.rs +++ /dev/null @@ -1,61 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_ingest_gc_stats() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - let mut ingestion = tree.ingestion()?; - ingestion.write("big", &big_value)?; - ingestion.write("smol", "small value")?; - ingestion.finish()?; - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let mut ingestion = tree.ingestion()?; - ingestion.write("big", &new_big_value)?; - ingestion.finish()?; - - // Blob file has no fragmentation before compaction (in stats) - // so it is not rewritten - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_ingest_relink.rs b/crates/lsm-tree/tests/blob_ingest_relink.rs deleted file mode 100644 index 619d0f451..000000000 --- a/crates/lsm-tree/tests/blob_ingest_relink.rs +++ /dev/null @@ -1,66 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_ingest_relink() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - let mut ingestion = tree.ingestion()?; - ingestion.write("big", &big_value)?; - ingestion.write("smol", "small value")?; - ingestion.finish()?; - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: big_value.len() as u64, - on_disk_bytes: big_value.len() as u64, - len: 1, - }]), - tree.current_version() - .iter_tables() - .next() - .unwrap() - .list_blob_file_references()?, - ); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: big_value.len() as u64, - on_disk_bytes: big_value.len() as u64, - len: 1, - }]), - tree.current_version() - .iter_tables() - .next() - .unwrap() - .list_blob_file_references()?, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_major_compact_drop_dead_files.rs b/crates/lsm-tree/tests/blob_major_compact_drop_dead_files.rs deleted file mode 100644 index 5509fb3a6..000000000 --- a/crates/lsm-tree/tests/blob_major_compact_drop_dead_files.rs +++ /dev/null @@ -1,100 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_tree_major_compact_drop_dead_files() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.insert("big", &big_value, 1); - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - tree.insert("big", &big_value, 2); - tree.flush_active_memtable(0)?; - assert_eq!(3, tree.table_count()); - assert_eq!(3, tree.blob_file_count()); - - tree.insert("big", &big_value, 3); - tree.flush_active_memtable(0)?; - assert_eq!(4, tree.table_count()); - assert_eq!(4, tree.blob_file_count()); - - tree.insert("big", &new_big_value, 4); - tree.flush_active_memtable(0)?; - assert_eq!(5, tree.table_count()); - assert_eq!(5, tree.blob_file_count()); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(5, tree.blob_file_count()); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map.insert(1, FragmentationEntry::new(1, size, size)); - map.insert(2, FragmentationEntry::new(1, size, size)); - map.insert(3, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_major_compact_gc_stats.rs b/crates/lsm-tree/tests/blob_major_compact_gc_stats.rs deleted file mode 100644 index 3f5d147f7..000000000 --- a/crates/lsm-tree/tests/blob_major_compact_gc_stats.rs +++ /dev/null @@ -1,205 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_tree_major_compact_gc_stats() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.insert("big", &new_big_value, 1); - - tree.flush_active_memtable(0)?; - - // Blob file has no fragmentation before compaction (in stats) - // so it is not rewritten - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} - -#[test] -fn blob_tree_major_compact_gc_stats_2() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("big", &big_value, 1); - tree.insert("big", &big_value, 2); - tree.insert("big", &big_value, 3); - tree.insert("big", &big_value, 4); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - // Blob file has no fragmentation before compaction (in stats) - // so it is not rewritten - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0,1,2,3 are expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(4, size * 4, size * 4)); - map - }, - &*gc_stats, - ); - } - - Ok(()) -} - -#[test] -fn blob_tree_major_compact_gc_stats_tombstone() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("another_big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.remove("big", 1); - - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: 2 * big_value.len() as u64, - on_disk_bytes: 2 * big_value.len() as u64, - len: 2, - }]), - tree.current_version() - .iter_tables() - .nth(1) - .unwrap() - .list_blob_file_references()?, - ); - - // Blob file has no fragmentation before compaction (in stats) - // so it is not rewritten - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: big_value.len() as u64, - on_disk_bytes: big_value.len() as u64, - len: 1, - }]), - tree.current_version() - .iter_tables() - .next() - .unwrap() - .list_blob_file_references()?, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_major_compact_relink.rs b/crates/lsm-tree/tests/blob_major_compact_relink.rs deleted file mode 100644 index c2bdcba7e..000000000 --- a/crates/lsm-tree/tests/blob_major_compact_relink.rs +++ /dev/null @@ -1,69 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_major_compact_relink() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: big_value.len() as u64, - on_disk_bytes: big_value.len() as u64, - len: 1, - }]), - tree.current_version() - .iter_tables() - .next() - .unwrap() - .list_blob_file_references()?, - ); - - tree.flush_active_memtable(1)?; - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - assert_eq!( - Some(vec![lsm_tree::table::writer::LinkedFile { - blob_file_id: 0, - bytes: big_value.len() as u64, - on_disk_bytes: big_value.len() as u64, - len: 1, - }]), - tree.current_version() - .iter_tables() - .next() - .unwrap() - .list_blob_file_references()?, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_major_compact_relocation.rs b/crates/lsm-tree/tests/blob_major_compact_relocation.rs deleted file mode 100644 index e915fa8f6..000000000 --- a/crates/lsm-tree/tests/blob_major_compact_relocation.rs +++ /dev/null @@ -1,326 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_tree_major_compact_relocation_simple() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::None) - .age_cutoff(1.0), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("big2", &big_value, 0); - tree.insert("smol", "smol", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("big2", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.insert("big", &new_big_value, 1); - - tree.flush_active_memtable(0)?; - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - let value = tree.get("big2", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - let value = tree.get("big2", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - let value = tree.get("big2", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - } - - Ok(()) -} - -#[test] -fn blob_tree_major_compact_relocation_repeated_key() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(2_000); - let very_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::None) - .age_cutoff(1.0), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("a", &big_value, 0); - tree.insert("b", &big_value, 0); - tree.insert("c", &very_big_value, 0); - tree.insert("d", &big_value, 0); - tree.insert("e", &big_value, 0); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, very_big_value); - let value = tree.get("d", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.remove("c", 1); - - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("d", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("d", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = very_big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("d", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - } - - Ok(()) -} - -#[test] -fn blob_tree_major_compact_relocation_interleaved() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(2_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::None) - .age_cutoff(1.0), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("a", b"smol", 0); - tree.insert("b", &big_value, 0); - tree.insert("c", b"smol", 0); - tree.insert("d", &big_value, 0); - tree.insert("e", b"smol", 0); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("d", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.remove("d", 1); - - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("d", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("d", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("a", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("b", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - let value = tree.get("c", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - let value = tree.get("d", SeqNo::MAX)?; - assert!(value.is_none()); - let value = tree.get("e", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"smol"); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_major_compact_relocation_recovery.rs b/crates/lsm-tree/tests/blob_major_compact_relocation_recovery.rs deleted file mode 100644 index fd5b536f1..000000000 --- a/crates/lsm-tree/tests/blob_major_compact_relocation_recovery.rs +++ /dev/null @@ -1,130 +0,0 @@ -#[test_log::test] -fn blob_tree_major_compact_relocation_recovery_no_comp() -> lsm_tree::Result<()> { - use lsm_tree::{ - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, - }; - - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(4_096); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::None) - .separation_threshold(1) - .staleness_threshold(0.0000001) - .age_cutoff(1.0), - )) - .open()?; - - tree.insert("a", &big_value, 0); - tree.insert("b", b"smol", 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.remove("b", 1); - tree.flush_active_memtable(0)?; - - tree.major_compact(u64::MAX, 1_000)?; - tree.major_compact(u64::MAX, 1_000)?; - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - } - - Ok(()) -} - -#[test_log::test] -#[cfg(feature = "lz4")] -fn blob_tree_major_compact_relocation_recovery() -> lsm_tree::Result<()> { - use lsm_tree::{ - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, - }; - - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(4_096); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .compression(lsm_tree::CompressionType::Lz4) - .separation_threshold(1) - .staleness_threshold(0.0000001) - .age_cutoff(1.0), - )) - .open()?; - - tree.insert("a", &big_value, 0); - tree.insert("b", b"smol", 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.remove("b", 1); - tree.flush_active_memtable(0)?; - - tree.major_compact(u64::MAX, 1_000)?; - tree.major_compact(u64::MAX, 1_000)?; - assert_eq!(1, tree.blob_file_count()); - - { - let gc_stats = tree.current_version().gc_stats().clone(); - - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats); - } - - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let value = tree.get("a", SeqNo::MAX)?.unwrap(); - assert_eq!(&*value, big_value); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_nuke_gc_stats.rs b/crates/lsm-tree/tests/blob_nuke_gc_stats.rs deleted file mode 100644 index faecd5788..000000000 --- a/crates/lsm-tree/tests/blob_nuke_gc_stats.rs +++ /dev/null @@ -1,110 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_tree_nuke_gc_stats() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.drop_range::<&[u8], _>(..)?; - - // NOTE: Because the blob does not have any incoming references anymore - // it is pruned from the Version - assert_eq!(0, tree.blob_file_count()); - assert_eq!(0, tree.table_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 was dropped - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - Ok(()) -} - -#[test] -fn blob_tree_nuke_gc_stats_multi() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("big", &big_value, 1); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.drop_range::<&[u8], _>(..)?; - - // NOTE: Because the blob does not have any incoming references anymore - // it is pruned from the Version - assert_eq!(0, tree.blob_file_count()); - assert_eq!(0, tree.table_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0, big":1 were dropped - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(2, size * 2, size * 2)); - map - }, - &*gc_stats, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_pinned_fd.rs b/crates/lsm-tree/tests/blob_pinned_fd.rs deleted file mode 100644 index d793fe0cd..000000000 --- a/crates/lsm-tree/tests/blob_pinned_fd.rs +++ /dev/null @@ -1,58 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_with_pinned_fd() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .use_descriptor_table(None) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"small value"); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .use_descriptor_table(None) - .with_kv_separation(Some(Default::default())) - .open()?; - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"small value"); - - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_recover_gc_stats.rs b/crates/lsm-tree/tests/blob_recover_gc_stats.rs deleted file mode 100644 index 40b618569..000000000 --- a/crates/lsm-tree/tests/blob_recover_gc_stats.rs +++ /dev/null @@ -1,82 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, - SequenceNumberCounter, -}; -use test_log::test; - -#[test] -fn blob_tree_recover_gc_stats() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.insert("big", &new_big_value, 1); - - tree.flush_active_memtable(0)?; - assert_eq!(2, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "big":0 is still expired - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_recovery.rs b/crates/lsm-tree/tests/blob_recovery.rs deleted file mode 100644 index 391f36446..000000000 --- a/crates/lsm-tree/tests/blob_recovery.rs +++ /dev/null @@ -1,49 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_recovery() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(4_096); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - - tree.insert("big", &big_value, 0); - tree.flush_active_memtable(0)?; - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert!(!tree.is_empty(SeqNo::MAX, None)?); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_register_table_rotation.rs b/crates/lsm-tree/tests/blob_register_table_rotation.rs deleted file mode 100644 index 64d13fb97..000000000 --- a/crates/lsm-tree/tests/blob_register_table_rotation.rs +++ /dev/null @@ -1,135 +0,0 @@ -use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, KvSeparationOptions, - SequenceNumberCounter, -}; -use test_log::test; - -// Force one block per table and one blob per block -// -// Then check if item_count in a table matches the number of referenced blobs (so 1). -// -// See https://github.com/fjall-rs/lsm-tree/commit/0d2d7b2071c65f2538bb01e4512907892991dcbe -#[test] -fn blob_register_table_rotation() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .data_block_size_policy(BlockSizePolicy::all(1)) - .with_kv_separation(Some( - KvSeparationOptions::default() - .separation_threshold(0) - .age_cutoff(1.0) - .staleness_threshold(0.0), - )) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - tree.insert("d", "d", 0); - tree.insert("e", "e", 0); - - tree.flush_active_memtable(0)?; - tree.major_compact(1, 0)?; - - assert_eq!(5, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - for table in tree.current_version().iter_tables() { - assert_eq!( - 1, - table - .list_blob_file_references()? - .unwrap() - .iter() - .map(|x| x.len) - .sum::(), - ); - assert_eq!( - 1, - table - .list_blob_file_references()? - .unwrap() - .iter() - .map(|x| x.bytes) - .sum::(), - ); - } - - Ok(()) -} - -#[test] -fn blob_register_table_rotation_relocation() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .data_block_size_policy(BlockSizePolicy::all(1)) - .with_kv_separation(Some( - KvSeparationOptions::default() - .separation_threshold(0) - .age_cutoff(1.0) - .staleness_threshold(0.0), - )) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - tree.insert("d", "d", 0); - tree.insert("e", "e", 0); - tree.insert("f", "f", 0); // f is not overwritten - - tree.flush_active_memtable(0)?; - tree.major_compact(1, 0)?; - - tree.insert("a", "a", 1); - tree.insert("b", "b", 1); - tree.insert("c", "c", 1); - tree.insert("d", "d", 1); - tree.insert("e", "e", 1); - - tree.flush_active_memtable(0)?; - tree.major_compact(1, 10)?; - - assert_eq!(6, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - tree.major_compact(1, 11)?; - - assert_eq!(6, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - for table in tree.current_version().iter_tables() { - assert_eq!( - 1, - table - .list_blob_file_references()? - .unwrap() - .iter() - .map(|x| x.len) - .sum::(), - ); - assert_eq!( - 1, - table - .list_blob_file_references()? - .unwrap() - .iter() - .map(|x| x.bytes) - .sum::(), - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_sep_threshold.rs b/crates/lsm-tree/tests/blob_sep_threshold.rs deleted file mode 100644 index 81e50b109..000000000 --- a/crates/lsm-tree/tests/blob_sep_threshold.rs +++ /dev/null @@ -1,30 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_separation_threshold() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().separation_threshold(1_024), - )) - .open()?; - - tree.insert("a", "a".repeat(1_023), 0); - tree.flush_active_memtable(0)?; - assert_eq!(0, tree.blob_file_count()); - - tree.insert("b", "b".repeat(1_024), 0); - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.blob_file_count()); - - assert_eq!(2, tree.len(SeqNo::MAX, None)?); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_simple.rs b/crates/lsm-tree/tests/blob_simple.rs deleted file mode 100644 index 852cb2873..000000000 --- a/crates/lsm-tree/tests/blob_simple.rs +++ /dev/null @@ -1,77 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Guard, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_simple_flush_read() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let new_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("smol", "small value", 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - assert_eq!( - big_value.len() as u32, - tree.size_of("big", SeqNo::MAX)?.unwrap(), - ); - - tree.flush_active_memtable(0)?; - - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"small value"); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.insert("big", &new_big_value, 1); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - - tree.flush_active_memtable(0)?; - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - - let value = tree.get("big", 1)?.expect("should exist"); - assert_eq!(&*value, big_value); - - let g = tree.prefix("big", 1, None).next().expect("should exist"); - assert_eq!(big_value.len() as u32, g.size()?); - } - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let value = tree.get("smol", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"small value"); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, new_big_value); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_tree_guarded_size.rs b/crates/lsm-tree/tests/blob_tree_guarded_size.rs deleted file mode 100644 index 4a5d80244..000000000 --- a/crates/lsm-tree/tests/blob_tree_guarded_size.rs +++ /dev/null @@ -1,25 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn blob_tree_guarded_size() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a".as_bytes(), "abc", 0); - tree.insert("b".as_bytes(), "a".repeat(10_000), 0); - - assert_eq!( - 10_003u32, - tree.iter(SeqNo::MAX, None).flat_map(Guard::size).sum(), - ); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/blob_tree_reload_blob.rs b/crates/lsm-tree/tests/blob_tree_reload_blob.rs deleted file mode 100644 index 56605295f..000000000 --- a/crates/lsm-tree/tests/blob_tree_reload_blob.rs +++ /dev/null @@ -1,144 +0,0 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, TreeType, -}; -use test_log::test; - -const ITEM_COUNT: usize = 10_000; - -#[test] -fn blob_tree_reload_empty() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert_eq!(tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), 0); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .map(|x| x.key()) - .rev() - .flatten() - .count(), - 0 - ); - assert_eq!(tree.tree_type(), TreeType::Blob); - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert_eq!(tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), 0); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .map(|x| x.key()) - .rev() - .flatten() - .count(), - 0 - ); - assert_eq!(tree.tree_type(), TreeType::Blob); - - tree.flush_active_memtable(0)?; - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert_eq!(tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), 0); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .map(|x| x.key()) - .rev() - .flatten() - .count(), - 0 - ); - assert_eq!(tree.tree_type(), TreeType::Blob); - } - - Ok(()) -} - -#[test] -fn blob_tree_reload() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - { - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .with_kv_separation(Some(Default::default())) - .open()?; - - for x in 0..ITEM_COUNT as u64 { - let key = x.to_be_bytes(); - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes(), seqno.next()); - } - - tree.flush_active_memtable(0)?; - - for x in 0..ITEM_COUNT as u64 { - let key: [u8; 8] = (x + ITEM_COUNT as u64).to_be_bytes(); - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes(), seqno.next()); - } - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT * 2); - assert_eq!( - tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT * 2 - ); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT * 2 - ); - - tree.flush_active_memtable(0)?; - } - - { - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT * 2); - assert_eq!( - tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT * 2 - ); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT * 2 - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/compaction_filter.rs b/crates/lsm-tree/tests/compaction_filter.rs index 0b7fdf682..ad9c468dc 100644 --- a/crates/lsm-tree/tests/compaction_filter.rs +++ b/crates/lsm-tree/tests/compaction_filter.rs @@ -1,8 +1,8 @@ +use lsm_tree::compaction::PullDown; use lsm_tree::compaction::filter::{ CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, }; -use lsm_tree::compaction::PullDown; -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, get_tmp_folder}; use std::sync::{Arc, Mutex}; use test_log::test; @@ -11,14 +11,13 @@ fn u32_s(n: u32) -> [u8; 4] { } fn u32_f(mut buf: &[u8]) -> u32 { - use byteorder::{ReadBytesExt, BE}; + use byteorder::{BE, ReadBytesExt}; buf.read_u32::().unwrap() } -fn filter_basic(blob: bool) -> lsm_tree::Result<()> { +fn filter_basic() -> lsm_tree::Result<()> { struct FilterState { - expect_blob: bool, disable: bool, do_rewrite: bool, saw_value: bool, @@ -28,7 +27,6 @@ fn filter_basic(blob: bool) -> lsm_tree::Result<()> { } let filter_state = Arc::new(Mutex::new(FilterState { - expect_blob: true, disable: true, do_rewrite: false, saw_value: false, @@ -59,7 +57,6 @@ fn filter_basic(blob: bool) -> lsm_tree::Result<()> { } else if key >= 0xff000000 { let value = u32_f(&item.value()?); assert_eq!(key & 0xff, value); - assert_eq!(item.is_indirection(), state.expect_blob); state.saw_value = true; if !state.do_rewrite { @@ -108,11 +105,6 @@ fn filter_basic(blob: bool) -> lsm_tree::Result<()> { SequenceNumberCounter::default(), SequenceNumberCounter::default(), ) - .with_kv_separation(if blob { - Some(KvSeparationOptions::default().separation_threshold(2)) - } else { - None - }) .with_compaction_filter_factory(Some(Arc::new(FilterFactory(filter_state.clone())))); config.level_count = 3; let tree = config.open()?; @@ -136,7 +128,6 @@ fn filter_basic(blob: bool) -> lsm_tree::Result<()> { let mut state = filter_state.lock().unwrap(); state.disable = false; - state.expect_blob = blob; state.expect_last_level = Some(false); drop(state); @@ -189,11 +180,6 @@ fn filter_basic(blob: bool) -> lsm_tree::Result<()> { } #[test] -fn compaction_filter_with_blob() -> lsm_tree::Result<()> { - filter_basic(true) -} - -#[test] -fn compaction_filter_no_blob() -> lsm_tree::Result<()> { - filter_basic(false) +fn compaction_filter() -> lsm_tree::Result<()> { + filter_basic() } diff --git a/crates/lsm-tree/tests/compaction_filter_gc_stats_change.rs b/crates/lsm-tree/tests/compaction_filter_gc_stats_change.rs deleted file mode 100644 index d9a146c6c..000000000 --- a/crates/lsm-tree/tests/compaction_filter_gc_stats_change.rs +++ /dev/null @@ -1,263 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, - compaction::filter::{ - CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, - }, - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn compaction_filter_gc_stats_change() -> lsm_tree::Result<()> { - struct MyFilter; - - impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key() == b"changethis" { - Ok(Verdict::ReplaceValue(b"winter2".repeat(128_000).into())) - } else { - Ok(Verdict::Keep) - } - } - } - - struct MyFilterFactory; - - impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } - } - - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let other_big_value = b"asdasd".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("changethis", &other_big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - // Because we are creating a new value, a new blob file is created - assert_eq!(2, tree.blob_file_count()); - - let value = tree.get("changethis", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"winter2".repeat(128_000)); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "changethis":0 was changed - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = other_big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(other_big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} - -#[test] -fn compaction_filter_gc_stats_change_non_blob() -> lsm_tree::Result<()> { - struct MyFilter; - - impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key() == b"changethis" { - Ok(Verdict::ReplaceValue(b"winter2".into())) - } else { - Ok(Verdict::Keep) - } - } - } - - struct MyFilterFactory; - - impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } - } - - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let small_value = b"asdasd"; - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("changethis", small_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - // Because we are creating a new value, but a small one, a new blob file is not created - assert_eq!(1, tree.blob_file_count()); - - let value = tree.get("changethis", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"winter2"); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "changethis":0 was changed - but not a blob - assert_eq!(&lsm_tree::HashMap::default(), &*gc_stats,); - assert_eq!(0, tree.stale_blob_bytes()); - } - - Ok(()) -} - -#[test] -fn compaction_filter_gc_stats_change_blob_writer_rotation() -> lsm_tree::Result<()> { - struct MyFilter; - - impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key().starts_with(b"changethis") { - Ok(Verdict::ReplaceValue(b"winter2".repeat(128_000).into())) - } else { - Ok(Verdict::Keep) - } - } - } - - struct MyFilterFactory; - - impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } - } - - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let other_big_value = b"asdasd".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .file_target_size(1) - .compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("changethis", &other_big_value, 0); - tree.insert("changethis2", &other_big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(3, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - // Because we are creating a new value, a new blob file is created - assert_eq!(5, tree.blob_file_count()); - - let value = tree.get("changethis", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"winter2".repeat(128_000)); - - let value = tree.get("changethis2", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, b"winter2".repeat(128_000)); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "changethis":0,"changethis2":0 were changed - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = other_big_value.len() as u64; - map.insert(1, FragmentationEntry::new(1, size, size)); - map.insert(2, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(2 * other_big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/compaction_filter_gc_stats_destroy.rs b/crates/lsm-tree/tests/compaction_filter_gc_stats_destroy.rs deleted file mode 100644 index f9f5b42b7..000000000 --- a/crates/lsm-tree/tests/compaction_filter_gc_stats_destroy.rs +++ /dev/null @@ -1,90 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, - compaction::filter::{ - CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, - }, - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -struct MyFilter; - -impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key() == b"removethis" { - Ok(Verdict::Destroy) - } else { - Ok(Verdict::Keep) - } - } -} - -struct MyFilterFactory; - -impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } -} - -#[test] -fn compaction_filter_gc_stats_destroy() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let other_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("removethis", &other_big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "removethis":0 is dropped/destroyed - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = other_big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(other_big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/compaction_filter_gc_stats_remove.rs b/crates/lsm-tree/tests/compaction_filter_gc_stats_remove.rs deleted file mode 100644 index 3cd7b3962..000000000 --- a/crates/lsm-tree/tests/compaction_filter_gc_stats_remove.rs +++ /dev/null @@ -1,90 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, - compaction::filter::{ - CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, - }, - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -struct MyFilter; - -impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key() == b"removethis" { - Ok(Verdict::Remove) - } else { - Ok(Verdict::Keep) - } - } -} - -struct MyFilterFactory; - -impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } -} - -#[test] -fn compaction_filter_gc_stats_remove() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let other_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("removethis", &other_big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "removethis":0 is removed - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = other_big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(other_big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/compaction_filter_gc_stats_remove_weak.rs b/crates/lsm-tree/tests/compaction_filter_gc_stats_remove_weak.rs deleted file mode 100644 index 0f7749fcf..000000000 --- a/crates/lsm-tree/tests/compaction_filter_gc_stats_remove_weak.rs +++ /dev/null @@ -1,90 +0,0 @@ -use lsm_tree::{ - blob_tree::FragmentationEntry, - compaction::filter::{ - CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, - }, - get_tmp_folder, AbstractTree, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -struct MyFilter; - -impl CompactionFilter for MyFilter { - fn filter_item( - &mut self, - item: ItemAccessor<'_>, - _ctx: &CompactionFilterContext, - ) -> lsm_tree::Result { - if item.key() == b"removethis" { - Ok(Verdict::RemoveWeak) - } else { - Ok(Verdict::Keep) - } - } -} - -struct MyFilterFactory; - -impl Factory for MyFilterFactory { - fn name(&self) -> &str { - "Test" - } - - fn make_filter(&self, _ctx: &CompactionFilterContext) -> Box { - Box::new(MyFilter) - } -} - -#[test] -fn compaction_filter_gc_stats_remove_weak() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - let path = folder.path(); - - let big_value = b"neptune!".repeat(128_000); - let other_big_value = b"winter!".repeat(128_000); - - { - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default().compression(lsm_tree::CompressionType::None), - )) - .with_compaction_filter_factory(Some(Arc::new(MyFilterFactory))) - .open()?; - - assert!(tree.get("big", SeqNo::MAX)?.is_none()); - tree.insert("big", &big_value, 0); - tree.insert("removethis", &other_big_value, 0); - - let value = tree.get("big", SeqNo::MAX)?.expect("should exist"); - assert_eq!(&*value, big_value); - - tree.flush_active_memtable(0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.major_compact(64_000_000, 1_000)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - let gc_stats = tree.current_version().gc_stats().clone(); - - // "removethis":0 is removed - assert_eq!( - &{ - let mut map = lsm_tree::HashMap::default(); - let size = other_big_value.len() as u64; - map.insert(0, FragmentationEntry::new(1, size, size)); - map - }, - &*gc_stats, - ); - assert_eq!(other_big_value.len() as u64, tree.stale_blob_bytes()); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/compaction_filter_snapshot.rs b/crates/lsm-tree/tests/compaction_filter_snapshot.rs index d93d1f758..0944518ac 100644 --- a/crates/lsm-tree/tests/compaction_filter_snapshot.rs +++ b/crates/lsm-tree/tests/compaction_filter_snapshot.rs @@ -1,7 +1,7 @@ use lsm_tree::compaction::filter::{ CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, }; -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, get_tmp_folder}; use std::sync::Arc; use test_log::test; diff --git a/crates/lsm-tree/tests/compaction_filter_ttl.rs b/crates/lsm-tree/tests/compaction_filter_ttl.rs index cc2ac755d..80b0df7cb 100644 --- a/crates/lsm-tree/tests/compaction_filter_ttl.rs +++ b/crates/lsm-tree/tests/compaction_filter_ttl.rs @@ -1,12 +1,12 @@ use lsm_tree::{ + AbstractTree, Config, compaction::filter::{ CompactionFilter, Context as CompactionFilterContext, Factory, ItemAccessor, Verdict, }, - AbstractTree, Config, }; use std::sync::{ - atomic::{AtomicU64, Ordering::Relaxed}, Arc, + atomic::{AtomicU64, Ordering::Relaxed}, }; use test_log::test; diff --git a/crates/lsm-tree/tests/compaction_readers_grouping.rs b/crates/lsm-tree/tests/compaction_readers_grouping.rs index 2fc000bda..bb9cc0f58 100644 --- a/crates/lsm-tree/tests/compaction_readers_grouping.rs +++ b/crates/lsm-tree/tests/compaction_readers_grouping.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use std::sync::Arc; use test_log::test; @@ -36,29 +36,35 @@ fn compaction_readers_grouping() -> lsm_tree::Result<()> { // breaking this tree.compact(Arc::new(lsm_tree::compaction::PullDown(2, 3)), 0)?; - assert!(!tree - .current_version() - .level(0) - .expect("level should exist") - .is_empty()); + assert!( + !tree + .current_version() + .level(0) + .expect("level should exist") + .is_empty() + ); - assert!(tree - .current_version() - .level(1) - .expect("level should exist") - .is_empty()); + assert!( + tree.current_version() + .level(1) + .expect("level should exist") + .is_empty() + ); - assert!(tree - .current_version() - .level(2) - .expect("level should exist") - .is_empty()); + assert!( + tree.current_version() + .level(2) + .expect("level should exist") + .is_empty() + ); - assert!(!tree - .current_version() - .level(3) - .expect("level should exist") - .is_empty()); + assert!( + !tree + .current_version() + .level(3) + .expect("level should exist") + .is_empty() + ); Ok(()) } diff --git a/crates/lsm-tree/tests/dirty_snapshot.rs b/crates/lsm-tree/tests/dirty_snapshot.rs deleted file mode 100644 index 018318eab..000000000 --- a/crates/lsm-tree/tests/dirty_snapshot.rs +++ /dev/null @@ -1,24 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn dirty_snapshot_after_drop_range() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(dir.path(), seqno.clone(), SequenceNumberCounter::default()).open()?; - - tree.insert("a", "a", seqno.next()); - tree.flush_active_memtable(0)?; - tree.insert("b", "b", seqno.next()); - tree.flush_active_memtable(0)?; - - let snapshot_seqno = seqno.get(); - assert_eq!(b"a", &*tree.get("a", snapshot_seqno)?.unwrap()); - - tree.drop_range("a"..="a")?; - assert_eq!(b"a", &*tree.get("a", snapshot_seqno)?.unwrap()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/expect_point_read_hits.rs b/crates/lsm-tree/tests/expect_point_read_hits.rs index f5e63ee5f..f1f68b430 100644 --- a/crates/lsm-tree/tests/expect_point_read_hits.rs +++ b/crates/lsm-tree/tests/expect_point_read_hits.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/fifo_ttl.rs b/crates/lsm-tree/tests/fifo_ttl.rs deleted file mode 100644 index 7b3e4533e..000000000 --- a/crates/lsm-tree/tests/fifo_ttl.rs +++ /dev/null @@ -1,134 +0,0 @@ -use lsm_tree::{ - compaction::Fifo, get_tmp_folder, AbstractTree, Config, KvSeparationOptions, - SequenceNumberCounter, -}; -use std::sync::Arc; - -#[test] -fn fifo_ttl_no_drop_when_recent_or_disabled() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - // Two quick tables (both recent) - tree.insert("a", "1", 0); - tree.flush_active_memtable(0)?; - tree.insert("b", "2", 1); - tree.flush_active_memtable(1)?; - - assert_eq!(2, tree.table_count()); - - // TTL enabled but not yet expired - let fifo_recent = Arc::new(Fifo::new(u64::MAX, Some(15))); - tree.compact(fifo_recent, 2)?; - assert_eq!(2, tree.table_count()); - - // TTL disabled explicitly - let fifo_disabled = Arc::new(Fifo::new(u64::MAX, None)); - tree.compact(fifo_disabled, 2)?; - assert_eq!(2, tree.table_count()); - - Ok(()) -} - -#[test] -fn fifo_below_limit_no_drop_standard() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - for i in 0..3u8 { - tree.insert([b'k', i].as_slice(), "v", i as u64); - tree.flush_active_memtable(i as u64)?; - } - - let fifo = Arc::new(Fifo::new(u64::MAX, None)); - tree.compact(fifo, 3)?; - - assert_eq!(3, tree.table_count()); - - Ok(()) -} - -#[test] -fn fifo_limit_considers_blob_bytes() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - // Create multiple small tables referencing blob files - for i in 0..3u8 { - tree.insert([b'k', i].as_slice(), "$", i as u64); // value goes to blob - tree.flush_active_memtable(i as u64)?; - } - - let before = tree.table_count(); - assert_eq!(3, before); - - // Very small limit forces dropping based on (table + blob) size - let fifo = Arc::new(Fifo::new(10, None)); - tree.compact(fifo, 3)?; - - // Should have dropped at least one table due to blob bytes pressure - assert!(tree.table_count() < before); - - Ok(()) -} - -#[test] -fn fifo_compact_empty_tree_noop() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - let fifo = Arc::new(Fifo::new(1_000_000, Some(1))); - tree.compact(fifo, 0)?; - - assert_eq!(0, tree.table_count()); - - Ok(()) -} - -#[test] -fn fifo_idempotent_when_within_limits() -> lsm_tree::Result<()> { - let dir = get_tmp_folder(); - let tree = Config::new( - dir.path(), - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - for i in 0..3u8 { - tree.insert([b'k', i].as_slice(), "v", i as u64); - tree.flush_active_memtable(i as u64)?; - } - - let fifo = Arc::new(Fifo::new(u64::MAX, None)); - tree.compact(fifo.clone(), 3)?; - let after_first = tree.table_count(); - - tree.compact(fifo, 3)?; - let after_second = tree.table_count(); - - assert_eq!(after_first, after_second); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/format_v5.rs b/crates/lsm-tree/tests/format_v5.rs new file mode 100644 index 000000000..05d9375d5 --- /dev/null +++ b/crates/lsm-tree/tests/format_v5.rs @@ -0,0 +1,92 @@ +use lsm_tree::{AbstractTree, Config, Error, SequenceNumberCounter, get_tmp_folder}; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, Write}, +}; + +#[test] +fn v4_manifest_is_rejected_before_table_recovery() -> lsm_tree::Result<()> { + let folder = get_tmp_folder(); + + { + let _tree = Config::new( + folder.path(), + SequenceNumberCounter::default(), + SequenceNumberCounter::default(), + ) + .open()?; + } + + let mut current = File::open(folder.path().join("current"))?; + let mut version_bytes = [0; size_of::()]; + current.read_exact(&mut version_bytes)?; + let manifest_path = folder + .path() + .join(format!("v{}", u64::from_le_bytes(version_bytes))); + + let reader = sfa::Reader::new(&manifest_path)?; + let version_offset = reader + .toc() + .section(b"format_version") + .expect("format version should exist") + .pos(); + + let mut manifest = OpenOptions::new().write(true).open(manifest_path)?; + manifest.seek(std::io::SeekFrom::Start(version_offset))?; + manifest.write_all(&[4])?; + drop(manifest); + + assert!(matches!( + Config::new( + folder.path(), + SequenceNumberCounter::default(), + SequenceNumberCounter::default(), + ) + .open(), + Err(Error::InvalidVersion(4)), + )); + Ok(()) +} + +#[test] +fn v4_table_is_rejected_before_metadata_recovery() -> lsm_tree::Result<()> { + let folder = get_tmp_folder(); + + { + let tree = Config::new( + folder.path(), + SequenceNumberCounter::default(), + SequenceNumberCounter::default(), + ) + .open()?; + tree.insert("key", "value", 0); + tree.flush_active_memtable(0)?; + } + + let table_path = std::fs::read_dir(folder.path().join("tables"))? + .next() + .expect("table should exist")? + .path(); + let reader = sfa::Reader::new(&table_path)?; + let version_offset = reader + .toc() + .section(b"table_version") + .expect("table version should exist") + .pos(); + + let mut table = OpenOptions::new().write(true).open(table_path)?; + table.seek(std::io::SeekFrom::Start(version_offset))?; + table.write_all(&[4])?; + drop(table); + + assert!(matches!( + Config::new( + folder.path(), + SequenceNumberCounter::default(), + SequenceNumberCounter::default(), + ) + .open(), + Err(Error::InvalidVersion(4)), + )); + Ok(()) +} diff --git a/crates/lsm-tree/tests/guard_if.rs b/crates/lsm-tree/tests/guard_if.rs index 6072d5d0f..1f31fdf31 100644 --- a/crates/lsm-tree/tests/guard_if.rs +++ b/crates/lsm-tree/tests/guard_if.rs @@ -1,6 +1,4 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, Guard, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -35,37 +33,3 @@ fn guard_into_inner_if() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn guard_into_inner_if_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - tree.insert("earth#name", "earth", 0); - tree.insert("earth#color", "BLUE", 0); - - assert_eq!(2, tree.iter(SeqNo::MAX, None).count()); - - assert_eq!( - 1, - tree.iter(SeqNo::MAX, None) - .filter_map(|guard| { - guard - .into_inner_if(|key| key.ends_with(b"#name")) - .unwrap() - .1 - }) - .count(), - ); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/ingest_dirty_snapshot.rs b/crates/lsm-tree/tests/ingest_dirty_snapshot.rs index 6bf1a6d7d..9eeb37cfb 100644 --- a/crates/lsm-tree/tests/ingest_dirty_snapshot.rs +++ b/crates/lsm-tree/tests/ingest_dirty_snapshot.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; #[test] fn ingestion_dirty_snapshot() -> lsm_tree::Result<()> { diff --git a/crates/lsm-tree/tests/ingestion_api.rs b/crates/lsm-tree/tests/ingestion_api.rs index 27ba70d1b..6bce63d70 100644 --- a/crates/lsm-tree/tests/ingestion_api.rs +++ b/crates/lsm-tree/tests/ingestion_api.rs @@ -1,6 +1,4 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; #[test] fn tree_ingestion_tombstones_delete_existing_keys() -> lsm_tree::Result<()> { @@ -147,37 +145,6 @@ fn ingestion_enforces_order_standard_panics() { // Panics here let _ = ingest.write(b"k1", b"v"); } - -#[test] -fn blob_ingestion_out_of_order_panics_without_blob_write() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = lsm_tree::Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(8))) - .open()?; - - let before = tree.blob_file_count(); - - // Use a small value for the first write to avoid blob I/O - let result = std::panic::catch_unwind(|| { - let mut ingest = tree.ingestion().unwrap(); - ingest.write(b"k2", b"x").unwrap(); - - // Second write would require blob I/O, but ordering check should fire before any blob write - let _ = ingest.write(b"k1", [1u8; 16]); - }); - assert!(result.is_err()); - - let after = tree.blob_file_count(); - assert_eq!(before, after); - - Ok(()) -} - #[test] fn memtable_put_overrides_table_tombstone() -> lsm_tree::Result<()> { use lsm_tree::AbstractTree; @@ -212,40 +179,6 @@ fn memtable_put_overrides_table_tombstone() -> lsm_tree::Result<()> { ); Ok(()) } - -#[test] -fn blob_tree_ingestion_tombstones_delete_existing_keys() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for i in 0..8u32 { - let key = format!("b{:03}", i); - tree.insert(key.as_bytes(), b"x", 0); - } - - let mut ingest = tree.ingestion()?; - for i in 0..8u32 { - let key = format!("b{:03}", i); - ingest.write_tombstone(key)?; - } - ingest.finish()?; - - for i in 0..8u32 { - let key = format!("b{:03}", i); - assert!(tree.get(key.as_bytes(), SeqNo::MAX)?.is_none()); - } - assert_eq!(tree.tombstone_count(), 8); - - Ok(()) -} - #[test] fn tree_ingestion_finish_no_writes_noop() -> lsm_tree::Result<()> { let folder = get_tmp_folder(); @@ -266,103 +199,3 @@ fn tree_ingestion_finish_no_writes_noop() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_ingestion_only_tombstones_does_not_create_blob_files() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for i in 0..5u32 { - let key = format!("d{:03}", i); - tree.insert(key.as_bytes(), b"value", 0); - } - - let before_blobs = tree.blob_file_count(); - - let mut ingest = tree.ingestion()?; - for i in 0..5u32 { - let key = format!("d{:03}", i); - ingest.write_tombstone(key)?; - } - ingest.finish()?; - - let after_blobs = tree.blob_file_count(); - assert_eq!(before_blobs, after_blobs); - - for i in 0..5u32 { - let key = format!("d{:03}", i); - assert!(tree.get(key.as_bytes(), SeqNo::MAX)?.is_none()); - } - - Ok(()) -} - -#[test] -fn blob_ingestion_finish_no_writes_noop() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let before_tables = tree.table_count(); - let before_blobs = tree.blob_file_count(); - - tree.ingestion()?.finish()?; - - let after_tables = tree.table_count(); - let after_blobs = tree.blob_file_count(); - - assert_eq!(before_tables, after_tables); - assert_eq!(before_blobs, after_blobs); - assert!(tree.is_empty(SeqNo::MAX, None)?); - - Ok(()) -} - -#[test] -fn blob_ingestion_separates_large_values_and_reads_ok() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(8))) - .open()?; - - let mut ingest = tree.ingestion()?; - ingest.write("k_big1", [1u8; 16])?; - ingest.write("k_big2", [2u8; 32])?; - ingest.write("k_small", "abc")?; - ingest.finish()?; - - assert!(tree.blob_file_count() >= 1); - - assert_eq!( - tree.get("k_small", SeqNo::MAX)?, - Some(b"abc".as_slice().into()) - ); - assert_eq!( - tree.get("k_big1", SeqNo::MAX)?.as_deref().map(|s| s.len()), - Some(16) - ); - assert_eq!( - tree.get("k_big2", SeqNo::MAX)?.as_deref().map(|s| s.len()), - Some(32) - ); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/ingestion_invariants.rs b/crates/lsm-tree/tests/ingestion_invariants.rs index 1c9a94127..d7204a940 100644 --- a/crates/lsm-tree/tests/ingestion_invariants.rs +++ b/crates/lsm-tree/tests/ingestion_invariants.rs @@ -1,6 +1,4 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use std::sync::mpsc; use std::thread; use std::time::Duration; @@ -129,53 +127,3 @@ fn ingestion_blocks_memtable_writes_until_finish() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_ingestion_honors_invariants_and_blocks_writes() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - // Write small values into memtable and then start blob ingestion - for i in 0..4u32 { - let k = format!("b{:03}", i); - tree.insert(k.as_bytes(), b"y", 1); - } - - let (started_tx, started_rx) = mpsc::channel(); - let (done_tx, done_rx) = mpsc::channel(); - let tree2 = tree.clone(); - - let ingest = tree.ingestion()?; - - let handle = thread::spawn(move || { - started_tx.send(()).ok(); - tree2.insert(b"b999", b"z", 31); - done_tx.send(()).ok(); - }); - - started_rx.recv().unwrap(); - thread::sleep(Duration::from_millis(100)); - assert!(done_rx.try_recv().is_ok()); - - handle.join().unwrap(); - ingest.finish()?; - - // Data visible after ingestion, including concurrent write - for i in 0..4u32 { - let k = format!("b{:03}", i); - assert_eq!( - tree.get(k.as_bytes(), SeqNo::MAX)?, - Some(b"y".as_slice().into()) - ); - } - assert_eq!(tree.get(b"b999", SeqNo::MAX)?, Some(b"z".as_slice().into())); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/ingestion_seqno.rs b/crates/lsm-tree/tests/ingestion_seqno.rs index 105965ed9..28ea7077b 100644 --- a/crates/lsm-tree/tests/ingestion_seqno.rs +++ b/crates/lsm-tree/tests/ingestion_seqno.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; #[test] fn ingestion_persisted_seqno() -> lsm_tree::Result<()> { diff --git a/crates/lsm-tree/tests/latest_snapshot.rs b/crates/lsm-tree/tests/latest_snapshot.rs index 2ba104784..40d6f8d6e 100644 --- a/crates/lsm-tree/tests/latest_snapshot.rs +++ b/crates/lsm-tree/tests/latest_snapshot.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/leveled_trivial_move.rs b/crates/lsm-tree/tests/leveled_trivial_move.rs index 008c33ed5..cb264ba95 100644 --- a/crates/lsm-tree/tests/leveled_trivial_move.rs +++ b/crates/lsm-tree/tests/leveled_trivial_move.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use std::sync::Arc; use test_log::test; diff --git a/crates/lsm-tree/tests/memtable_id.rs b/crates/lsm-tree/tests/memtable_id.rs index a6aa670ea..93852564f 100644 --- a/crates/lsm-tree/tests/memtable_id.rs +++ b/crates/lsm-tree/tests/memtable_id.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; // https://github.com/fjall-rs/lsm-tree/issues/230 diff --git a/crates/lsm-tree/tests/model_1.rs b/crates/lsm-tree/tests/model_1.rs index 89108251e..1bdafa461 100644 --- a/crates/lsm-tree/tests/model_1.rs +++ b/crates/lsm-tree/tests/model_1.rs @@ -1,6 +1,6 @@ // Found by model testing -use lsm_tree::{get_tmp_folder, AbstractTree, Result, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Result, SequenceNumberCounter, get_tmp_folder}; use std::sync::Arc; use test_log::test; diff --git a/crates/lsm-tree/tests/model_2.rs b/crates/lsm-tree/tests/model_2.rs deleted file mode 100644 index 1c84296c9..000000000 --- a/crates/lsm-tree/tests/model_2.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Found by model testing - -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, Result, SequenceNumberCounter}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn model_2() -> Result<()> { - let folder = get_tmp_folder(); - - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(5))) - .open()?; - let compaction = Arc::new(lsm_tree::compaction::Leveled::default()); - - let value = b"hellohello"; - - tree.insert("a", value, 3); - tree.flush_active_memtable(0)?; - tree.compact(compaction.clone(), 0)?; - assert_eq!(1, tree.table_count()); - assert_eq!(1, tree.blob_file_count()); - - tree.insert("b", value, 4); - tree.flush_active_memtable(0)?; - tree.compact(compaction.clone(), 0)?; - assert_eq!(2, tree.table_count()); - assert_eq!(2, tree.blob_file_count()); - - tree.insert("a", value, 5); - tree.flush_active_memtable(0)?; - tree.compact(compaction.clone(), 0)?; - assert_eq!(3, tree.table_count()); - assert_eq!(3, tree.blob_file_count()); - - tree.drop_range::<&[u8], _>(..)?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/model_3.rs b/crates/lsm-tree/tests/model_3.rs deleted file mode 100644 index df6d8c55b..000000000 --- a/crates/lsm-tree/tests/model_3.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Found by model testing - -use lsm_tree::{get_tmp_folder, AbstractTree, KvSeparationOptions, Result, SequenceNumberCounter}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn model_3() -> Result<()> { - let folder = get_tmp_folder(); - - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(5))) - .open()?; - let compaction = Arc::new(lsm_tree::compaction::Leveled::default()); - - let value = b"hellohello"; - - tree.insert("a", value, 1); - tree.insert("i", value, 1); - tree.flush_active_memtable(0)?; - - tree.insert("a", value, 2); - tree.insert("f", value, 2); - tree.flush_active_memtable(0)?; - - tree.insert("a", value, 3); - tree.insert("h", value, 3); - tree.flush_active_memtable(0)?; - - tree.insert("a", value, 4); - tree.insert("b", value, 4); - tree.flush_active_memtable(0)?; - - tree.insert("c", value, 5); - tree.insert("g", value, 5); - tree.flush_active_memtable(0)?; - - tree.insert("b", value, 6); - tree.insert("c", value, 6); - tree.insert("d", value, 6); - tree.insert("e", value, 6); - tree.flush_active_memtable(15)?; - tree.compact(compaction.clone(), 41)?; - - tree.insert("a", value, 7); - tree.flush_active_memtable(16)?; - - tree.insert("a", value, 8); - tree.flush_active_memtable(17)?; - - tree.insert("a", value, 9); - tree.flush_active_memtable(18)?; - - tree.insert("a", value, 10); - tree.flush_active_memtable(19)?; - tree.compact(compaction.clone(), 19)?; - - tree.drop_range::<&[u8], _>(..)?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/model_4.rs b/crates/lsm-tree/tests/model_4.rs deleted file mode 100644 index 4789f3e93..000000000 --- a/crates/lsm-tree/tests/model_4.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Found by model testing - -use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, KvSeparationOptions, Result, - SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn model_4() -> Result<()> { - let folder = get_tmp_folder(); - - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(5))) - .data_block_size_policy(BlockSizePolicy::all(100)) - .open()?; - - let compaction = - Arc::new(lsm_tree::compaction::Leveled::default().with_table_target_size(1_000)); - - let value = b"hellohello"; - - tree.insert([0], value, 132); - tree.insert([0], value, 133); - tree.insert([1], value, 134); - tree.insert([2], value, 135); - tree.insert([3], value, 136); - tree.insert([4], value, 137); - tree.insert([5], value, 138); - tree.insert([6], value, 139); - tree.insert([7], value, 140); - tree.insert([0, 0, 0, 0, 0, 0, 9, 217], value, 141); - tree.insert([0, 0, 0, 0, 0, 0, 2, 77], value, 142); - tree.insert([0, 0, 0, 0, 0, 0, 33, 92], value, 143); - tree.insert([0, 0, 0, 0, 0, 0, 38, 41], value, 144); - tree.insert([0, 0, 0, 0, 0, 0, 22, 143], value, 145); - tree.insert([0, 0, 0, 0, 0, 0, 22, 161], value, 146); - tree.insert([0, 0, 0, 0, 0, 0, 9, 143], value, 148); - tree.insert([0, 0, 0, 0, 0, 0, 25, 222], value, 149); - tree.insert([0, 0, 0, 0, 0, 0, 11, 144], value, 150); - tree.insert([0, 0, 0, 0, 0, 0, 8, 208], value, 151); - tree.insert([0, 0, 0, 0, 0, 0, 31, 195], value, 152); - tree.insert([0, 0, 0, 0, 0, 0, 27, 47], value, 153); - tree.insert([0, 0, 0, 0, 0, 0, 31, 104], value, 154); - tree.insert([0, 0, 0, 0, 0, 0, 14, 219], value, 155); - tree.insert([0, 0, 0, 0, 0, 0, 17, 125], value, 156); - tree.insert([0, 0, 0, 0, 0, 0, 15, 52], value, 157); - tree.insert([0, 0, 0, 0, 0, 0, 20, 230], value, 158); - tree.insert([0, 0, 0, 0, 0, 0, 16, 88], value, 159); - tree.insert([0, 0, 0, 0, 0, 0, 9, 26], value, 160); - tree.insert([0, 0, 0, 0, 0, 0, 20, 21], value, 161); - tree.insert([0, 0, 0, 0, 0, 0, 27, 86], value, 162); - tree.insert([0, 0, 0, 0, 0, 0, 4, 112], value, 163); - tree.insert([0, 0, 0, 0, 0, 0, 12, 60], value, 164); - tree.insert([0, 0, 0, 0, 0, 0, 8, 186], value, 165); - tree.insert([0, 0, 0, 0, 0, 0, 34, 18], value, 166); - tree.insert([0, 0, 0, 0, 0, 0, 15, 156], value, 167); - tree.insert([0, 0, 0, 0, 0, 0, 5, 91], value, 168); - tree.insert([0, 0, 0, 0, 0, 0, 36, 0], value, 169); - tree.insert([0, 0, 0, 0, 0, 0, 38, 249], value, 170); - tree.insert([0, 0, 0, 0, 0, 0, 23, 42], value, 171); - tree.insert([0, 0, 0, 0, 0, 0, 23, 14], value, 172); - tree.insert([0, 0, 0, 0, 0, 0, 32, 119], value, 173); - tree.insert([0, 0, 0, 0, 0, 0, 31, 9], value, 174); - tree.insert([0, 0, 0, 0, 0, 0, 4, 170], value, 175); - tree.insert([0, 0, 0, 0, 0, 0, 18, 119], value, 176); - tree.insert([0, 0, 0, 0, 0, 0, 4, 178], value, 177); - tree.insert([0, 0, 0, 0, 0, 0, 4, 36], value, 178); - tree.insert([0, 0, 0, 0, 0, 0, 36, 53], value, 179); - tree.insert([0, 0, 0, 0, 0, 0, 35, 157], value, 181); - tree.insert([0, 0, 0, 0, 0, 0, 22, 24], value, 182); - tree.insert([0, 0, 0, 0, 0, 0, 33, 247], value, 183); - tree.insert([0, 0, 0, 0, 0, 0, 26, 236], value, 185); - tree.flush_active_memtable(86)?; - - tree.insert([0], value, 186); - tree.flush_active_memtable(87)?; - - tree.insert([0, 0, 0, 0, 0, 0, 7, 49], value, 187); - tree.flush_active_memtable(88)?; - - tree.insert([0, 0, 0, 0, 0, 0, 18, 134], value, 188); - tree.flush_active_memtable(89)?; - tree.compact(compaction.clone(), 89)?; - - tree.insert([0], value, 189); - tree.flush_active_memtable(90)?; - - tree.insert([0], value, 190); - tree.flush_active_memtable(91)?; - - tree.insert([0], value, 191); - tree.flush_active_memtable(92)?; - - tree.insert([0], value, 192); - tree.flush_active_memtable(93)?; - tree.compact(compaction.clone(), 93)?; - - tree.drop_range::<&[u8], _>(..)?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/model_5.rs b/crates/lsm-tree/tests/model_5.rs deleted file mode 100644 index d683c387d..000000000 --- a/crates/lsm-tree/tests/model_5.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Found by model testing - -use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, KvSeparationOptions, Result, - SequenceNumberCounter, -}; -use std::sync::Arc; -use test_log::test; - -#[test] -fn model_5() -> Result<()> { - let folder = get_tmp_folder(); - - let path = folder.path(); - - let tree = lsm_tree::Config::new( - path, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(5))) - .data_block_size_policy(BlockSizePolicy::all(100)) - .open()?; - - let compaction = Arc::new(lsm_tree::compaction::Leveled::default().with_table_target_size(150)); - - let value = b"hellohello"; - - tree.insert([0], value, 753); - tree.insert([0, 0, 0, 0, 0, 0, 1, 0], value, 754); - tree.insert([0, 0, 0, 0, 0, 0, 0, 140], value, 756); - tree.insert([0, 0, 0, 0, 0, 0, 0, 127], value, 762); - tree.insert([0, 0, 0, 0, 0, 0, 0, 98], value, 763); - tree.insert([0, 0, 0, 0, 0, 0, 2, 138], value, 764); - tree.insert([0, 0, 0, 0, 0, 0, 3, 150], value, 765); - tree.insert([0, 0, 0, 0, 0, 0, 0, 23], value, 766); - tree.insert([0, 0, 0, 0, 0, 0, 3, 121], value, 767); - tree.insert([0, 0, 0, 0, 0, 0, 1, 212], value, 842); - tree.insert([0, 0, 0, 0, 0, 0, 2, 152], value, 843); - tree.insert([0, 0, 0, 0, 0, 0, 2, 241], value, 844); - tree.flush_active_memtable(798)?; - - tree.insert([0, 0, 0, 0, 0, 0, 3, 120], value, 898); - tree.flush_active_memtable(799)?; - - tree.insert([0, 0, 0, 0, 0, 0, 3, 89], value, 899); - tree.flush_active_memtable(800)?; - - tree.insert([0, 0, 0, 0, 0, 0, 1, 52], value, 901); - tree.insert([0, 0, 0, 0, 0, 0, 0, 177], value, 902); - tree.insert([0, 0, 0, 0, 0, 0, 3, 43], value, 903); - tree.insert([0, 0, 0, 0, 0, 0, 3, 41], value, 904); - tree.insert([0, 0, 0, 0, 0, 0, 3, 160], value, 905); - tree.insert([0, 0, 0, 0, 0, 0, 1, 182], value, 906); - tree.insert([0, 0, 0, 0, 0, 0, 0, 73], value, 907); - tree.insert([0, 0, 0, 0, 0, 0, 0, 78], value, 912); - tree.insert([0, 0, 0, 0, 0, 0, 2, 103], value, 913); - tree.insert([0, 0, 0, 0, 0, 0, 1, 39], value, 914); - tree.insert([0, 0, 0, 0, 0, 0, 1, 78], value, 927); - tree.insert([0, 0, 0, 0, 0, 0, 0, 244], value, 928); - tree.insert([0, 0, 0, 0, 0, 0, 2, 76], value, 929); - tree.insert([0, 0, 0, 0, 0, 0, 1, 202], value, 934); - tree.insert([0, 0, 0, 0, 0, 0, 2, 140], value, 936); - tree.insert([0, 0, 0, 0, 0, 0, 2, 152], value, 937); - tree.flush_active_memtable(886)?; - tree.compact(compaction.clone(), 886)?; - - tree.insert([0, 0, 0, 0, 0, 0, 3, 145], value, 989); - tree.flush_active_memtable(890)?; - - tree.insert([0, 0, 0, 0, 0, 0, 3, 99], value, 993); - tree.flush_active_memtable(894)?; - - tree.insert([0, 0, 0, 0, 0, 0, 1, 106], value, 997); - tree.flush_active_memtable(898)?; - - tree.insert([0, 0, 0, 0, 0, 0, 2, 99], value, 1001); - tree.flush_active_memtable(902)?; - tree.compact(compaction.clone(), 902)?; - - tree.drop_range::<&[u8], _>(..)?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/model_6.rs b/crates/lsm-tree/tests/model_6.rs deleted file mode 100644 index b75f4f7e7..000000000 --- a/crates/lsm-tree/tests/model_6.rs +++ /dev/null @@ -1,1293 +0,0 @@ -// Found by model testing - -#![cfg_attr(rustfmt, rustfmt_skip)] - -use lsm_tree::{ - get_tmp_folder, - AbstractTree, KvSeparationOptions, Result, SequenceNumberCounter, config::{BlockSizePolicy, CompressionPolicy} -}; -use std::sync::Arc; -use test_log::test; - -// Yes this file is very large, it's hard to condense it to a more minimal repro -#[test] -fn model_6() -> Result<()> { - let folder = get_tmp_folder(); - - let path = folder.path(); - - let tree = lsm_tree::Config::new(path, SequenceNumberCounter::default(), SequenceNumberCounter::default()) - .data_block_compression_policy(CompressionPolicy::disabled()) - .index_block_compression_policy(CompressionPolicy::disabled()) - .data_block_size_policy(BlockSizePolicy::all(100)) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(10))) - .open()?; - - let compaction = Arc::new( - lsm_tree::compaction::Leveled::default() - .with_table_target_size(1_024) - ); - - tree.insert([0, 0, 0, 0, 0, 0, 3, 152], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170598); -tree.insert([0, 0, 0, 0, 0, 0, 0, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170599); -tree.insert([0, 0, 0, 0, 0, 0, 1, 184], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170600); -tree.flush_active_memtable(170501)?; -tree.insert([0, 0, 0, 0, 0, 0, 3, 1], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170601); -tree.insert([0, 0, 0, 0, 0, 0, 2, 31], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170602); -tree.insert([0, 0, 0, 0, 0, 0, 3, 95], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170603); -tree.insert([0, 0, 0, 0, 0, 0, 1, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170604); -tree.insert([0, 0, 0, 0, 0, 0, 0, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170605); -tree.insert([0, 0, 0, 0, 0, 0, 3, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170606); -tree.insert([0, 0, 0, 0, 0, 0, 2, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170607); -tree.insert([0, 0, 0, 0, 0, 0, 0, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170608); -tree.insert([0, 0, 0, 0, 0, 0, 2, 151], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170609); -tree.insert([0, 0, 0, 0, 0, 0, 2, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170610); -tree.insert([0, 0, 0, 0, 0, 0, 2, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170611); -tree.insert([0, 0, 0, 0, 0, 0, 2, 133], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170612); -tree.insert([0, 0, 0, 0, 0, 0, 3, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170613); -tree.insert([0, 0, 0, 0, 0, 0, 2, 189], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170614); -tree.insert([0, 0, 0, 0, 0, 0, 3, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170615); -tree.insert([0, 0, 0, 0, 0, 0, 1, 157], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170616); -tree.insert([0, 0, 0, 0, 0, 0, 2, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170617); -tree.insert([0, 0, 0, 0, 0, 0, 3, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170619); -tree.insert([0, 0, 0, 0, 0, 0, 2, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170620); -tree.insert([0, 0, 0, 0, 0, 0, 0, 188], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170621); -tree.insert([0, 0, 0, 0, 0, 0, 0, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170624); -tree.insert([0, 0, 0, 0, 0, 0, 1, 140], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170625); -tree.insert([0, 0, 0, 0, 0, 0, 0, 255], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170626); -tree.insert([0, 0, 0, 0, 0, 0, 1, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170628); -tree.insert([0, 0, 0, 0, 0, 0, 2, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170629); -tree.insert([0, 0, 0, 0, 0, 0, 0, 103], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170630); -tree.insert([0, 0, 0, 0, 0, 0, 0, 225], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170631); -tree.insert([0, 0, 0, 0, 0, 0, 0, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170633); -tree.insert([0, 0, 0, 0, 0, 0, 0, 242], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170634); -tree.insert([0, 0, 0, 0, 0, 0, 0, 122], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170635); -tree.insert([0, 0, 0, 0, 0, 0, 0, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170636); -tree.insert([0, 0, 0, 0, 0, 0, 1, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170638); -tree.insert([0, 0, 0, 0, 0, 0, 0, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170639); -tree.insert([0, 0, 0, 0, 0, 0, 1, 213], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170641); -tree.insert([0, 0, 0, 0, 0, 0, 2, 150], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170643); -tree.insert([0, 0, 0, 0, 0, 0, 0, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170644); -tree.insert([0, 0, 0, 0, 0, 0, 1, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170645); -tree.insert([0, 0, 0, 0, 0, 0, 0, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170647); -tree.insert([0, 0, 0, 0, 0, 0, 0, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170648); -tree.insert([0, 0, 0, 0, 0, 0, 0, 73], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170649); -tree.insert([0, 0, 0, 0, 0, 0, 0, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170650); -tree.insert([0, 0, 0, 0, 0, 0, 1, 1], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170651); -tree.insert([0, 0, 0, 0, 0, 0, 2, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170653); -tree.insert([0, 0, 0, 0, 0, 0, 1, 204], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170654); -tree.insert([0, 0, 0, 0, 0, 0, 1, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170655); -tree.insert([0, 0, 0, 0, 0, 0, 2, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170656); -tree.insert([0, 0, 0, 0, 0, 0, 1, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170657); -tree.insert([0, 0, 0, 0, 0, 0, 2, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170658); -tree.insert([0, 0, 0, 0, 0, 0, 0, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170659); -tree.insert([0, 0, 0, 0, 0, 0, 1, 248], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170660); -tree.insert([0, 0, 0, 0, 0, 0, 2, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170661); -tree.insert([0, 0, 0, 0, 0, 0, 1, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170662); -tree.insert([0, 0, 0, 0, 0, 0, 0, 79], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170664); -tree.insert([0, 0, 0, 0, 0, 0, 1, 216], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170665); -tree.insert([0, 0, 0, 0, 0, 0, 0, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170666); -tree.insert([0, 0, 0, 0, 0, 0, 0, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170669); -tree.insert([0, 0, 0, 0, 0, 0, 1, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170671); -tree.insert([0, 0, 0, 0, 0, 0, 0, 38], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170675); -tree.insert([0, 0, 0, 0, 0, 0, 1, 156], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170676); -tree.insert([0, 0, 0, 0, 0, 0, 0, 123], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170677); -tree.insert([0, 0, 0, 0, 0, 0, 0, 238], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170679); -tree.insert([0, 0, 0, 0, 0, 0, 0, 164], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170680); -tree.insert([0, 0, 0, 0, 0, 0, 1, 244], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170681); -tree.insert([0, 0, 0, 0, 0, 0, 0, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170687); -tree.insert([0, 0, 0, 0, 0, 0, 0, 201], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170689); -tree.insert([0, 0, 0, 0, 0, 0, 1, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170690); -tree.insert([0, 0, 0, 0, 0, 0, 0, 247], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170692); -tree.insert([0, 0, 0, 0, 0, 0, 1, 29], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170694); -tree.insert([0, 0, 0, 0, 0, 0, 1, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170695); -tree.insert([0, 0, 0, 0, 0, 0, 1, 158], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170699); -tree.insert([0, 0, 0, 0, 0, 0, 0, 236], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170702); -tree.insert([0, 0, 0, 0, 0, 0, 2, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170705); -tree.insert([0, 0, 0, 0, 0, 0, 0, 107], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170707); -tree.insert([0, 0, 0, 0, 0, 0, 1, 167], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170709); -tree.insert([0, 0, 0, 0, 0, 0, 0, 180], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170710); -tree.insert([0, 0, 0, 0, 0, 0, 3, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170711); -tree.insert([0, 0, 0, 0, 0, 0, 3, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170712); -tree.insert([0, 0, 0, 0, 0, 0, 1, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170713); -tree.insert([0, 0, 0, 0, 0, 0, 3, 66], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170714); -tree.insert([0, 0, 0, 0, 0, 0, 1, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170715); -tree.insert([0, 0, 0, 0, 0, 0, 1, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170716); -tree.insert([0, 0, 0, 0, 0, 0, 1, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170717); -tree.insert([0, 0, 0, 0, 0, 0, 3, 222], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170718); -tree.insert([0, 0, 0, 0, 0, 0, 0, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170719); -tree.insert([0, 0, 0, 0, 0, 0, 3, 209], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170720); -tree.insert([0, 0, 0, 0, 0, 0, 1, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170721); -tree.insert([0, 0, 0, 0, 0, 0, 3, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170722); -tree.insert([0, 0, 0, 0, 0, 0, 0, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170723); -tree.insert([0, 0, 0, 0, 0, 0, 2, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170724); -tree.insert([0, 0, 0, 0, 0, 0, 3, 44], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170725); -tree.insert([0, 0, 0, 0, 0, 0, 2, 158], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170726); -tree.insert([0, 0, 0, 0, 0, 0, 3, 67], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170727); -tree.insert([0, 0, 0, 0, 0, 0, 0, 152], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170728); -tree.insert([0, 0, 0, 0, 0, 0, 3, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170729); -tree.insert([0, 0, 0, 0, 0, 0, 0, 75], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170730); -tree.insert([0, 0, 0, 0, 0, 0, 1, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170731); -tree.insert([0, 0, 0, 0, 0, 0, 1, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170732); -tree.insert([0, 0, 0, 0, 0, 0, 1, 143], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170733); -tree.insert([0, 0, 0, 0, 0, 0, 1, 254], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170735); -tree.insert([0, 0, 0, 0, 0, 0, 2, 183], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170736); -tree.insert([0, 0, 0, 0, 0, 0, 1, 236], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170737); -tree.insert([0, 0, 0, 0, 0, 0, 3, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170738); -tree.insert([0, 0, 0, 0, 0, 0, 2, 6], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170739); -tree.insert([0, 0, 0, 0, 0, 0, 2, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170740); -tree.insert([0, 0, 0, 0, 0, 0, 2, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170741); -tree.insert([0, 0, 0, 0, 0, 0, 2, 70], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170742); -tree.insert([0, 0, 0, 0, 0, 0, 3, 29], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170743); -tree.insert([0, 0, 0, 0, 0, 0, 0, 101], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170744); -tree.insert([0, 0, 0, 0, 0, 0, 1, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170745); -tree.insert([0, 0, 0, 0, 0, 0, 3, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170746); -tree.insert([0, 0, 0, 0, 0, 0, 0, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170747); -tree.insert([0, 0, 0, 0, 0, 0, 1, 215], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170748); -tree.insert([0, 0, 0, 0, 0, 0, 1, 3], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170749); -tree.insert([0, 0, 0, 0, 0, 0, 3, 65], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170751); -tree.insert([0, 0, 0, 0, 0, 0, 1, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170752); -tree.insert([0, 0, 0, 0, 0, 0, 3, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170754); -tree.insert([0, 0, 0, 0, 0, 0, 0, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170755); -tree.insert([0, 0, 0, 0, 0, 0, 0, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170756); -tree.insert([0, 0, 0, 0, 0, 0, 1, 100], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170757); -tree.insert([0, 0, 0, 0, 0, 0, 2, 134], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170758); -tree.insert([0, 0, 0, 0, 0, 0, 1, 86], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170759); -tree.insert([0, 0, 0, 0, 0, 0, 0, 95], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170760); -tree.insert([0, 0, 0, 0, 0, 0, 1, 176], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170761); -tree.insert([0, 0, 0, 0, 0, 0, 2, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170764); -tree.insert([0, 0, 0, 0, 0, 0, 3, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170765); -tree.insert([0, 0, 0, 0, 0, 0, 3, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170766); -tree.insert([0, 0, 0, 0, 0, 0, 1, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170767); -tree.insert([0, 0, 0, 0, 0, 0, 2, 248], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170768); -tree.insert([0, 0, 0, 0, 0, 0, 1, 129], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170769); -tree.insert([0, 0, 0, 0, 0, 0, 0, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170770); -tree.insert([0, 0, 0, 0, 0, 0, 1, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170771); -tree.insert([0, 0, 0, 0, 0, 0, 1, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170772); -tree.insert([0, 0, 0, 0, 0, 0, 3, 210], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170773); -tree.insert([0, 0, 0, 0, 0, 0, 1, 81], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170774); -tree.insert([0, 0, 0, 0, 0, 0, 0, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170775); -tree.insert([0, 0, 0, 0, 0, 0, 1, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170776); -tree.insert([0, 0, 0, 0, 0, 0, 1, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170777); -tree.insert([0, 0, 0, 0, 0, 0, 3, 75], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170778); -tree.insert([0, 0, 0, 0, 0, 0, 3, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170779); -tree.insert([0, 0, 0, 0, 0, 0, 2, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170780); -tree.insert([0, 0, 0, 0, 0, 0, 3, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170781); -tree.insert([0, 0, 0, 0, 0, 0, 1, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170782); -tree.insert([0, 0, 0, 0, 0, 0, 0, 144], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170784); -tree.insert([0, 0, 0, 0, 0, 0, 2, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170785); -tree.insert([0, 0, 0, 0, 0, 0, 2, 236], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170786); -tree.insert([0, 0, 0, 0, 0, 0, 1, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170787); -tree.insert([0, 0, 0, 0, 0, 0, 3, 18], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170788); -tree.insert([0, 0, 0, 0, 0, 0, 3, 175], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170789); -tree.insert([0, 0, 0, 0, 0, 0, 1, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170790); -tree.insert([0, 0, 0, 0, 0, 0, 2, 255], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170791); -tree.insert([0, 0, 0, 0, 0, 0, 0, 109], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170793); -tree.insert([0, 0, 0, 0, 0, 0, 1, 148], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170794); -tree.insert([0, 0, 0, 0, 0, 0, 0, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170795); -tree.insert([0, 0, 0, 0, 0, 0, 2, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170796); -tree.insert([0, 0, 0, 0, 0, 0, 3, 91], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170797); -tree.insert([0, 0, 0, 0, 0, 0, 0, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170798); -tree.insert([0, 0, 0, 0, 0, 0, 1, 247], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170799); -tree.flush_active_memtable(170701)?; -tree.insert([0, 0, 0, 0, 0, 0, 1, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170801); -tree.insert([0, 0, 0, 0, 0, 0, 1, 165], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170802); -tree.insert([0, 0, 0, 0, 0, 0, 0, 116], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170804); -tree.insert([0, 0, 0, 0, 0, 0, 0, 32], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170805); -tree.insert([0, 0, 0, 0, 0, 0, 0, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170809); -tree.insert([0, 0, 0, 0, 0, 0, 1, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170812); -tree.insert([0, 0, 0, 0, 0, 0, 2, 145], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170813); -tree.insert([0, 0, 0, 0, 0, 0, 1, 251], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170814); -tree.insert([0, 0, 0, 0, 0, 0, 0, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170815); -tree.insert([0, 0, 0, 0, 0, 0, 3, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170816); -tree.insert([0, 0, 0, 0, 0, 0, 3, 189], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170817); -tree.insert([0, 0, 0, 0, 0, 0, 2, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170818); -tree.insert([0, 0, 0, 0, 0, 0, 3, 145], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170819); -tree.insert([0, 0, 0, 0, 0, 0, 3, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170820); -tree.insert([0, 0, 0, 0, 0, 0, 0, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170822); -tree.insert([0, 0, 0, 0, 0, 0, 2, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170823); -tree.insert([0, 0, 0, 0, 0, 0, 0, 211], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170824); -tree.insert([0, 0, 0, 0, 0, 0, 3, 92], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170825); -tree.insert([0, 0, 0, 0, 0, 0, 0, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170826); -tree.insert([0, 0, 0, 0, 0, 0, 2, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170827); -tree.insert([0, 0, 0, 0, 0, 0, 0, 133], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170828); -tree.insert([0, 0, 0, 0, 0, 0, 2, 167], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170829); -tree.insert([0, 0, 0, 0, 0, 0, 0, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170830); -tree.insert([0, 0, 0, 0, 0, 0, 1, 242], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170831); -tree.insert([0, 0, 0, 0, 0, 0, 0, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170832); -tree.insert([0, 0, 0, 0, 0, 0, 1, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170833); -tree.insert([0, 0, 0, 0, 0, 0, 1, 240], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170834); -tree.insert([0, 0, 0, 0, 0, 0, 2, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170836); -tree.insert([0, 0, 0, 0, 0, 0, 3, 185], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170837); -tree.insert([0, 0, 0, 0, 0, 0, 0, 145], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170838); -tree.insert([0, 0, 0, 0, 0, 0, 2, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170840); -tree.insert([0, 0, 0, 0, 0, 0, 3, 143], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170841); -tree.insert([0, 0, 0, 0, 0, 0, 0, 227], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170842); -tree.insert([0, 0, 0, 0, 0, 0, 2, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170843); -tree.insert([0, 0, 0, 0, 0, 0, 1, 151], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170844); -tree.insert([0, 0, 0, 0, 0, 0, 1, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170846); -tree.insert([0, 0, 0, 0, 0, 0, 2, 240], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170847); -tree.insert([0, 0, 0, 0, 0, 0, 3, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170848); -tree.insert([0, 0, 0, 0, 0, 0, 1, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170850); -tree.insert([0, 0, 0, 0, 0, 0, 0, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170851); -tree.insert([0, 0, 0, 0, 0, 0, 1, 101], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170852); -tree.insert([0, 0, 0, 0, 0, 0, 3, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170853); -tree.insert([0, 0, 0, 0, 0, 0, 2, 76], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170855); -tree.insert([0, 0, 0, 0, 0, 0, 1, 234], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170856); -tree.insert([0, 0, 0, 0, 0, 0, 1, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170857); -tree.insert([0, 0, 0, 0, 0, 0, 0, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170859); -tree.insert([0, 0, 0, 0, 0, 0, 1, 228], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170860); -tree.insert([0, 0, 0, 0, 0, 0, 2, 159], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170861); -tree.insert([0, 0, 0, 0, 0, 0, 2, 233], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170862); -tree.insert([0, 0, 0, 0, 0, 0, 3, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170863); -tree.insert([0, 0, 0, 0, 0, 0, 1, 95], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170864); -tree.insert([0, 0, 0, 0, 0, 0, 0, 254], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170865); -tree.insert([0, 0, 0, 0, 0, 0, 1, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170866); -tree.insert([0, 0, 0, 0, 0, 0, 2, 61], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170868); -tree.insert([0, 0, 0, 0, 0, 0, 3, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170869); -tree.insert([0, 0, 0, 0, 0, 0, 3, 151], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170871); -tree.insert([0, 0, 0, 0, 0, 0, 0, 239], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170873); -tree.insert([0, 0, 0, 0, 0, 0, 0, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170874); -tree.insert([0, 0, 0, 0, 0, 0, 1, 137], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170875); -tree.insert([0, 0, 0, 0, 0, 0, 2, 36], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170877); -tree.insert([0, 0, 0, 0, 0, 0, 2, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170879); -tree.insert([0, 0, 0, 0, 0, 0, 2, 230], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170880); -tree.insert([0, 0, 0, 0, 0, 0, 3, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170881); -tree.insert([0, 0, 0, 0, 0, 0, 3, 137], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170882); -tree.insert([0, 0, 0, 0, 0, 0, 2, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170883); -tree.insert([0, 0, 0, 0, 0, 0, 0, 229], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170885); -tree.insert([0, 0, 0, 0, 0, 0, 2, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170886); -tree.insert([0, 0, 0, 0, 0, 0, 0, 31], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170887); -tree.insert([0, 0, 0, 0, 0, 0, 0, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170888); -tree.insert([0, 0, 0, 0, 0, 0, 2, 91], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170889); -tree.insert([0, 0, 0, 0, 0, 0, 3, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170890); -tree.insert([0, 0, 0, 0, 0, 0, 1, 222], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170891); -tree.insert([0, 0, 0, 0, 0, 0, 1, 136], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170892); -tree.insert([0, 0, 0, 0, 0, 0, 2, 44], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170894); -tree.insert([0, 0, 0, 0, 0, 0, 1, 180], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170896); -tree.insert([0, 0, 0, 0, 0, 0, 0, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170897); -tree.insert([0, 0, 0, 0, 0, 0, 2, 107], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170898); -tree.insert([0, 0, 0, 0, 0, 0, 3, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170899); -tree.insert([0, 0, 0, 0, 0, 0, 3, 135], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170900); -tree.insert([0, 0, 0, 0, 0, 0, 2, 251], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170901); -tree.insert([0, 0, 0, 0, 0, 0, 0, 167], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170902); -tree.insert([0, 0, 0, 0, 0, 0, 3, 32], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170903); -tree.insert([0, 0, 0, 0, 0, 0, 1, 104], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170904); -tree.insert([0, 0, 0, 0, 0, 0, 3, 204], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170905); -tree.insert([0, 0, 0, 0, 0, 0, 1, 232], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170906); -tree.insert([0, 0, 0, 0, 0, 0, 3, 134], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170907); -tree.insert([0, 0, 0, 0, 0, 0, 0, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170908); -tree.insert([0, 0, 0, 0, 0, 0, 2, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170909); -tree.insert([0, 0, 0, 0, 0, 0, 3, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170911); -tree.insert([0, 0, 0, 0, 0, 0, 3, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170914); -tree.insert([0, 0, 0, 0, 0, 0, 2, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170915); -tree.insert([0, 0, 0, 0, 0, 0, 3, 20], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170918); -tree.insert([0, 0, 0, 0, 0, 0, 0, 231], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170919); -tree.insert([0, 0, 0, 0, 0, 0, 2, 213], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170920); -tree.insert([0, 0, 0, 0, 0, 0, 1, 141], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170921); -tree.insert([0, 0, 0, 0, 0, 0, 0, 128], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170922); -tree.insert([0, 0, 0, 0, 0, 0, 0, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170923); -tree.insert([0, 0, 0, 0, 0, 0, 2, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170924); -tree.insert([0, 0, 0, 0, 0, 0, 1, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170926); -tree.insert([0, 0, 0, 0, 0, 0, 3, 173], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170927); -tree.insert([0, 0, 0, 0, 0, 0, 3, 106], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170928); -tree.insert([0, 0, 0, 0, 0, 0, 2, 59], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170929); -tree.insert([0, 0, 0, 0, 0, 0, 1, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170930); -tree.insert([0, 0, 0, 0, 0, 0, 1, 205], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170932); -tree.insert([0, 0, 0, 0, 0, 0, 0, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170933); -tree.insert([0, 0, 0, 0, 0, 0, 1, 51], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170935); -tree.insert([0, 0, 0, 0, 0, 0, 3, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170937); -tree.insert([0, 0, 0, 0, 0, 0, 1, 246], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170938); -tree.insert([0, 0, 0, 0, 0, 0, 3, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170939); -tree.insert([0, 0, 0, 0, 0, 0, 1, 160], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170940); -tree.insert([0, 0, 0, 0, 0, 0, 1, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170943); -tree.insert([0, 0, 0, 0, 0, 0, 1, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170945); -tree.insert([0, 0, 0, 0, 0, 0, 0, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170946); -tree.insert([0, 0, 0, 0, 0, 0, 2, 204], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170947); -tree.insert([0, 0, 0, 0, 0, 0, 2, 216], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170949); -tree.insert([0, 0, 0, 0, 0, 0, 2, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170953); -tree.insert([0, 0, 0, 0, 0, 0, 2, 94], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170956); -tree.insert([0, 0, 0, 0, 0, 0, 1, 128], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170957); -tree.insert([0, 0, 0, 0, 0, 0, 1, 15], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170958); -tree.insert([0, 0, 0, 0, 0, 0, 1, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170959); -tree.insert([0, 0, 0, 0, 0, 0, 1, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170961); -tree.insert([0, 0, 0, 0, 0, 0, 0, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170964); -tree.insert([0, 0, 0, 0, 0, 0, 0, 150], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170965); -tree.insert([0, 0, 0, 0, 0, 0, 1, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170966); -tree.insert([0, 0, 0, 0, 0, 0, 3, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170967); -tree.insert([0, 0, 0, 0, 0, 0, 1, 57], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170968); -tree.insert([0, 0, 0, 0, 0, 0, 2, 188], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170970); -tree.insert([0, 0, 0, 0, 0, 0, 3, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170972); -tree.insert([0, 0, 0, 0, 0, 0, 3, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170973); -tree.insert([0, 0, 0, 0, 0, 0, 3, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170974); -tree.insert([0, 0, 0, 0, 0, 0, 2, 212], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170976); -tree.insert([0, 0, 0, 0, 0, 0, 3, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170978); -tree.insert([0, 0, 0, 0, 0, 0, 1, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170980); -tree.insert([0, 0, 0, 0, 0, 0, 3, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170981); -tree.insert([0, 0, 0, 0, 0, 0, 1, 214], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170982); -tree.insert([0, 0, 0, 0, 0, 0, 0, 19], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170983); -tree.insert([0, 0, 0, 0, 0, 0, 0, 246], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170985); -tree.insert([0, 0, 0, 0, 0, 0, 0, 77], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170986); -tree.insert([0, 0, 0, 0, 0, 0, 3, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170987); -tree.insert([0, 0, 0, 0, 0, 0, 2, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170988); -tree.insert([0, 0, 0, 0, 0, 0, 0, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170989); -tree.insert([0, 0, 0, 0, 0, 0, 2, 127], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170990); -tree.insert([0, 0, 0, 0, 0, 0, 0, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170991); -tree.insert([0, 0, 0, 0, 0, 0, 1, 173], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170993); -tree.insert([0, 0, 0, 0, 0, 0, 3, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170994); -tree.insert([0, 0, 0, 0, 0, 0, 0, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170995); -tree.insert([0, 0, 0, 0, 0, 0, 2, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170996); -tree.insert([0, 0, 0, 0, 0, 0, 1, 38], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170998); -tree.insert([0, 0, 0, 0, 0, 0, 1, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 170999); -tree.insert([0, 0, 0, 0, 0, 0, 0, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171000); -tree.flush_active_memtable(170901)?; -tree.insert([0, 0, 0, 0, 0, 0, 2, 163], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171001); -tree.insert([0, 0, 0, 0, 0, 0, 2, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171003); -tree.insert([0, 0, 0, 0, 0, 0, 3, 86], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171004); -tree.insert([0, 0, 0, 0, 0, 0, 1, 8], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171005); -tree.insert([0, 0, 0, 0, 0, 0, 0, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171006); -tree.insert([0, 0, 0, 0, 0, 0, 3, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171007); -tree.insert([0, 0, 0, 0, 0, 0, 1, 208], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171008); -tree.insert([0, 0, 0, 0, 0, 0, 0, 237], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171010); -tree.insert([0, 0, 0, 0, 0, 0, 1, 190], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171012); -tree.insert([0, 0, 0, 0, 0, 0, 2, 221], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171013); -tree.insert([0, 0, 0, 0, 0, 0, 2, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171014); -tree.insert([0, 0, 0, 0, 0, 0, 1, 231], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171015); -tree.insert([0, 0, 0, 0, 0, 0, 0, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171016); -tree.insert([0, 0, 0, 0, 0, 0, 1, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171017); -tree.insert([0, 0, 0, 0, 0, 0, 2, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171018); -tree.insert([0, 0, 0, 0, 0, 0, 0, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171019); -tree.insert([0, 0, 0, 0, 0, 0, 3, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171022); -tree.insert([0, 0, 0, 0, 0, 0, 0, 235], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171024); -tree.insert([0, 0, 0, 0, 0, 0, 1, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171027); -tree.insert([0, 0, 0, 0, 0, 0, 1, 107], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171034); -tree.insert([0, 0, 0, 0, 0, 0, 1, 233], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171042); -tree.insert([0, 0, 0, 0, 0, 0, 1, 0], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171044); -tree.insert([0, 0, 0, 0, 0, 0, 2, 77], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171045); -tree.insert([0, 0, 0, 0, 0, 0, 0, 253], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171050); -tree.insert([0, 0, 0, 0, 0, 0, 0, 241], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171051); -tree.insert([0, 0, 0, 0, 0, 0, 2, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171052); -tree.insert([0, 0, 0, 0, 0, 0, 0, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171053); -tree.insert([0, 0, 0, 0, 0, 0, 0, 24], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171063); -tree.insert([0, 0, 0, 0, 0, 0, 1, 24], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171065); -tree.insert([0, 0, 0, 0, 0, 0, 3, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171070); -tree.insert([0, 0, 0, 0, 0, 0, 3, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171071); -tree.insert([0, 0, 0, 0, 0, 0, 3, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171075); -tree.insert([0, 0, 0, 0, 0, 0, 3, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171076); -tree.insert([0, 0, 0, 0, 0, 0, 1, 146], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171078); -tree.insert([0, 0, 0, 0, 0, 0, 3, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171080); -tree.insert([0, 0, 0, 0, 0, 0, 0, 233], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171082); -tree.insert([0, 0, 0, 0, 0, 0, 0, 1], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171085); -tree.insert([0, 0, 0, 0, 0, 0, 2, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171088); -tree.insert([0, 0, 0, 0, 0, 0, 0, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171089); -tree.insert([0, 0, 0, 0, 0, 0, 1, 202], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171091); -tree.insert([0, 0, 0, 0, 0, 0, 1, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171092); -tree.insert([0, 0, 0, 0, 0, 0, 2, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171094); -tree.insert([0, 0, 0, 0, 0, 0, 1, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171095); -tree.insert([0, 0, 0, 0, 0, 0, 1, 37], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171097); -tree.insert([0, 0, 0, 0, 0, 0, 2, 191], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171098); -tree.insert([0, 0, 0, 0, 0, 0, 0, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171099); -tree.insert([0, 0, 0, 0, 0, 0, 2, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171103); -tree.insert([0, 0, 0, 0, 0, 0, 1, 237], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171104); -tree.insert([0, 0, 0, 0, 0, 0, 0, 67], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171105); -tree.insert([0, 0, 0, 0, 0, 0, 0, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171110); -tree.insert([0, 0, 0, 0, 0, 0, 2, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171111); -tree.insert([0, 0, 0, 0, 0, 0, 1, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171113); -tree.insert([0, 0, 0, 0, 0, 0, 2, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171114); -tree.insert([0, 0, 0, 0, 0, 0, 1, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171115); -tree.insert([0, 0, 0, 0, 0, 0, 1, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171116); -tree.insert([0, 0, 0, 0, 0, 0, 0, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171117); -tree.insert([0, 0, 0, 0, 0, 0, 2, 249], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171118); -tree.insert([0, 0, 0, 0, 0, 0, 2, 223], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171122); -tree.insert([0, 0, 0, 0, 0, 0, 0, 108], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171133); -tree.insert([0, 0, 0, 0, 0, 0, 1, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171173); -tree.insert([0, 0, 0, 0, 0, 0, 0, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171174); -tree.insert([0, 0, 0, 0, 0, 0, 1, 152], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171175); -tree.insert([0, 0, 0, 0, 0, 0, 2, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171180); -tree.flush_active_memtable(171101)?; -tree.compact(compaction.clone(), 171101)?; -tree.insert([0, 0, 0, 0, 0, 0, 3, 6], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171201); -tree.insert([0, 0, 0, 0, 0, 0, 1, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171202); -tree.insert([0, 0, 0, 0, 0, 0, 3, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171203); -tree.insert([0, 0, 0, 0, 0, 0, 2, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171204); -tree.insert([0, 0, 0, 0, 0, 0, 0, 232], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171205); -tree.insert([0, 0, 0, 0, 0, 0, 3, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171206); -tree.insert([0, 0, 0, 0, 0, 0, 3, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171207); -tree.insert([0, 0, 0, 0, 0, 0, 3, 216], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171208); -tree.insert([0, 0, 0, 0, 0, 0, 1, 161], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171209); -tree.insert([0, 0, 0, 0, 0, 0, 0, 106], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171210); -tree.insert([0, 0, 0, 0, 0, 0, 0, 6], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171211); -tree.insert([0, 0, 0, 0, 0, 0, 2, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171212); -tree.insert([0, 0, 0, 0, 0, 0, 2, 229], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171213); -tree.insert([0, 0, 0, 0, 0, 0, 0, 210], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171214); -tree.insert([0, 0, 0, 0, 0, 0, 0, 65], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171215); -tree.insert([0, 0, 0, 0, 0, 0, 0, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171216); -tree.insert([0, 0, 0, 0, 0, 0, 1, 203], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171217); -tree.insert([0, 0, 0, 0, 0, 0, 2, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171218); -tree.insert([0, 0, 0, 0, 0, 0, 0, 209], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171219); -tree.insert([0, 0, 0, 0, 0, 0, 0, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171220); -tree.insert([0, 0, 0, 0, 0, 0, 3, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171221); -tree.insert([0, 0, 0, 0, 0, 0, 1, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171223); -tree.insert([0, 0, 0, 0, 0, 0, 0, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171224); -tree.insert([0, 0, 0, 0, 0, 0, 1, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171225); -tree.insert([0, 0, 0, 0, 0, 0, 0, 37], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171226); -tree.insert([0, 0, 0, 0, 0, 0, 2, 200], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171227); -tree.insert([0, 0, 0, 0, 0, 0, 3, 200], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171228); -tree.insert([0, 0, 0, 0, 0, 0, 2, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171229); -tree.insert([0, 0, 0, 0, 0, 0, 1, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171230); -tree.insert([0, 0, 0, 0, 0, 0, 2, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171231); -tree.insert([0, 0, 0, 0, 0, 0, 2, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171232); -tree.insert([0, 0, 0, 0, 0, 0, 2, 235], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171233); -tree.insert([0, 0, 0, 0, 0, 0, 0, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171234); -tree.insert([0, 0, 0, 0, 0, 0, 0, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171235); -tree.insert([0, 0, 0, 0, 0, 0, 1, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171236); -tree.insert([0, 0, 0, 0, 0, 0, 1, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171237); -tree.insert([0, 0, 0, 0, 0, 0, 1, 242], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171238); -tree.insert([0, 0, 0, 0, 0, 0, 0, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171240); -tree.insert([0, 0, 0, 0, 0, 0, 1, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171241); -tree.insert([0, 0, 0, 0, 0, 0, 3, 143], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171242); -tree.insert([0, 0, 0, 0, 0, 0, 0, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171243); -tree.insert([0, 0, 0, 0, 0, 0, 3, 37], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171244); -tree.insert([0, 0, 0, 0, 0, 0, 0, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171245); -tree.insert([0, 0, 0, 0, 0, 0, 2, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171246); -tree.insert([0, 0, 0, 0, 0, 0, 3, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171247); -tree.insert([0, 0, 0, 0, 0, 0, 1, 135], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171248); -tree.insert([0, 0, 0, 0, 0, 0, 2, 103], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171249); -tree.insert([0, 0, 0, 0, 0, 0, 1, 3], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171250); -tree.insert([0, 0, 0, 0, 0, 0, 1, 245], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171251); -tree.insert([0, 0, 0, 0, 0, 0, 1, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171252); -tree.insert([0, 0, 0, 0, 0, 0, 0, 206], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171253); -tree.insert([0, 0, 0, 0, 0, 0, 0, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171254); -tree.insert([0, 0, 0, 0, 0, 0, 3, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171255); -tree.insert([0, 0, 0, 0, 0, 0, 2, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171256); -tree.insert([0, 0, 0, 0, 0, 0, 3, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171257); -tree.insert([0, 0, 0, 0, 0, 0, 0, 140], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171258); -tree.insert([0, 0, 0, 0, 0, 0, 2, 59], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171259); -tree.insert([0, 0, 0, 0, 0, 0, 2, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171260); -tree.insert([0, 0, 0, 0, 0, 0, 0, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171262); -tree.insert([0, 0, 0, 0, 0, 0, 1, 184], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171263); -tree.insert([0, 0, 0, 0, 0, 0, 3, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171264); -tree.insert([0, 0, 0, 0, 0, 0, 0, 161], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171265); -tree.insert([0, 0, 0, 0, 0, 0, 2, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171266); -tree.insert([0, 0, 0, 0, 0, 0, 2, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171267); -tree.insert([0, 0, 0, 0, 0, 0, 1, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171268); -tree.insert([0, 0, 0, 0, 0, 0, 3, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171269); -tree.insert([0, 0, 0, 0, 0, 0, 3, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171270); -tree.insert([0, 0, 0, 0, 0, 0, 1, 241], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171271); -tree.insert([0, 0, 0, 0, 0, 0, 1, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171272); -tree.insert([0, 0, 0, 0, 0, 0, 2, 104], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171273); -tree.insert([0, 0, 0, 0, 0, 0, 0, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171274); -tree.insert([0, 0, 0, 0, 0, 0, 3, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171275); -tree.insert([0, 0, 0, 0, 0, 0, 0, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171276); -tree.insert([0, 0, 0, 0, 0, 0, 0, 101], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171277); -tree.insert([0, 0, 0, 0, 0, 0, 0, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171278); -tree.insert([0, 0, 0, 0, 0, 0, 0, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171279); -tree.insert([0, 0, 0, 0, 0, 0, 3, 161], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171280); -tree.insert([0, 0, 0, 0, 0, 0, 1, 122], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171281); -tree.insert([0, 0, 0, 0, 0, 0, 0, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171282); -tree.insert([0, 0, 0, 0, 0, 0, 3, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171283); -tree.insert([0, 0, 0, 0, 0, 0, 0, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171284); -tree.insert([0, 0, 0, 0, 0, 0, 0, 185], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171285); -tree.insert([0, 0, 0, 0, 0, 0, 2, 56], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171286); -tree.insert([0, 0, 0, 0, 0, 0, 0, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171287); -tree.insert([0, 0, 0, 0, 0, 0, 1, 216], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171288); -tree.insert([0, 0, 0, 0, 0, 0, 2, 185], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171289); -tree.insert([0, 0, 0, 0, 0, 0, 0, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171290); -tree.insert([0, 0, 0, 0, 0, 0, 2, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171291); -tree.insert([0, 0, 0, 0, 0, 0, 1, 236], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171292); -tree.insert([0, 0, 0, 0, 0, 0, 0, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171293); -tree.insert([0, 0, 0, 0, 0, 0, 3, 123], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171294); -tree.insert([0, 0, 0, 0, 0, 0, 0, 238], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171295); -tree.insert([0, 0, 0, 0, 0, 0, 0, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171296); -tree.insert([0, 0, 0, 0, 0, 0, 0, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171297); -tree.insert([0, 0, 0, 0, 0, 0, 3, 81], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171298); -tree.insert([0, 0, 0, 0, 0, 0, 0, 76], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171300); -tree.insert([0, 0, 0, 0, 0, 0, 2, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171301); -tree.insert([0, 0, 0, 0, 0, 0, 3, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171302); -tree.insert([0, 0, 0, 0, 0, 0, 3, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171303); -tree.insert([0, 0, 0, 0, 0, 0, 2, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171304); -tree.insert([0, 0, 0, 0, 0, 0, 0, 218], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171305); -tree.insert([0, 0, 0, 0, 0, 0, 3, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171306); -tree.insert([0, 0, 0, 0, 0, 0, 3, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171307); -tree.insert([0, 0, 0, 0, 0, 0, 2, 94], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171308); -tree.insert([0, 0, 0, 0, 0, 0, 0, 207], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171309); -tree.insert([0, 0, 0, 0, 0, 0, 0, 92], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171310); -tree.insert([0, 0, 0, 0, 0, 0, 0, 143], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171311); -tree.insert([0, 0, 0, 0, 0, 0, 0, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171312); -tree.insert([0, 0, 0, 0, 0, 0, 1, 168], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171313); -tree.insert([0, 0, 0, 0, 0, 0, 1, 232], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171314); -tree.insert([0, 0, 0, 0, 0, 0, 2, 122], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171315); -tree.insert([0, 0, 0, 0, 0, 0, 1, 108], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171316); -tree.insert([0, 0, 0, 0, 0, 0, 3, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171317); -tree.insert([0, 0, 0, 0, 0, 0, 0, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171318); -tree.insert([0, 0, 0, 0, 0, 0, 3, 106], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171320); -tree.insert([0, 0, 0, 0, 0, 0, 2, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171321); -tree.insert([0, 0, 0, 0, 0, 0, 3, 106], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171322); -tree.insert([0, 0, 0, 0, 0, 0, 0, 91], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171323); -tree.insert([0, 0, 0, 0, 0, 0, 3, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171324); -tree.insert([0, 0, 0, 0, 0, 0, 0, 73], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171325); -tree.insert([0, 0, 0, 0, 0, 0, 3, 157], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171326); -tree.insert([0, 0, 0, 0, 0, 0, 0, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171327); -tree.insert([0, 0, 0, 0, 0, 0, 3, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171328); -tree.insert([0, 0, 0, 0, 0, 0, 1, 59], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171329); -tree.insert([0, 0, 0, 0, 0, 0, 1, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171330); -tree.insert([0, 0, 0, 0, 0, 0, 1, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171331); -tree.insert([0, 0, 0, 0, 0, 0, 0, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171332); -tree.insert([0, 0, 0, 0, 0, 0, 0, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171333); -tree.insert([0, 0, 0, 0, 0, 0, 3, 136], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171334); -tree.insert([0, 0, 0, 0, 0, 0, 2, 61], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171336); -tree.insert([0, 0, 0, 0, 0, 0, 1, 251], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171337); -tree.insert([0, 0, 0, 0, 0, 0, 1, 243], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171338); -tree.insert([0, 0, 0, 0, 0, 0, 1, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171339); -tree.insert([0, 0, 0, 0, 0, 0, 0, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171340); -tree.insert([0, 0, 0, 0, 0, 0, 3, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171341); -tree.insert([0, 0, 0, 0, 0, 0, 2, 139], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171343); -tree.insert([0, 0, 0, 0, 0, 0, 2, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171344); -tree.insert([0, 0, 0, 0, 0, 0, 3, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171345); -tree.insert([0, 0, 0, 0, 0, 0, 3, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171346); -tree.insert([0, 0, 0, 0, 0, 0, 1, 235], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171347); -tree.insert([0, 0, 0, 0, 0, 0, 0, 245], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171348); -tree.insert([0, 0, 0, 0, 0, 0, 2, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171349); -tree.insert([0, 0, 0, 0, 0, 0, 1, 24], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171350); -tree.insert([0, 0, 0, 0, 0, 0, 2, 253], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171351); -tree.insert([0, 0, 0, 0, 0, 0, 3, 75], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171352); -tree.insert([0, 0, 0, 0, 0, 0, 2, 177], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171353); -tree.insert([0, 0, 0, 0, 0, 0, 1, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171354); -tree.insert([0, 0, 0, 0, 0, 0, 3, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171355); -tree.insert([0, 0, 0, 0, 0, 0, 2, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171356); -tree.insert([0, 0, 0, 0, 0, 0, 1, 61], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171357); -tree.insert([0, 0, 0, 0, 0, 0, 0, 251], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171358); -tree.insert([0, 0, 0, 0, 0, 0, 2, 221], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171359); -tree.insert([0, 0, 0, 0, 0, 0, 1, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171360); -tree.insert([0, 0, 0, 0, 0, 0, 3, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171361); -tree.insert([0, 0, 0, 0, 0, 0, 3, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171362); -tree.insert([0, 0, 0, 0, 0, 0, 1, 31], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171363); -tree.insert([0, 0, 0, 0, 0, 0, 2, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171364); -tree.insert([0, 0, 0, 0, 0, 0, 2, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171365); -tree.insert([0, 0, 0, 0, 0, 0, 2, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171366); -tree.insert([0, 0, 0, 0, 0, 0, 2, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171367); -tree.insert([0, 0, 0, 0, 0, 0, 2, 88], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171368); -tree.insert([0, 0, 0, 0, 0, 0, 0, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171369); -tree.insert([0, 0, 0, 0, 0, 0, 1, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171370); -tree.insert([0, 0, 0, 0, 0, 0, 2, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171371); -tree.insert([0, 0, 0, 0, 0, 0, 3, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171372); -tree.insert([0, 0, 0, 0, 0, 0, 1, 251], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171373); -tree.insert([0, 0, 0, 0, 0, 0, 2, 207], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171374); -tree.insert([0, 0, 0, 0, 0, 0, 1, 127], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171375); -tree.insert([0, 0, 0, 0, 0, 0, 0, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171376); -tree.insert([0, 0, 0, 0, 0, 0, 3, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171377); -tree.insert([0, 0, 0, 0, 0, 0, 3, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171378); -tree.insert([0, 0, 0, 0, 0, 0, 2, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171379); -tree.insert([0, 0, 0, 0, 0, 0, 3, 176], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171380); -tree.insert([0, 0, 0, 0, 0, 0, 3, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171381); -tree.insert([0, 0, 0, 0, 0, 0, 2, 230], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171382); -tree.insert([0, 0, 0, 0, 0, 0, 3, 31], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171383); -tree.insert([0, 0, 0, 0, 0, 0, 0, 129], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171385); -tree.insert([0, 0, 0, 0, 0, 0, 2, 201], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171387); -tree.insert([0, 0, 0, 0, 0, 0, 0, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171388); -tree.insert([0, 0, 0, 0, 0, 0, 0, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171389); -tree.insert([0, 0, 0, 0, 0, 0, 1, 210], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171390); -tree.insert([0, 0, 0, 0, 0, 0, 3, 92], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171391); -tree.insert([0, 0, 0, 0, 0, 0, 3, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171392); -tree.insert([0, 0, 0, 0, 0, 0, 2, 91], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171393); -tree.insert([0, 0, 0, 0, 0, 0, 3, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171394); -tree.insert([0, 0, 0, 0, 0, 0, 1, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171395); -tree.insert([0, 0, 0, 0, 0, 0, 2, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171396); -tree.insert([0, 0, 0, 0, 0, 0, 2, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171397); -tree.insert([0, 0, 0, 0, 0, 0, 1, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171398); -tree.insert([0, 0, 0, 0, 0, 0, 1, 128], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171400); -tree.flush_active_memtable(171301)?; -tree.insert([0, 0, 0, 0, 0, 0, 3, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171401); -tree.insert([0, 0, 0, 0, 0, 0, 3, 2], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171402); -tree.insert([0, 0, 0, 0, 0, 0, 1, 139], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171403); -tree.insert([0, 0, 0, 0, 0, 0, 0, 210], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171404); -tree.insert([0, 0, 0, 0, 0, 0, 0, 183], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171405); -tree.insert([0, 0, 0, 0, 0, 0, 0, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171406); -tree.insert([0, 0, 0, 0, 0, 0, 1, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171407); -tree.insert([0, 0, 0, 0, 0, 0, 2, 249], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171408); -tree.insert([0, 0, 0, 0, 0, 0, 3, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171409); -tree.insert([0, 0, 0, 0, 0, 0, 0, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171410); -tree.insert([0, 0, 0, 0, 0, 0, 1, 240], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171411); -tree.insert([0, 0, 0, 0, 0, 0, 0, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171412); -tree.insert([0, 0, 0, 0, 0, 0, 2, 161], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171413); -tree.insert([0, 0, 0, 0, 0, 0, 0, 219], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171414); -tree.insert([0, 0, 0, 0, 0, 0, 1, 200], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171415); -tree.insert([0, 0, 0, 0, 0, 0, 1, 73], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171417); -tree.insert([0, 0, 0, 0, 0, 0, 0, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171418); -tree.insert([0, 0, 0, 0, 0, 0, 0, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171419); -tree.insert([0, 0, 0, 0, 0, 0, 0, 3], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171420); -tree.insert([0, 0, 0, 0, 0, 0, 1, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171421); -tree.insert([0, 0, 0, 0, 0, 0, 0, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171422); -tree.insert([0, 0, 0, 0, 0, 0, 0, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171425); -tree.insert([0, 0, 0, 0, 0, 0, 0, 152], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171426); -tree.insert([0, 0, 0, 0, 0, 0, 1, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171427); -tree.insert([0, 0, 0, 0, 0, 0, 1, 242], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171428); -tree.insert([0, 0, 0, 0, 0, 0, 2, 177], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171431); -tree.insert([0, 0, 0, 0, 0, 0, 1, 87], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171433); -tree.insert([0, 0, 0, 0, 0, 0, 0, 176], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171434); -tree.insert([0, 0, 0, 0, 0, 0, 0, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171436); -tree.insert([0, 0, 0, 0, 0, 0, 1, 15], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171437); -tree.insert([0, 0, 0, 0, 0, 0, 1, 110], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171438); -tree.insert([0, 0, 0, 0, 0, 0, 1, 197], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171439); -tree.insert([0, 0, 0, 0, 0, 0, 0, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171440); -tree.insert([0, 0, 0, 0, 0, 0, 1, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171443); -tree.insert([0, 0, 0, 0, 0, 0, 1, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171444); -tree.insert([0, 0, 0, 0, 0, 0, 3, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171445); -tree.insert([0, 0, 0, 0, 0, 0, 0, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171446); -tree.insert([0, 0, 0, 0, 0, 0, 3, 31], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171449); -tree.insert([0, 0, 0, 0, 0, 0, 3, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171455); -tree.insert([0, 0, 0, 0, 0, 0, 0, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171456); -tree.insert([0, 0, 0, 0, 0, 0, 2, 157], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171457); -tree.insert([0, 0, 0, 0, 0, 0, 2, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171458); -tree.insert([0, 0, 0, 0, 0, 0, 3, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171459); -tree.insert([0, 0, 0, 0, 0, 0, 3, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171460); -tree.insert([0, 0, 0, 0, 0, 0, 0, 163], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171461); -tree.insert([0, 0, 0, 0, 0, 0, 3, 32], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171462); -tree.insert([0, 0, 0, 0, 0, 0, 2, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171463); -tree.insert([0, 0, 0, 0, 0, 0, 0, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171464); -tree.insert([0, 0, 0, 0, 0, 0, 3, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171465); -tree.insert([0, 0, 0, 0, 0, 0, 2, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171466); -tree.insert([0, 0, 0, 0, 0, 0, 3, 88], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171467); -tree.insert([0, 0, 0, 0, 0, 0, 3, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171468); -tree.insert([0, 0, 0, 0, 0, 0, 1, 148], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171469); -tree.insert([0, 0, 0, 0, 0, 0, 3, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171470); -tree.insert([0, 0, 0, 0, 0, 0, 2, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171471); -tree.insert([0, 0, 0, 0, 0, 0, 1, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171472); -tree.insert([0, 0, 0, 0, 0, 0, 2, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171473); -tree.insert([0, 0, 0, 0, 0, 0, 1, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171474); -tree.insert([0, 0, 0, 0, 0, 0, 0, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171475); -tree.insert([0, 0, 0, 0, 0, 0, 2, 225], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171476); -tree.insert([0, 0, 0, 0, 0, 0, 1, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171478); -tree.insert([0, 0, 0, 0, 0, 0, 0, 134], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171479); -tree.insert([0, 0, 0, 0, 0, 0, 3, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171480); -tree.insert([0, 0, 0, 0, 0, 0, 3, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171481); -tree.insert([0, 0, 0, 0, 0, 0, 0, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171482); -tree.insert([0, 0, 0, 0, 0, 0, 3, 61], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171483); -tree.insert([0, 0, 0, 0, 0, 0, 3, 24], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171484); -tree.insert([0, 0, 0, 0, 0, 0, 3, 141], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171485); -tree.insert([0, 0, 0, 0, 0, 0, 2, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171486); -tree.insert([0, 0, 0, 0, 0, 0, 2, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171488); -tree.insert([0, 0, 0, 0, 0, 0, 0, 255], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171489); -tree.insert([0, 0, 0, 0, 0, 0, 0, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171491); -tree.insert([0, 0, 0, 0, 0, 0, 0, 234], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171492); -tree.insert([0, 0, 0, 0, 0, 0, 2, 130], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171493); -tree.insert([0, 0, 0, 0, 0, 0, 0, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171494); -tree.insert([0, 0, 0, 0, 0, 0, 2, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171495); -tree.insert([0, 0, 0, 0, 0, 0, 3, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171496); -tree.insert([0, 0, 0, 0, 0, 0, 3, 38], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171497); -tree.insert([0, 0, 0, 0, 0, 0, 2, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171498); -tree.insert([0, 0, 0, 0, 0, 0, 1, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171499); -tree.insert([0, 0, 0, 0, 0, 0, 0, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171500); -tree.insert([0, 0, 0, 0, 0, 0, 3, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171502); -tree.insert([0, 0, 0, 0, 0, 0, 0, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171503); -tree.insert([0, 0, 0, 0, 0, 0, 3, 132], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171504); -tree.insert([0, 0, 0, 0, 0, 0, 1, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171505); -tree.insert([0, 0, 0, 0, 0, 0, 0, 20], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171506); -tree.insert([0, 0, 0, 0, 0, 0, 1, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171507); -tree.insert([0, 0, 0, 0, 0, 0, 3, 100], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171508); -tree.insert([0, 0, 0, 0, 0, 0, 3, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171509); -tree.insert([0, 0, 0, 0, 0, 0, 0, 242], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171511); -tree.insert([0, 0, 0, 0, 0, 0, 2, 230], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171512); -tree.insert([0, 0, 0, 0, 0, 0, 3, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171514); -tree.insert([0, 0, 0, 0, 0, 0, 1, 239], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171515); -tree.insert([0, 0, 0, 0, 0, 0, 0, 83], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171516); -tree.insert([0, 0, 0, 0, 0, 0, 3, 72], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171517); -tree.insert([0, 0, 0, 0, 0, 0, 1, 108], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171518); -tree.insert([0, 0, 0, 0, 0, 0, 0, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171519); -tree.insert([0, 0, 0, 0, 0, 0, 2, 239], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171520); -tree.insert([0, 0, 0, 0, 0, 0, 2, 3], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171521); -tree.insert([0, 0, 0, 0, 0, 0, 0, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171522); -tree.insert([0, 0, 0, 0, 0, 0, 2, 215], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171524); -tree.insert([0, 0, 0, 0, 0, 0, 2, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171525); -tree.insert([0, 0, 0, 0, 0, 0, 2, 209], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171526); -tree.insert([0, 0, 0, 0, 0, 0, 0, 227], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171527); -tree.insert([0, 0, 0, 0, 0, 0, 1, 116], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171528); -tree.insert([0, 0, 0, 0, 0, 0, 3, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171530); -tree.insert([0, 0, 0, 0, 0, 0, 1, 214], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171531); -tree.insert([0, 0, 0, 0, 0, 0, 3, 177], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171532); -tree.insert([0, 0, 0, 0, 0, 0, 0, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171533); -tree.insert([0, 0, 0, 0, 0, 0, 1, 79], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171534); -tree.insert([0, 0, 0, 0, 0, 0, 0, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171535); -tree.insert([0, 0, 0, 0, 0, 0, 3, 176], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171536); -tree.insert([0, 0, 0, 0, 0, 0, 3, 86], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171537); -tree.insert([0, 0, 0, 0, 0, 0, 3, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171538); -tree.insert([0, 0, 0, 0, 0, 0, 1, 173], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171539); -tree.insert([0, 0, 0, 0, 0, 0, 0, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171540); -tree.insert([0, 0, 0, 0, 0, 0, 1, 157], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171541); -tree.insert([0, 0, 0, 0, 0, 0, 2, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171542); -tree.insert([0, 0, 0, 0, 0, 0, 2, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171545); -tree.insert([0, 0, 0, 0, 0, 0, 0, 254], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171546); -tree.insert([0, 0, 0, 0, 0, 0, 1, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171548); -tree.insert([0, 0, 0, 0, 0, 0, 3, 123], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171550); -tree.insert([0, 0, 0, 0, 0, 0, 2, 158], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171551); -tree.insert([0, 0, 0, 0, 0, 0, 3, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171552); -tree.insert([0, 0, 0, 0, 0, 0, 1, 149], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171553); -tree.insert([0, 0, 0, 0, 0, 0, 3, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171555); -tree.insert([0, 0, 0, 0, 0, 0, 3, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171556); -tree.insert([0, 0, 0, 0, 0, 0, 2, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171557); -tree.insert([0, 0, 0, 0, 0, 0, 2, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171561); -tree.insert([0, 0, 0, 0, 0, 0, 1, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171563); -tree.insert([0, 0, 0, 0, 0, 0, 0, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171564); -tree.insert([0, 0, 0, 0, 0, 0, 0, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171565); -tree.insert([0, 0, 0, 0, 0, 0, 1, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171568); -tree.insert([0, 0, 0, 0, 0, 0, 0, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171570); -tree.insert([0, 0, 0, 0, 0, 0, 1, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171571); -tree.insert([0, 0, 0, 0, 0, 0, 2, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171572); -tree.insert([0, 0, 0, 0, 0, 0, 0, 165], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171573); -tree.insert([0, 0, 0, 0, 0, 0, 0, 141], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171574); -tree.insert([0, 0, 0, 0, 0, 0, 2, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171575); -tree.insert([0, 0, 0, 0, 0, 0, 2, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171576); -tree.insert([0, 0, 0, 0, 0, 0, 1, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171577); -tree.insert([0, 0, 0, 0, 0, 0, 0, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171578); -tree.insert([0, 0, 0, 0, 0, 0, 2, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171581); -tree.insert([0, 0, 0, 0, 0, 0, 1, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171582); -tree.insert([0, 0, 0, 0, 0, 0, 2, 207], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171583); -tree.insert([0, 0, 0, 0, 0, 0, 1, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171584); -tree.insert([0, 0, 0, 0, 0, 0, 1, 231], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171585); -tree.insert([0, 0, 0, 0, 0, 0, 0, 189], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171586); -tree.insert([0, 0, 0, 0, 0, 0, 1, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171589); -tree.insert([0, 0, 0, 0, 0, 0, 2, 92], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171592); -tree.insert([0, 0, 0, 0, 0, 0, 0, 73], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171593); -tree.insert([0, 0, 0, 0, 0, 0, 0, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171595); -tree.insert([0, 0, 0, 0, 0, 0, 0, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171596); -tree.insert([0, 0, 0, 0, 0, 0, 2, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171597); -tree.insert([0, 0, 0, 0, 0, 0, 0, 32], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171599); -tree.flush_active_memtable(171501)?; -tree.insert([0, 0, 0, 0, 0, 0, 1, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171601); -tree.insert([0, 0, 0, 0, 0, 0, 0, 77], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171602); -tree.insert([0, 0, 0, 0, 0, 0, 0, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171604); -tree.insert([0, 0, 0, 0, 0, 0, 1, 94], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171606); -tree.insert([0, 0, 0, 0, 0, 0, 2, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171607); -tree.insert([0, 0, 0, 0, 0, 0, 3, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171608); -tree.insert([0, 0, 0, 0, 0, 0, 2, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171609); -tree.insert([0, 0, 0, 0, 0, 0, 2, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171613); -tree.insert([0, 0, 0, 0, 0, 0, 2, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171614); -tree.insert([0, 0, 0, 0, 0, 0, 1, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171615); -tree.insert([0, 0, 0, 0, 0, 0, 1, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171616); -tree.insert([0, 0, 0, 0, 0, 0, 0, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171618); -tree.insert([0, 0, 0, 0, 0, 0, 0, 249], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171621); -tree.insert([0, 0, 0, 0, 0, 0, 2, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171622); -tree.insert([0, 0, 0, 0, 0, 0, 1, 156], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171623); -tree.insert([0, 0, 0, 0, 0, 0, 2, 73], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171624); -tree.insert([0, 0, 0, 0, 0, 0, 2, 19], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171628); -tree.insert([0, 0, 0, 0, 0, 0, 2, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171629); -tree.insert([0, 0, 0, 0, 0, 0, 2, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171633); -tree.insert([0, 0, 0, 0, 0, 0, 0, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171635); -tree.insert([0, 0, 0, 0, 0, 0, 2, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171636); -tree.insert([0, 0, 0, 0, 0, 0, 3, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171637); -tree.insert([0, 0, 0, 0, 0, 0, 2, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171638); -tree.insert([0, 0, 0, 0, 0, 0, 0, 206], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171639); -tree.insert([0, 0, 0, 0, 0, 0, 0, 29], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171643); -tree.insert([0, 0, 0, 0, 0, 0, 1, 133], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171644); -tree.insert([0, 0, 0, 0, 0, 0, 0, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171645); -tree.insert([0, 0, 0, 0, 0, 0, 1, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171646); -tree.insert([0, 0, 0, 0, 0, 0, 0, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171647); -tree.insert([0, 0, 0, 0, 0, 0, 0, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171649); -tree.insert([0, 0, 0, 0, 0, 0, 1, 168], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171651); -tree.insert([0, 0, 0, 0, 0, 0, 1, 202], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171653); -tree.insert([0, 0, 0, 0, 0, 0, 1, 229], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171657); -tree.insert([0, 0, 0, 0, 0, 0, 2, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171658); -tree.insert([0, 0, 0, 0, 0, 0, 0, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171659); -tree.insert([0, 0, 0, 0, 0, 0, 1, 6], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171662); -tree.insert([0, 0, 0, 0, 0, 0, 2, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171663); -tree.insert([0, 0, 0, 0, 0, 0, 1, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171666); -tree.insert([0, 0, 0, 0, 0, 0, 2, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171667); -tree.insert([0, 0, 0, 0, 0, 0, 2, 105], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171675); -tree.insert([0, 0, 0, 0, 0, 0, 1, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171678); -tree.insert([0, 0, 0, 0, 0, 0, 0, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171679); -tree.insert([0, 0, 0, 0, 0, 0, 1, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171681); -tree.insert([0, 0, 0, 0, 0, 0, 1, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171686); -tree.insert([0, 0, 0, 0, 0, 0, 0, 81], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171687); -tree.insert([0, 0, 0, 0, 0, 0, 1, 254], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171692); -tree.insert([0, 0, 0, 0, 0, 0, 0, 35], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171695); -tree.insert([0, 0, 0, 0, 0, 0, 1, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171699); -tree.insert([0, 0, 0, 0, 0, 0, 1, 247], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171700); -tree.insert([0, 0, 0, 0, 0, 0, 1, 174], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171703); -tree.insert([0, 0, 0, 0, 0, 0, 0, 215], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171705); -tree.insert([0, 0, 0, 0, 0, 0, 2, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171707); -tree.insert([0, 0, 0, 0, 0, 0, 0, 253], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171717); -tree.insert([0, 0, 0, 0, 0, 0, 1, 70], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171723); -tree.insert([0, 0, 0, 0, 0, 0, 2, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171724); -tree.insert([0, 0, 0, 0, 0, 0, 0, 156], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171725); -tree.insert([0, 0, 0, 0, 0, 0, 1, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171728); -tree.insert([0, 0, 0, 0, 0, 0, 1, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171733); -tree.insert([0, 0, 0, 0, 0, 0, 0, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171735); -tree.insert([0, 0, 0, 0, 0, 0, 0, 72], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171740); -tree.insert([0, 0, 0, 0, 0, 0, 2, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171742); -tree.insert([0, 0, 0, 0, 0, 0, 1, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171746); -tree.insert([0, 0, 0, 0, 0, 0, 0, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171747); -tree.insert([0, 0, 0, 0, 0, 0, 2, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171751); -tree.insert([0, 0, 0, 0, 0, 0, 0, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171752); -tree.insert([0, 0, 0, 0, 0, 0, 1, 26], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171754); -tree.insert([0, 0, 0, 0, 0, 0, 2, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171763); -tree.insert([0, 0, 0, 0, 0, 0, 1, 248], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171764); -tree.insert([0, 0, 0, 0, 0, 0, 2, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171765); -tree.insert([0, 0, 0, 0, 0, 0, 0, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171767); -tree.insert([0, 0, 0, 0, 0, 0, 0, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171768); -tree.insert([0, 0, 0, 0, 0, 0, 0, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171770); -tree.insert([0, 0, 0, 0, 0, 0, 1, 180], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171771); -tree.insert([0, 0, 0, 0, 0, 0, 0, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171772); -tree.insert([0, 0, 0, 0, 0, 0, 2, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171777); -tree.insert([0, 0, 0, 0, 0, 0, 1, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171782); -tree.insert([0, 0, 0, 0, 0, 0, 2, 151], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171785); -tree.insert([0, 0, 0, 0, 0, 0, 1, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171786); -tree.insert([0, 0, 0, 0, 0, 0, 0, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171787); -tree.insert([0, 0, 0, 0, 0, 0, 2, 186], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171788); -tree.insert([0, 0, 0, 0, 0, 0, 2, 134], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171792); -tree.insert([0, 0, 0, 0, 0, 0, 2, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171793); -tree.insert([0, 0, 0, 0, 0, 0, 1, 123], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171794); -tree.insert([0, 0, 0, 0, 0, 0, 1, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171797); -tree.insert([0, 0, 0, 0, 0, 0, 1, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171799); -tree.flush_active_memtable(171701)?; -tree.insert([0, 0, 0, 0, 0, 0, 1, 8], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171805); -tree.insert([0, 0, 0, 0, 0, 0, 2, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171806); -tree.insert([0, 0, 0, 0, 0, 0, 2, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171809); -tree.insert([0, 0, 0, 0, 0, 0, 2, 102], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171810); -tree.insert([0, 0, 0, 0, 0, 0, 0, 228], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171811); -tree.insert([0, 0, 0, 0, 0, 0, 1, 67], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171812); -tree.insert([0, 0, 0, 0, 0, 0, 1, 189], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171813); -tree.insert([0, 0, 0, 0, 0, 0, 2, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171814); -tree.insert([0, 0, 0, 0, 0, 0, 1, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171815); -tree.insert([0, 0, 0, 0, 0, 0, 0, 204], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171819); -tree.insert([0, 0, 0, 0, 0, 0, 1, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171821); -tree.insert([0, 0, 0, 0, 0, 0, 1, 212], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171823); -tree.insert([0, 0, 0, 0, 0, 0, 2, 164], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171824); -tree.insert([0, 0, 0, 0, 0, 0, 1, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171825); -tree.insert([0, 0, 0, 0, 0, 0, 1, 104], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171831); -tree.insert([0, 0, 0, 0, 0, 0, 1, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171833); -tree.insert([0, 0, 0, 0, 0, 0, 0, 237], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171834); -tree.insert([0, 0, 0, 0, 0, 0, 2, 184], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171836); -tree.insert([0, 0, 0, 0, 0, 0, 2, 173], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171837); -tree.insert([0, 0, 0, 0, 0, 0, 2, 144], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171838); -tree.insert([0, 0, 0, 0, 0, 0, 2, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171848); -tree.insert([0, 0, 0, 0, 0, 0, 1, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171850); -tree.insert([0, 0, 0, 0, 0, 0, 2, 168], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171856); -tree.insert([0, 0, 0, 0, 0, 0, 1, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171862); -tree.insert([0, 0, 0, 0, 0, 0, 0, 203], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171863); -tree.insert([0, 0, 0, 0, 0, 0, 0, 105], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171865); -tree.insert([0, 0, 0, 0, 0, 0, 2, 116], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171866); -tree.insert([0, 0, 0, 0, 0, 0, 1, 164], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171868); -tree.insert([0, 0, 0, 0, 0, 0, 1, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171873); -tree.insert([0, 0, 0, 0, 0, 0, 0, 247], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171875); -tree.insert([0, 0, 0, 0, 0, 0, 1, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171877); -tree.insert([0, 0, 0, 0, 0, 0, 0, 190], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171882); -tree.insert([0, 0, 0, 0, 0, 0, 0, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171885); -tree.insert([0, 0, 0, 0, 0, 0, 1, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171890); -tree.insert([0, 0, 0, 0, 0, 0, 2, 66], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171891); -tree.insert([0, 0, 0, 0, 0, 0, 1, 132], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171892); -tree.insert([0, 0, 0, 0, 0, 0, 0, 36], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171895); -tree.insert([0, 0, 0, 0, 0, 0, 0, 122], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171898); -tree.insert([0, 0, 0, 0, 0, 0, 0, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171899); -tree.insert([0, 0, 0, 0, 0, 0, 0, 62], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171901); -tree.insert([0, 0, 0, 0, 0, 0, 2, 129], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171902); -tree.insert([0, 0, 0, 0, 0, 0, 0, 139], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171905); -tree.insert([0, 0, 0, 0, 0, 0, 1, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171907); -tree.insert([0, 0, 0, 0, 0, 0, 0, 145], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171908); -tree.insert([0, 0, 0, 0, 0, 0, 1, 20], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171911); -tree.insert([0, 0, 0, 0, 0, 0, 2, 100], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171917); -tree.insert([0, 0, 0, 0, 0, 0, 0, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171918); -tree.insert([0, 0, 0, 0, 0, 0, 1, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171921); -tree.insert([0, 0, 0, 0, 0, 0, 2, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171922); -tree.insert([0, 0, 0, 0, 0, 0, 2, 149], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171925); -tree.insert([0, 0, 0, 0, 0, 0, 1, 246], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171926); -tree.insert([0, 0, 0, 0, 0, 0, 1, 95], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171928); -tree.insert([0, 0, 0, 0, 0, 0, 0, 76], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171931); -tree.insert([0, 0, 0, 0, 0, 0, 1, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171936); -tree.insert([0, 0, 0, 0, 0, 0, 1, 152], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171937); -tree.insert([0, 0, 0, 0, 0, 0, 1, 158], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171947); -tree.insert([0, 0, 0, 0, 0, 0, 2, 51], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171948); -tree.insert([0, 0, 0, 0, 0, 0, 0, 51], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171950); -tree.insert([0, 0, 0, 0, 0, 0, 0, 144], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171958); -tree.insert([0, 0, 0, 0, 0, 0, 0, 244], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171959); -tree.insert([0, 0, 0, 0, 0, 0, 1, 109], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171963); -tree.insert([0, 0, 0, 0, 0, 0, 0, 59], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171965); -tree.insert([0, 0, 0, 0, 0, 0, 0, 76], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171975); -tree.insert([0, 0, 0, 0, 0, 0, 0, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171976); -tree.insert([0, 0, 0, 0, 0, 0, 1, 163], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171977); -tree.insert([0, 0, 0, 0, 0, 0, 1, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171985); -tree.insert([0, 0, 0, 0, 0, 0, 1, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171987); -tree.insert([0, 0, 0, 0, 0, 0, 1, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171993); -tree.insert([0, 0, 0, 0, 0, 0, 1, 208], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 171997); -tree.flush_active_memtable(171901)?; -tree.compact(compaction.clone(), 171901)?; - - -tree.insert([0, 0, 0, 0, 0, 0, 3, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172007); -tree.insert([0, 0, 0, 0, 0, 0, 1, 37], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172008); -tree.insert([0, 0, 0, 0, 0, 0, 3, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172009); -tree.insert([0, 0, 0, 0, 0, 0, 3, 14], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172010); -tree.insert([0, 0, 0, 0, 0, 0, 2, 39], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172011); -tree.insert([0, 0, 0, 0, 0, 0, 2, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172012); -tree.insert([0, 0, 0, 0, 0, 0, 2, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172013); -tree.insert([0, 0, 0, 0, 0, 0, 0, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172014); -tree.insert([0, 0, 0, 0, 0, 0, 1, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172015); -tree.insert([0, 0, 0, 0, 0, 0, 2, 57], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172016); -tree.insert([0, 0, 0, 0, 0, 0, 0, 127], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172017); -tree.insert([0, 0, 0, 0, 0, 0, 1, 72], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172018); -tree.insert([0, 0, 0, 0, 0, 0, 3, 0], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172020); -tree.insert([0, 0, 0, 0, 0, 0, 1, 213], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172023); -tree.insert([0, 0, 0, 0, 0, 0, 0, 51], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172024); -tree.insert([0, 0, 0, 0, 0, 0, 2, 139], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172025); -tree.insert([0, 0, 0, 0, 0, 0, 1, 235], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172026); -tree.insert([0, 0, 0, 0, 0, 0, 2, 160], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172027); -tree.insert([0, 0, 0, 0, 0, 0, 2, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172028); -tree.insert([0, 0, 0, 0, 0, 0, 1, 194], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172029); -tree.insert([0, 0, 0, 0, 0, 0, 1, 105], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172031); -tree.insert([0, 0, 0, 0, 0, 0, 1, 59], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172033); -tree.insert([0, 0, 0, 0, 0, 0, 2, 85], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172035); -tree.insert([0, 0, 0, 0, 0, 0, 1, 19], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172036); -tree.insert([0, 0, 0, 0, 0, 0, 2, 29], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172037); -tree.insert([0, 0, 0, 0, 0, 0, 2, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172038); -tree.insert([0, 0, 0, 0, 0, 0, 1, 18], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172039); -tree.insert([0, 0, 0, 0, 0, 0, 0, 0], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172043); -tree.insert([0, 0, 0, 0, 0, 0, 0, 144], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172044); -tree.insert([0, 0, 0, 0, 0, 0, 1, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172045); -tree.insert([0, 0, 0, 0, 0, 0, 1, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172046); -tree.insert([0, 0, 0, 0, 0, 0, 3, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172047); -tree.insert([0, 0, 0, 0, 0, 0, 2, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172048); -tree.insert([0, 0, 0, 0, 0, 0, 1, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172049); -tree.insert([0, 0, 0, 0, 0, 0, 2, 103], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172050); -tree.insert([0, 0, 0, 0, 0, 0, 1, 231], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172051); -tree.insert([0, 0, 0, 0, 0, 0, 2, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172052); -tree.insert([0, 0, 0, 0, 0, 0, 3, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172053); -tree.insert([0, 0, 0, 0, 0, 0, 1, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172054); -tree.insert([0, 0, 0, 0, 0, 0, 1, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172056); -tree.insert([0, 0, 0, 0, 0, 0, 1, 184], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172057); -tree.insert([0, 0, 0, 0, 0, 0, 1, 212], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172058); -tree.insert([0, 0, 0, 0, 0, 0, 0, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172059); -tree.insert([0, 0, 0, 0, 0, 0, 1, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172061); -tree.insert([0, 0, 0, 0, 0, 0, 1, 173], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172062); -tree.insert([0, 0, 0, 0, 0, 0, 1, 237], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172064); -tree.insert([0, 0, 0, 0, 0, 0, 1, 25], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172065); -tree.insert([0, 0, 0, 0, 0, 0, 1, 125], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172066); -tree.insert([0, 0, 0, 0, 0, 0, 2, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172067); -tree.insert([0, 0, 0, 0, 0, 0, 1, 245], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172068); -tree.insert([0, 0, 0, 0, 0, 0, 1, 35], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172069); -tree.insert([0, 0, 0, 0, 0, 0, 3, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172070); -tree.insert([0, 0, 0, 0, 0, 0, 3, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172071); -tree.insert([0, 0, 0, 0, 0, 0, 3, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172072); -tree.insert([0, 0, 0, 0, 0, 0, 2, 74], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172073); -tree.insert([0, 0, 0, 0, 0, 0, 2, 15], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172074); -tree.insert([0, 0, 0, 0, 0, 0, 2, 109], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172075); -tree.insert([0, 0, 0, 0, 0, 0, 1, 39], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172077); -tree.insert([0, 0, 0, 0, 0, 0, 2, 221], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172078); -tree.insert([0, 0, 0, 0, 0, 0, 2, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172081); -tree.insert([0, 0, 0, 0, 0, 0, 0, 180], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172082); -tree.insert([0, 0, 0, 0, 0, 0, 2, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172083); -tree.insert([0, 0, 0, 0, 0, 0, 0, 223], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172085); -tree.insert([0, 0, 0, 0, 0, 0, 3, 109], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172087); -tree.insert([0, 0, 0, 0, 0, 0, 2, 175], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172088); -tree.insert([0, 0, 0, 0, 0, 0, 2, 228], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172089); -tree.insert([0, 0, 0, 0, 0, 0, 0, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172090); -tree.insert([0, 0, 0, 0, 0, 0, 0, 56], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172091); -tree.insert([0, 0, 0, 0, 0, 0, 0, 157], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172092); -tree.insert([0, 0, 0, 0, 0, 0, 0, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172093); -tree.insert([0, 0, 0, 0, 0, 0, 0, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172094); -tree.insert([0, 0, 0, 0, 0, 0, 0, 76], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172095); -tree.insert([0, 0, 0, 0, 0, 0, 2, 253], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172096); -tree.insert([0, 0, 0, 0, 0, 0, 0, 210], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172097); -tree.insert([0, 0, 0, 0, 0, 0, 3, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172098); -tree.insert([0, 0, 0, 0, 0, 0, 2, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172101); -tree.insert([0, 0, 0, 0, 0, 0, 2, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172102); -tree.insert([0, 0, 0, 0, 0, 0, 0, 85], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172104); -tree.insert([0, 0, 0, 0, 0, 0, 2, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172105); -tree.insert([0, 0, 0, 0, 0, 0, 2, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172108); -tree.insert([0, 0, 0, 0, 0, 0, 1, 249], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172110); -tree.insert([0, 0, 0, 0, 0, 0, 1, 243], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172111); -tree.insert([0, 0, 0, 0, 0, 0, 1, 48], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172112); -tree.insert([0, 0, 0, 0, 0, 0, 1, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172113); -tree.insert([0, 0, 0, 0, 0, 0, 2, 121], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172114); -tree.insert([0, 0, 0, 0, 0, 0, 1, 2], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172116); -tree.insert([0, 0, 0, 0, 0, 0, 0, 8], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172117); -tree.insert([0, 0, 0, 0, 0, 0, 1, 128], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172118); -tree.insert([0, 0, 0, 0, 0, 0, 1, 104], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172119); -tree.insert([0, 0, 0, 0, 0, 0, 0, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172122); -tree.insert([0, 0, 0, 0, 0, 0, 0, 122], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172123); -tree.insert([0, 0, 0, 0, 0, 0, 0, 123], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172124); -tree.insert([0, 0, 0, 0, 0, 0, 2, 140], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172125); -tree.insert([0, 0, 0, 0, 0, 0, 2, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172126); -tree.insert([0, 0, 0, 0, 0, 0, 0, 235], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172127); -tree.insert([0, 0, 0, 0, 0, 0, 1, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172128); -tree.insert([0, 0, 0, 0, 0, 0, 1, 149], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172149); -tree.insert([0, 0, 0, 0, 0, 0, 0, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172151); -tree.insert([0, 0, 0, 0, 0, 0, 1, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172152); -tree.insert([0, 0, 0, 0, 0, 0, 0, 64], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172153); -tree.insert([0, 0, 0, 0, 0, 0, 0, 161], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172157); -tree.insert([0, 0, 0, 0, 0, 0, 1, 200], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172158); -tree.insert([0, 0, 0, 0, 0, 0, 1, 234], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172163); -tree.insert([0, 0, 0, 0, 0, 0, 1, 196], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172164); -tree.insert([0, 0, 0, 0, 0, 0, 1, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172167); -tree.flush_active_memtable(172101)?; -tree.compact(compaction.clone(), 172101)?; -tree.insert([0, 0, 0, 0, 0, 0, 0, 219], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172222); -tree.insert([0, 0, 0, 0, 0, 0, 0, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172225); -tree.insert([0, 0, 0, 0, 0, 0, 2, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172226); -tree.insert([0, 0, 0, 0, 0, 0, 0, 108], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172227); -tree.insert([0, 0, 0, 0, 0, 0, 0, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172229); -tree.insert([0, 0, 0, 0, 0, 0, 0, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172231); -tree.insert([0, 0, 0, 0, 0, 0, 1, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172233); -tree.insert([0, 0, 0, 0, 0, 0, 2, 100], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172235); -tree.insert([0, 0, 0, 0, 0, 0, 3, 40], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172237); -tree.insert([0, 0, 0, 0, 0, 0, 3, 39], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172238); -tree.insert([0, 0, 0, 0, 0, 0, 2, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172239); -tree.insert([0, 0, 0, 0, 0, 0, 1, 172], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172240); -tree.insert([0, 0, 0, 0, 0, 0, 0, 143], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172242); -tree.insert([0, 0, 0, 0, 0, 0, 1, 101], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172245); -tree.insert([0, 0, 0, 0, 0, 0, 0, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172247); -tree.insert([0, 0, 0, 0, 0, 0, 0, 88], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172251); -tree.insert([0, 0, 0, 0, 0, 0, 0, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172252); -tree.insert([0, 0, 0, 0, 0, 0, 2, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172253); -tree.insert([0, 0, 0, 0, 0, 0, 1, 233], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172257); -tree.insert([0, 0, 0, 0, 0, 0, 0, 229], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172259); -tree.insert([0, 0, 0, 0, 0, 0, 1, 32], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172260); -tree.insert([0, 0, 0, 0, 0, 0, 1, 211], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172261); -tree.insert([0, 0, 0, 0, 0, 0, 2, 69], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172262); -tree.insert([0, 0, 0, 0, 0, 0, 2, 86], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172263); -tree.insert([0, 0, 0, 0, 0, 0, 2, 56], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172264); -tree.insert([0, 0, 0, 0, 0, 0, 2, 190], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172265); -tree.insert([0, 0, 0, 0, 0, 0, 2, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172270); -tree.insert([0, 0, 0, 0, 0, 0, 2, 21], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172271); -tree.insert([0, 0, 0, 0, 0, 0, 1, 118], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172273); -tree.insert([0, 0, 0, 0, 0, 0, 1, 193], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172274); -tree.insert([0, 0, 0, 0, 0, 0, 0, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172275); -tree.insert([0, 0, 0, 0, 0, 0, 2, 200], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172276); -tree.insert([0, 0, 0, 0, 0, 0, 0, 225], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172278); -tree.insert([0, 0, 0, 0, 0, 0, 1, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172279); -tree.insert([0, 0, 0, 0, 0, 0, 0, 129], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172280); -tree.insert([0, 0, 0, 0, 0, 0, 0, 226], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172281); -tree.insert([0, 0, 0, 0, 0, 0, 2, 9], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172286); -tree.insert([0, 0, 0, 0, 0, 0, 0, 50], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172289); -tree.insert([0, 0, 0, 0, 0, 0, 2, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172290); -tree.insert([0, 0, 0, 0, 0, 0, 0, 120], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172291); -tree.insert([0, 0, 0, 0, 0, 0, 1, 239], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172292); -tree.insert([0, 0, 0, 0, 0, 0, 0, 61], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172293); -tree.insert([0, 0, 0, 0, 0, 0, 0, 246], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172294); -tree.insert([0, 0, 0, 0, 0, 0, 0, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172296); -tree.insert([0, 0, 0, 0, 0, 0, 0, 68], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172297); -tree.insert([0, 0, 0, 0, 0, 0, 3, 18], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172298); -tree.insert([0, 0, 0, 0, 0, 0, 0, 131], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172299); -tree.insert([0, 0, 0, 0, 0, 0, 2, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172301); -tree.insert([0, 0, 0, 0, 0, 0, 0, 142], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172304); -tree.insert([0, 0, 0, 0, 0, 0, 2, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172305); -tree.insert([0, 0, 0, 0, 0, 0, 1, 42], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172307); -tree.insert([0, 0, 0, 0, 0, 0, 0, 128], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172308); -tree.insert([0, 0, 0, 0, 0, 0, 3, 36], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172310); -tree.insert([0, 0, 0, 0, 0, 0, 0, 184], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172311); -tree.insert([0, 0, 0, 0, 0, 0, 1, 182], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172313); -tree.insert([0, 0, 0, 0, 0, 0, 1, 5], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172315); -tree.insert([0, 0, 0, 0, 0, 0, 2, 198], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172317); -tree.insert([0, 0, 0, 0, 0, 0, 0, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172318); -tree.insert([0, 0, 0, 0, 0, 0, 0, 39], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172321); -tree.insert([0, 0, 0, 0, 0, 0, 2, 36], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172323); -tree.insert([0, 0, 0, 0, 0, 0, 0, 192], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172324); -tree.insert([0, 0, 0, 0, 0, 0, 1, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172327); -tree.insert([0, 0, 0, 0, 0, 0, 1, 181], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172328); -tree.insert([0, 0, 0, 0, 0, 0, 0, 116], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172331); -tree.insert([0, 0, 0, 0, 0, 0, 0, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172337); -tree.insert([0, 0, 0, 0, 0, 0, 1, 30], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172338); -tree.insert([0, 0, 0, 0, 0, 0, 0, 243], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172340); -tree.insert([0, 0, 0, 0, 0, 0, 0, 90], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172342); -tree.insert([0, 0, 0, 0, 0, 0, 0, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172343); -tree.insert([0, 0, 0, 0, 0, 0, 1, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172350); -tree.insert([0, 0, 0, 0, 0, 0, 0, 240], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172352); -tree.insert([0, 0, 0, 0, 0, 0, 2, 8], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172354); -tree.insert([0, 0, 0, 0, 0, 0, 2, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172355); -tree.insert([0, 0, 0, 0, 0, 0, 2, 87], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172357); -tree.insert([0, 0, 0, 0, 0, 0, 1, 127], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172358); -tree.insert([0, 0, 0, 0, 0, 0, 0, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172359); -tree.insert([0, 0, 0, 0, 0, 0, 2, 115], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172360); -tree.insert([0, 0, 0, 0, 0, 0, 2, 126], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172362); -tree.insert([0, 0, 0, 0, 0, 0, 2, 105], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172365); -tree.insert([0, 0, 0, 0, 0, 0, 2, 191], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172366); -tree.insert([0, 0, 0, 0, 0, 0, 1, 209], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172367); -tree.insert([0, 0, 0, 0, 0, 0, 0, 156], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172368); -tree.insert([0, 0, 0, 0, 0, 0, 2, 208], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172370); -tree.insert([0, 0, 0, 0, 0, 0, 2, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172371); -tree.insert([0, 0, 0, 0, 0, 0, 1, 87], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172372); -tree.insert([0, 0, 0, 0, 0, 0, 1, 44], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172373); -tree.insert([0, 0, 0, 0, 0, 0, 0, 232], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172374); -tree.insert([0, 0, 0, 0, 0, 0, 2, 47], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172376); -tree.insert([0, 0, 0, 0, 0, 0, 1, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172378); -tree.insert([0, 0, 0, 0, 0, 0, 2, 150], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172380); -tree.insert([0, 0, 0, 0, 0, 0, 2, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172381); -tree.insert([0, 0, 0, 0, 0, 0, 1, 53], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172383); -tree.insert([0, 0, 0, 0, 0, 0, 1, 81], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172387); -tree.insert([0, 0, 0, 0, 0, 0, 1, 227], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172390); -tree.insert([0, 0, 0, 0, 0, 0, 0, 38], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172392); -tree.insert([0, 0, 0, 0, 0, 0, 2, 162], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172393); -tree.insert([0, 0, 0, 0, 0, 0, 1, 65], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172394); -tree.insert([0, 0, 0, 0, 0, 0, 1, 255], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172397); -tree.insert([0, 0, 0, 0, 0, 0, 0, 95], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172398); -tree.insert([0, 0, 0, 0, 0, 0, 3, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172399); -tree.insert([0, 0, 0, 0, 0, 0, 0, 66], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172400); -tree.flush_active_memtable(172301)?; -tree.insert([0, 0, 0, 0, 0, 0, 1, 27], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172402); -tree.insert([0, 0, 0, 0, 0, 0, 0, 187], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172404); -tree.insert([0, 0, 0, 0, 0, 0, 2, 211], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172408); -tree.insert([0, 0, 0, 0, 0, 0, 1, 216], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172409); -tree.insert([0, 0, 0, 0, 0, 0, 0, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172414); -tree.insert([0, 0, 0, 0, 0, 0, 0, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172416); -tree.insert([0, 0, 0, 0, 0, 0, 3, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172417); -tree.insert([0, 0, 0, 0, 0, 0, 1, 176], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172418); -tree.insert([0, 0, 0, 0, 0, 0, 0, 215], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172420); -tree.insert([0, 0, 0, 0, 0, 0, 1, 223], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172423); -tree.insert([0, 0, 0, 0, 0, 0, 0, 221], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172424); -tree.insert([0, 0, 0, 0, 0, 0, 0, 54], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172425); -tree.insert([0, 0, 0, 0, 0, 0, 0, 166], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172426); -tree.insert([0, 0, 0, 0, 0, 0, 1, 167], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172427); -tree.insert([0, 0, 0, 0, 0, 0, 2, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172428); -tree.insert([0, 0, 0, 0, 0, 0, 2, 217], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172429); -tree.insert([0, 0, 0, 0, 0, 0, 1, 56], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172432); -tree.insert([0, 0, 0, 0, 0, 0, 0, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172433); -tree.insert([0, 0, 0, 0, 0, 0, 2, 89], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172434); -tree.insert([0, 0, 0, 0, 0, 0, 1, 96], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172435); -tree.insert([0, 0, 0, 0, 0, 0, 1, 58], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172436); -tree.insert([0, 0, 0, 0, 0, 0, 2, 88], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172438); -tree.insert([0, 0, 0, 0, 0, 0, 0, 188], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172439); -tree.insert([0, 0, 0, 0, 0, 0, 1, 99], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172440); -tree.insert([0, 0, 0, 0, 0, 0, 2, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172441); -tree.insert([0, 0, 0, 0, 0, 0, 1, 83], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172442); -tree.insert([0, 0, 0, 0, 0, 0, 2, 49], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172443); -tree.insert([0, 0, 0, 0, 0, 0, 1, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172444); -tree.insert([0, 0, 0, 0, 0, 0, 0, 211], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172445); -tree.insert([0, 0, 0, 0, 0, 0, 0, 227], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172449); -tree.insert([0, 0, 0, 0, 0, 0, 3, 22], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172450); -tree.insert([0, 0, 0, 0, 0, 0, 0, 230], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172451); -tree.insert([0, 0, 0, 0, 0, 0, 1, 112], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172455); -tree.insert([0, 0, 0, 0, 0, 0, 0, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172457); -tree.insert([0, 0, 0, 0, 0, 0, 2, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172458); -tree.insert([0, 0, 0, 0, 0, 0, 1, 124], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172461); -tree.insert([0, 0, 0, 0, 0, 0, 1, 111], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172462); -tree.insert([0, 0, 0, 0, 0, 0, 0, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172463); -tree.insert([0, 0, 0, 0, 0, 0, 0, 253], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172465); -tree.insert([0, 0, 0, 0, 0, 0, 1, 8], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172466); -tree.insert([0, 0, 0, 0, 0, 0, 1, 85], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172470); -tree.insert([0, 0, 0, 0, 0, 0, 1, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172471); -tree.insert([0, 0, 0, 0, 0, 0, 1, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172472); -tree.insert([0, 0, 0, 0, 0, 0, 1, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172479); -tree.insert([0, 0, 0, 0, 0, 0, 1, 138], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172486); -tree.insert([0, 0, 0, 0, 0, 0, 0, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172488); -tree.insert([0, 0, 0, 0, 0, 0, 2, 41], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172489); -tree.insert([0, 0, 0, 0, 0, 0, 1, 224], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172490); -tree.insert([0, 0, 0, 0, 0, 0, 1, 228], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172491); -tree.insert([0, 0, 0, 0, 0, 0, 2, 35], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172493); -tree.insert([0, 0, 0, 0, 0, 0, 0, 75], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172494); -tree.insert([0, 0, 0, 0, 0, 0, 1, 183], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172495); -tree.insert([0, 0, 0, 0, 0, 0, 1, 20], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172496); -tree.insert([0, 0, 0, 0, 0, 0, 2, 243], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172497); -tree.insert([0, 0, 0, 0, 0, 0, 3, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172498); -tree.insert([0, 0, 0, 0, 0, 0, 3, 55], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172508); -tree.insert([0, 0, 0, 0, 0, 0, 1, 195], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172510); -tree.insert([0, 0, 0, 0, 0, 0, 2, 222], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172511); -tree.insert([0, 0, 0, 0, 0, 0, 1, 97], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172512); -tree.insert([0, 0, 0, 0, 0, 0, 0, 134], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172513); -tree.insert([0, 0, 0, 0, 0, 0, 0, 206], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172514); -tree.insert([0, 0, 0, 0, 0, 0, 1, 236], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172516); -tree.insert([0, 0, 0, 0, 0, 0, 3, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172519); -tree.insert([0, 0, 0, 0, 0, 0, 2, 7], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172523); -tree.insert([0, 0, 0, 0, 0, 0, 2, 13], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172525); -tree.insert([0, 0, 0, 0, 0, 0, 1, 201], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172527); -tree.insert([0, 0, 0, 0, 0, 0, 1, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172528); -tree.insert([0, 0, 0, 0, 0, 0, 1, 156], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172529); -tree.insert([0, 0, 0, 0, 0, 0, 1, 246], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172535); -tree.insert([0, 0, 0, 0, 0, 0, 0, 78], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172536); -tree.insert([0, 0, 0, 0, 0, 0, 1, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172537); -tree.insert([0, 0, 0, 0, 0, 0, 1, 62], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172538); -tree.insert([0, 0, 0, 0, 0, 0, 0, 94], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172541); -tree.insert([0, 0, 0, 0, 0, 0, 2, 71], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172542); -tree.insert([0, 0, 0, 0, 0, 0, 1, 145], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172547); -tree.insert([0, 0, 0, 0, 0, 0, 2, 178], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172548); -tree.insert([0, 0, 0, 0, 0, 0, 0, 252], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172549); -tree.insert([0, 0, 0, 0, 0, 0, 2, 154], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172551); -tree.insert([0, 0, 0, 0, 0, 0, 1, 140], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172554); -tree.insert([0, 0, 0, 0, 0, 0, 2, 98], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172555); -tree.insert([0, 0, 0, 0, 0, 0, 0, 24], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172556); -tree.insert([0, 0, 0, 0, 0, 0, 2, 159], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172558); -tree.insert([0, 0, 0, 0, 0, 0, 2, 220], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172559); -tree.insert([0, 0, 0, 0, 0, 0, 3, 43], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172560); -tree.insert([0, 0, 0, 0, 0, 0, 0, 249], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172562); -tree.insert([0, 0, 0, 0, 0, 0, 0, 11], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172563); -tree.insert([0, 0, 0, 0, 0, 0, 3, 19], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172566); -tree.insert([0, 0, 0, 0, 0, 0, 1, 36], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172567); -tree.insert([0, 0, 0, 0, 0, 0, 0, 19], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172570); -tree.insert([0, 0, 0, 0, 0, 0, 2, 171], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172574); -tree.insert([0, 0, 0, 0, 0, 0, 0, 222], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172579); -tree.insert([0, 0, 0, 0, 0, 0, 0, 201], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172582); -tree.insert([0, 0, 0, 0, 0, 0, 1, 119], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172584); -tree.insert([0, 0, 0, 0, 0, 0, 2, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172586); -tree.insert([0, 0, 0, 0, 0, 0, 1, 130], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172587); -tree.insert([0, 0, 0, 0, 0, 0, 1, 17], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172591); -tree.insert([0, 0, 0, 0, 0, 0, 1, 91], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172595); -tree.insert([0, 0, 0, 0, 0, 0, 0, 33], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172599); -tree.flush_active_memtable(172501)?; -tree.insert([0, 0, 0, 0, 0, 0, 0, 114], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172602); -tree.insert([0, 0, 0, 0, 0, 0, 1, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172605); -tree.insert([0, 0, 0, 0, 0, 0, 0, 100], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172609); -tree.insert([0, 0, 0, 0, 0, 0, 0, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172610); -tree.insert([0, 0, 0, 0, 0, 0, 2, 117], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172614); -tree.insert([0, 0, 0, 0, 0, 0, 2, 52], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172615); -tree.insert([0, 0, 0, 0, 0, 0, 3, 35], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172616); -tree.insert([0, 0, 0, 0, 0, 0, 0, 231], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172619); -tree.insert([0, 0, 0, 0, 0, 0, 0, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172625); -tree.insert([0, 0, 0, 0, 0, 0, 1, 147], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172626); -tree.insert([0, 0, 0, 0, 0, 0, 3, 3], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172627); -tree.insert([0, 0, 0, 0, 0, 0, 1, 247], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172628); -tree.insert([0, 0, 0, 0, 0, 0, 1, 190], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172631); -tree.insert([0, 0, 0, 0, 0, 0, 1, 164], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172632); -tree.insert([0, 0, 0, 0, 0, 0, 1, 137], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172633); -tree.insert([0, 0, 0, 0, 0, 0, 1, 168], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172634); -tree.insert([0, 0, 0, 0, 0, 0, 3, 12], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172637); -tree.insert([0, 0, 0, 0, 0, 0, 2, 183], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172638); -tree.insert([0, 0, 0, 0, 0, 0, 0, 113], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172645); -tree.insert([0, 0, 0, 0, 0, 0, 0, 233], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172649); -tree.insert([0, 0, 0, 0, 0, 0, 2, 67], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172650); -tree.insert([0, 0, 0, 0, 0, 0, 1, 203], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172655); -tree.insert([0, 0, 0, 0, 0, 0, 2, 80], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172656); -tree.insert([0, 0, 0, 0, 0, 0, 0, 179], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172658); -tree.insert([0, 0, 0, 0, 0, 0, 1, 45], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172660); -tree.insert([0, 0, 0, 0, 0, 0, 0, 2], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172661); -tree.insert([0, 0, 0, 0, 0, 0, 1, 93], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172663); -tree.insert([0, 0, 0, 0, 0, 0, 1, 28], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172665); -tree.insert([0, 0, 0, 0, 0, 0, 0, 164], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172672); -tree.insert([0, 0, 0, 0, 0, 0, 0, 60], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172675); -tree.insert([0, 0, 0, 0, 0, 0, 0, 170], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172679); -tree.insert([0, 0, 0, 0, 0, 0, 0, 92], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172684); -tree.insert([0, 0, 0, 0, 0, 0, 1, 197], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172688); -tree.insert([0, 0, 0, 0, 0, 0, 0, 169], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172693); -tree.insert([0, 0, 0, 0, 0, 0, 3, 23], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172700); -tree.insert([0, 0, 0, 0, 0, 0, 0, 153], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172702); -tree.insert([0, 0, 0, 0, 0, 0, 2, 4], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172704); -tree.insert([0, 0, 0, 0, 0, 0, 0, 84], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172705); -tree.insert([0, 0, 0, 0, 0, 0, 0, 250], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172706); -tree.insert([0, 0, 0, 0, 0, 0, 1, 218], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172708); -tree.insert([0, 0, 0, 0, 0, 0, 0, 132], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172728); -tree.insert([0, 0, 0, 0, 0, 0, 1, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172733); -tree.insert([0, 0, 0, 0, 0, 0, 0, 63], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172735); -tree.insert([0, 0, 0, 0, 0, 0, 0, 77], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172737); -tree.insert([0, 0, 0, 0, 0, 0, 0, 35], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172740); -tree.insert([0, 0, 0, 0, 0, 0, 0, 103], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172742); -tree.insert([0, 0, 0, 0, 0, 0, 0, 16], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172749); -tree.insert([0, 0, 0, 0, 0, 0, 1, 38], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172750); -tree.insert([0, 0, 0, 0, 0, 0, 0, 155], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172755); -tree.insert([0, 0, 0, 0, 0, 0, 0, 255], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172756); -tree.insert([0, 0, 0, 0, 0, 0, 1, 46], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172761); -tree.insert([0, 0, 0, 0, 0, 0, 0, 10], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172765); -tree.insert([0, 0, 0, 0, 0, 0, 0, 82], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172766); -tree.insert([0, 0, 0, 0, 0, 0, 0, 177], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172767); -tree.insert([0, 0, 0, 0, 0, 0, 0, 199], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172774); -tree.insert([0, 0, 0, 0, 0, 0, 0, 106], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172786); -tree.insert([0, 0, 0, 0, 0, 0, 1, 34], [104, 101, 108, 108, 111, 104, 101, 108, 108, 111], 172788); -tree.flush_active_memtable(172701)?; -tree.compact(compaction.clone(), 172701)?; - -tree.compact(compaction.clone(), 172901)?; - -tree.compact(compaction.clone(), 173101)?; - - tree.drop_range::<&[u8], _>(..)?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/multi_trees.rs b/crates/lsm-tree/tests/multi_trees.rs index 5e516b277..3a0dc889e 100644 --- a/crates/lsm-tree/tests/multi_trees.rs +++ b/crates/lsm-tree/tests/multi_trees.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/mvcc_slab.rs b/crates/lsm-tree/tests/mvcc_slab.rs index 223ac5659..7806f87d6 100644 --- a/crates/lsm-tree/tests/mvcc_slab.rs +++ b/crates/lsm-tree/tests/mvcc_slab.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SequenceNumberCounter, + AbstractTree, Config, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; @@ -38,40 +38,3 @@ fn table_reader_mvcc_slab() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn table_reader_mvcc_slab_blob() -> lsm_tree::Result<()> { - const ITEM_COUNT: usize = 1_000; - - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .data_block_size_policy(BlockSizePolicy::all(1_024)) - // .index_block_size_policy(BlockSizePolicy::all(1_024)) - .with_kv_separation(Some(Default::default())) - .open()?; - - for _ in 0..ITEM_COUNT { - tree.insert("a", "neptune".repeat(10_000), seqno.next()); - } - tree.insert("b", "", 0); - - tree.flush_active_memtable(0)?; - - let version = tree.current_version(); - - let table = version - .level(0) - .expect("level should exist") - .first() - .expect("run should exist") - .first() - .expect("table should exist"); - - let reader = table.iter(); - assert_eq!(reader.count(), ITEM_COUNT + 1); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/recover_cleanup_orphans.rs b/crates/lsm-tree/tests/recover_cleanup_orphans.rs index b9f1d2f71..52fd04c81 100644 --- a/crates/lsm-tree/tests/recover_cleanup_orphans.rs +++ b/crates/lsm-tree/tests/recover_cleanup_orphans.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -38,53 +38,3 @@ fn tree_recovery_cleanup_orphans() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_recovery_cleanup_orphans_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some( - KvSeparationOptions::default() - .age_cutoff(1.0) - .staleness_threshold(0.01), - )) - .open()?; - - tree.insert("a", "a".repeat(10_000), 0); - tree.insert("a", "a".repeat(10_000), 1); - tree.flush_active_memtable(0)?; - - assert!(folder.path().join("blobs").join("0").try_exists()?); - - tree.major_compact(u64::MAX, 5)?; - - assert!(folder.path().join("blobs").join("0").try_exists()?); - - tree.major_compact(u64::MAX, 10)?; - - assert!(folder.path().join("blobs").join("1").try_exists()?); - } - - std::fs::File::create(folder.path().join("blobs").join("0"))?; - - { - let _tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert!(!folder.path().join("blobs").join("0").try_exists()?); - assert!(folder.path().join("blobs").join("1").try_exists()?); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/recovery_mac_ds_store.rs b/crates/lsm-tree/tests/recovery_mac_ds_store.rs index b767b60dc..c9af5990c 100644 --- a/crates/lsm-tree/tests/recovery_mac_ds_store.rs +++ b/crates/lsm-tree/tests/recovery_mac_ds_store.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/recovery_mac_underscore.rs b/crates/lsm-tree/tests/recovery_mac_underscore.rs index b32315c1c..b2dcdaade 100644 --- a/crates/lsm-tree/tests/recovery_mac_underscore.rs +++ b/crates/lsm-tree/tests/recovery_mac_underscore.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/regression_286.rs b/crates/lsm-tree/tests/regression_286.rs deleted file mode 100644 index f692262b0..000000000 --- a/crates/lsm-tree/tests/regression_286.rs +++ /dev/null @@ -1,32 +0,0 @@ -use lsm_tree::AbstractTree; - -#[test_log::test] -fn regression_286() -> lsm_tree::Result<()> { - let folder = lsm_tree::get_tmp_folder(); - let seqno = lsm_tree::SequenceNumberCounter::default(); - - { - let tree = lsm_tree::Config::new( - &folder, - seqno.clone(), - lsm_tree::SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(lsm_tree::KvSeparationOptions::default())) - .open()?; - - tree.insert("foo", b"1", 0); - tree.clear()?; - } - - { - let _tree = lsm_tree::Config::new( - &folder, - seqno.clone(), - lsm_tree::SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(lsm_tree::KvSeparationOptions::default())) - .open()?; - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/snapshot_compact.rs b/crates/lsm-tree/tests/snapshot_compact.rs index 60fbef86b..e02db1c89 100644 --- a/crates/lsm-tree/tests/snapshot_compact.rs +++ b/crates/lsm-tree/tests/snapshot_compact.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/snapshot_len.rs b/crates/lsm-tree/tests/snapshot_len.rs index 636b8520e..434198c24 100644 --- a/crates/lsm-tree/tests/snapshot_len.rs +++ b/crates/lsm-tree/tests/snapshot_len.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 100; diff --git a/crates/lsm-tree/tests/snapshot_point_read.rs b/crates/lsm-tree/tests/snapshot_point_read.rs index 0a4f5aa7d..14c9ad865 100644 --- a/crates/lsm-tree/tests/snapshot_point_read.rs +++ b/crates/lsm-tree/tests/snapshot_point_read.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/snapshot_zombie.rs b/crates/lsm-tree/tests/snapshot_zombie.rs index 1605051ac..c8c152859 100644 --- a/crates/lsm-tree/tests/snapshot_zombie.rs +++ b/crates/lsm-tree/tests/snapshot_zombie.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 5; diff --git a/crates/lsm-tree/tests/static_iterators.rs b/crates/lsm-tree/tests/static_iterators.rs index 83b7e0a91..a11351500 100644 --- a/crates/lsm-tree/tests/static_iterators.rs +++ b/crates/lsm-tree/tests/static_iterators.rs @@ -2,7 +2,7 @@ // 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::{get_tmp_folder, AbstractTree, Config, Guard, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SequenceNumberCounter, get_tmp_folder}; use test_log::test; /// Test that iterators can be stored in a struct (proving they're 'static) @@ -177,42 +177,6 @@ fn static_iterator_with_segments() -> lsm_tree::Result<()> { Ok(()) } -/// Test BlobTree static iterator -#[test] -fn static_iterator_blob_tree() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - // Insert values that will be separated to blob files - let large_value = vec![b'X'; 2048]; - for i in 0..20 { - tree.insert(format!("blob_{:03}", i), large_value.clone(), i as u64); - } - - tree.flush_active_memtable(20)?; - - let iter = tree.range("blob_000"..="blob_019", 20, None); - drop(tree); - - let results: Vec<_> = iter - .map(|guard| guard.into_inner()) - .collect::>>()?; - - assert_eq!(results.len(), 20); - for (_, value) in results { - assert_eq!(value.len(), 2048); - } - - Ok(()) -} - /// Test that iterator sees a consistent snapshot #[test] fn static_iterator_snapshot_isolation() -> lsm_tree::Result<()> { diff --git a/crates/lsm-tree/tests/table_full_file_checksum.rs b/crates/lsm-tree/tests/table_full_file_checksum.rs index c8f328a8c..29af94e12 100644 --- a/crates/lsm-tree/tests/table_full_file_checksum.rs +++ b/crates/lsm-tree/tests/table_full_file_checksum.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; use xxhash_rust::xxh3::xxh3_128; diff --git a/crates/lsm-tree/tests/table_point_reads.rs b/crates/lsm-tree/tests/table_point_reads.rs index 95e59cd17..d9fc1222e 100644 --- a/crates/lsm-tree/tests/table_point_reads.rs +++ b/crates/lsm-tree/tests/table_point_reads.rs @@ -1,6 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SeqNo, - SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; @@ -120,53 +119,3 @@ fn table_point_reads_mvcc_slab() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_tree_table_point_reads_mvcc_slab() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .data_block_size_policy(BlockSizePolicy::all(1_024)) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let keys = [0, 1, 2] - .into_iter() - .map(u64::to_be_bytes) - .collect::>(); - - for key in &keys { - for seqno in 0..ITEM_COUNT as u64 { - tree.insert(key, seqno.to_string(), seqno); - } - } - tree.flush_active_memtable(0)?; - - for key in &keys { - let item = tree.get(key, SeqNo::MAX)?.unwrap(); - assert_eq!( - String::from_utf8_lossy(&item).parse::().unwrap(), - ITEM_COUNT as u64 - 1 - ); - } - - for key in &keys { - // NOTE: Need to start at seqno=1 - for seqno in 1..ITEM_COUNT as u64 { - let value = tree.get(key, seqno)?.unwrap(); - - // NOTE: When snapshot is =1, it will read any items with - // seqno less than 1 - assert_eq!( - String::from_utf8_lossy(&value).parse::().unwrap(), - seqno - 1 - ); - } - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/table_range.rs b/crates/lsm-tree/tests/table_range.rs index dddf7e72c..684360cb8 100644 --- a/crates/lsm-tree/tests/table_range.rs +++ b/crates/lsm-tree/tests/table_range.rs @@ -1,6 +1,6 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, Guard, SeqNo, - SequenceNumberCounter, + AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, + get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/table_range_oob.rs b/crates/lsm-tree/tests/table_range_oob.rs index 5082176ee..dc006b612 100644 --- a/crates/lsm-tree/tests/table_range_oob.rs +++ b/crates/lsm-tree/tests/table_range_oob.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/table_remove_weak.rs b/crates/lsm-tree/tests/table_remove_weak.rs index 59dabd8b1..40c84bc22 100644 --- a/crates/lsm-tree/tests/table_remove_weak.rs +++ b/crates/lsm-tree/tests/table_remove_weak.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/table_weak_tombstones.rs b/crates/lsm-tree/tests/table_weak_tombstones.rs index b49ef8fab..44b56f6e0 100644 --- a/crates/lsm-tree/tests/table_weak_tombstones.rs +++ b/crates/lsm-tree/tests/table_weak_tombstones.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; #[test] fn weak_tombstone_counts_single_pair() -> lsm_tree::Result<()> { diff --git a/crates/lsm-tree/tests/tree_approx_len.rs b/crates/lsm-tree/tests/tree_approx_len.rs index bf770fbb2..f873c5069 100644 --- a/crates/lsm-tree/tests/tree_approx_len.rs +++ b/crates/lsm-tree/tests/tree_approx_len.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -36,44 +36,6 @@ fn tree_approx_len_sealed() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_approx_len_sealed_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 0); - - tree.insert("a", "", 0); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 1); - - tree.insert("b", "", 0); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 2); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 2); - - let _ = tree.rotate_memtable().unwrap(); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 2); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 2); - - Ok(()) -} - #[test] fn tree_approx_len() -> lsm_tree::Result<()> { let folder = get_tmp_folder(); @@ -141,72 +103,3 @@ fn tree_approx_len() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_approx_len_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 0); - - tree.insert("a", "", 0); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 1); - - tree.insert("b", "", 0); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 2); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 2); - - tree.insert("a", "", 1); - - // Approximate count diverges - assert_eq!(tree.len(SeqNo::MAX, None)?, 2); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 3); - - tree.remove("a", 2); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 4); - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert!(!tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 4); - - tree.remove("b", 4); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 5); - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 5); - - tree.major_compact(u64::MAX, 5)?; - - // Approximate count converges - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.is_empty(SeqNo::MAX, None)?); - assert_eq!(tree.approximate_len(), 0); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_bulk_ingest.rs b/crates/lsm-tree/tests/tree_bulk_ingest.rs index 3e8e51a4a..428531895 100644 --- a/crates/lsm-tree/tests/tree_bulk_ingest.rs +++ b/crates/lsm-tree/tests/tree_bulk_ingest.rs @@ -1,6 +1,4 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, Guard, KvSeparationOptions, SeqNo, SequenceNumberCounter, -}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 100_000; @@ -99,107 +97,3 @@ fn tree_copy() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_tree_bulk_ingest() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - let visible_seqno = SequenceNumberCounter::default(); - - let tree = Config::new(&folder, seqno.clone(), visible_seqno.clone()) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let mut ingestion = tree.ingestion()?; - for x in 0..ITEM_COUNT as u64 { - let k = x.to_be_bytes(); - let v = nanoid::nanoid!(); - ingestion.write(k, v)?; - } - ingestion.finish()?; - - assert_eq!(visible_seqno.get(), seqno.get()); - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert_eq!( - tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT, - ); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT, - ); - assert_eq!(1, tree.blob_file_count()); - - Ok(()) -} - -#[test] -fn blob_tree_copy() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - let visible_seqno = SequenceNumberCounter::default(); - - let src = Config::new(&folder, seqno.clone(), visible_seqno.clone()) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let mut ingestion = src.ingestion()?; - for x in 0..ITEM_COUNT as u64 { - let k = x.to_be_bytes(); - let v = nanoid::nanoid!(); - ingestion.write(k, v)?; - } - ingestion.finish()?; - - assert_eq!(visible_seqno.get(), seqno.get()); - - assert_eq!(src.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert_eq!( - src.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT, - ); - assert_eq!( - src.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT, - ); - assert_eq!(1, src.blob_file_count()); - - let folder = get_tmp_folder(); - let dest = Config::new(&folder, seqno.clone(), visible_seqno.clone()) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - let mut ingestion = dest.ingestion()?; - for item in src.iter(SeqNo::MAX, None) { - let (k, v) = item.into_inner().unwrap(); - ingestion.write(k, v)?; - } - ingestion.finish()?; - - assert_eq!(visible_seqno.get(), seqno.get()); - - assert_eq!(dest.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert_eq!( - dest.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT, - ); - assert_eq!( - dest.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT, - ); - assert_eq!(1, dest.blob_file_count()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_clear.rs b/crates/lsm-tree/tests/tree_clear.rs index 022a8db48..32a427a86 100644 --- a/crates/lsm-tree/tests/tree_clear.rs +++ b/crates/lsm-tree/tests/tree_clear.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_clear_snapshot.rs b/crates/lsm-tree/tests/tree_clear_snapshot.rs index 21342f5f2..e4717190c 100644 --- a/crates/lsm-tree/tests/tree_clear_snapshot.rs +++ b/crates/lsm-tree/tests/tree_clear_snapshot.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_count.rs b/crates/lsm-tree/tests/tree_count.rs index 01a8c6de8..020343d25 100644 --- a/crates/lsm-tree/tests/tree_count.rs +++ b/crates/lsm-tree/tests/tree_count.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 1_000; @@ -70,43 +70,6 @@ fn tree_flushed_count() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_flushed_count_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - for x in 0..ITEM_COUNT as u64 { - let key = x.to_be_bytes(); - let value = nanoid::nanoid!(); - tree.insert(key, value.as_bytes(), 0); - } - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert_eq!( - tree.iter(SeqNo::MAX, None).flat_map(|x| x.key()).count(), - ITEM_COUNT - ); - assert_eq!( - tree.iter(SeqNo::MAX, None) - .rev() - .flat_map(|x| x.key()) - .count(), - ITEM_COUNT - ); - - Ok(()) -} - #[test] fn tree_non_locking_count() -> lsm_tree::Result<()> { use std::ops::Bound::{self, Excluded, Unbounded}; diff --git a/crates/lsm-tree/tests/tree_delete_loop.rs b/crates/lsm-tree/tests/tree_delete_loop.rs index fe29a801e..4aaf2174f 100644 --- a/crates/lsm-tree/tests/tree_delete_loop.rs +++ b/crates/lsm-tree/tests/tree_delete_loop.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_different_block_size.rs b/crates/lsm-tree/tests/tree_different_block_size.rs index 9cb2137f3..dfb6fbe11 100644 --- a/crates/lsm-tree/tests/tree_different_block_size.rs +++ b/crates/lsm-tree/tests/tree_different_block_size.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/tree_disjoint_iter.rs b/crates/lsm-tree/tests/tree_disjoint_iter.rs index a53a3afad..1e81422a4 100644 --- a/crates/lsm-tree/tests/tree_disjoint_iter.rs +++ b/crates/lsm-tree/tests/tree_disjoint_iter.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice, get_tmp_folder}; use test_log::test; macro_rules! iter_closed { diff --git a/crates/lsm-tree/tests/tree_disjoint_point_read.rs b/crates/lsm-tree/tests/tree_disjoint_point_read.rs index 895fe6615..c7cb79312 100644 --- a/crates/lsm-tree/tests/tree_disjoint_point_read.rs +++ b/crates/lsm-tree/tests/tree_disjoint_point_read.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; @@ -35,37 +35,3 @@ fn tree_disjoint_point_read() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_disjoint_point_read_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .data_block_size_policy(BlockSizePolicy::all(1_024)) - // .index_block_size_policy(BlockSizePolicy::all(1_024)) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - - tree.flush_active_memtable(0)?; - - tree.insert("d", "d", 0); - tree.insert("e", "e", 0); - tree.insert("f", "f", 0); - - tree.flush_active_memtable(0)?; - - for key in [b"a", b"b", b"c", b"d", b"e", b"f"] { - let value = tree.get(key, SeqNo::MAX).unwrap().unwrap(); - assert_eq!(&*value, key) - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_disjoint_prefix.rs b/crates/lsm-tree/tests/tree_disjoint_prefix.rs index fb1907b49..b75aaa181 100644 --- a/crates/lsm-tree/tests/tree_disjoint_prefix.rs +++ b/crates/lsm-tree/tests/tree_disjoint_prefix.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice, get_tmp_folder}; use test_log::test; macro_rules! iter_closed { diff --git a/crates/lsm-tree/tests/tree_disjoint_range.rs b/crates/lsm-tree/tests/tree_disjoint_range.rs index d2b81c086..ace48243d 100644 --- a/crates/lsm-tree/tests/tree_disjoint_range.rs +++ b/crates/lsm-tree/tests/tree_disjoint_range.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, Slice, get_tmp_folder}; use test_log::test; macro_rules! iter_closed { diff --git a/crates/lsm-tree/tests/tree_drop_range.rs b/crates/lsm-tree/tests/tree_drop_range.rs deleted file mode 100644 index 02cdd75de..000000000 --- a/crates/lsm-tree/tests/tree_drop_range.rs +++ /dev/null @@ -1,277 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, AnyTree, Config, SeqNo, SequenceNumberCounter}; -use std::ops::Bound::{Excluded, Included, Unbounded}; - -fn populate_tables(tree: &AnyTree) -> lsm_tree::Result<()> { - for key in 'a'..='e' { - tree.insert([key as u8], "", 0); - tree.flush_active_memtable(0)?; - } - Ok(()) -} - -#[test] -fn tree_drop_range_basic() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - assert_eq!(1, tree.l0_run_count()); - assert_eq!(5, tree.table_count()); - - tree.drop_range("a"..="c")?; - - assert!(!tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(tree.contains_key("d", SeqNo::MAX)?); - assert!(tree.contains_key("e", SeqNo::MAX)?); - - assert_eq!(1, tree.l0_run_count()); - assert_eq!(2, tree.table_count()); - - Ok(()) -} - -#[test] -fn tree_drop_range_partial_table_overlap_kept() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - for key in ['a', 'b', 'c', 'd', 'e'] { - tree.insert([key as u8], "", 0); - } - tree.flush_active_memtable(0)?; - - assert_eq!(1, tree.l0_run_count()); - assert_eq!(1, tree.table_count()); - - tree.drop_range("b".."d")?; - - for key in ['a', 'b', 'c', 'd', 'e'] { - assert!(tree.contains_key([key as u8], SeqNo::MAX)?); - } - - assert_eq!(1, tree.l0_run_count()); - assert_eq!(1, tree.table_count()); - - Ok(()) -} - -#[test] -fn tree_drop_range_upper_exclusive() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range("a".."d")?; - - assert!(!tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(tree.contains_key("d", SeqNo::MAX)?); - assert!(tree.contains_key("e", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_lower_exclusive() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range::<&str, _>((Excluded("a"), Included("c")))?; - - assert!(tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(tree.contains_key("d", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_unbounded_lower_inclusive_upper() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range::<&str, _>((Unbounded, Included("c")))?; - - assert!(!tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(tree.contains_key("d", SeqNo::MAX)?); - assert!(tree.contains_key("e", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_unbounded_lower_exclusive_upper() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range::<&str, _>((Unbounded, Excluded("d")))?; - - assert!(!tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(tree.contains_key("d", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_exclusive_empty_interval() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range::<&str, _>((Excluded("b"), Excluded("b")))?; - - assert!(tree.contains_key("a", SeqNo::MAX)?); - assert!(tree.contains_key("b", SeqNo::MAX)?); - assert!(tree.contains_key("c", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_empty_tree() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - tree.drop_range("a"..="c")?; - - assert_eq!(0, tree.l0_run_count()); - assert_eq!(0, tree.table_count()); - - Ok(()) -} - -#[test] -fn tree_drop_range_unbounded_upper() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range("c"..)?; - - assert!(tree.contains_key("a", SeqNo::MAX)?); - assert!(tree.contains_key("b", SeqNo::MAX)?); - assert!(!tree.contains_key("c", SeqNo::MAX)?); - assert!(!tree.contains_key("d", SeqNo::MAX)?); - assert!(!tree.contains_key("e", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_clear_all() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range::<&str, _>(..)?; - - assert_eq!(0, tree.l0_run_count()); - assert_eq!(0, tree.table_count()); - assert!(!tree.contains_key("a", SeqNo::MAX)?); - assert!(!tree.contains_key("e", SeqNo::MAX)?); - - Ok(()) -} - -#[test] -fn tree_drop_range_inverted_bounds_is_noop() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open()?; - - populate_tables(&tree)?; - - tree.drop_range("c".."a")?; - tree.drop_range("c"..="a")?; - - // All keys remain because the range is treated as empty. - for key in 'a'..='e' { - assert!(tree.contains_key([key as u8], SeqNo::MAX)?); - } - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_filter_hit_rate.rs b/crates/lsm-tree/tests/tree_filter_hit_rate.rs deleted file mode 100644 index 281eb2e1c..000000000 --- a/crates/lsm-tree/tests/tree_filter_hit_rate.rs +++ /dev/null @@ -1,86 +0,0 @@ -#[test_log::test] -#[cfg(feature = "metrics")] -fn tree_filter_hit_rate() -> lsm_tree::Result<()> { - use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; - - let a = { - let folder = get_tmp_folder(); - let path = folder.path(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(path, seqno.clone(), SequenceNumberCounter::default()).open()?; - - for k in 0u64..10_000 { - tree.insert(k.to_be_bytes(), "abc", seqno.next()); - } - tree.flush_active_memtable(0)?; - tree.major_compact(u64::MAX, 0)?; - - for k in 0u64..10_000 { - tree.get(k.to_be_bytes(), SeqNo::MAX).unwrap().unwrap(); - } - - tree.metrics().filter_efficiency() - }; - - { - let folder = get_tmp_folder(); - let path = folder.path(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(path, seqno.clone(), SequenceNumberCounter::default()) - .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(1))) - .open()?; - - for k in 0u64..10_000 { - tree.insert(k.to_be_bytes(), "abc", seqno.next()); - } - tree.flush_active_memtable(0)?; - tree.major_compact(u64::MAX, 0)?; - - for k in 0u64..10_000 { - tree.get(k.to_be_bytes(), SeqNo::MAX).unwrap().unwrap(); - } - - assert_eq!(a, tree.metrics().filter_efficiency()); - } - - Ok(()) -} - -#[test_log::test] -#[cfg(feature = "metrics")] -fn tree_filter_hit_rate_hit() -> lsm_tree::Result<()> { - use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, KvSeparationOptions, SeqNo, SequenceNumberCounter, - }; - - let folder = get_tmp_folder(); - let path = folder.path(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(path, seqno.clone(), SequenceNumberCounter::default()).open()?; - - tree.insert("a", "a", 0); - tree.insert("c", "a", 0); - tree.flush_active_memtable(0)?; - - assert!(tree.get("b", SeqNo::MAX)?.is_none()); - assert_eq!(1, tree.metrics().filter_queries()); - assert_eq!(1, tree.metrics().io_skipped_by_filter()); - assert_eq!(1.0, tree.metrics().filter_efficiency()); - - assert!(tree.get("a", SeqNo::MAX)?.is_some()); - assert_eq!(1.0, tree.metrics().filter_efficiency()); - - assert!(tree.get("a", SeqNo::MAX)?.is_some()); - assert_eq!(1.0, tree.metrics().filter_efficiency()); - - assert!(tree.get("a", SeqNo::MAX)?.is_some()); - assert_eq!(1.0, tree.metrics().filter_efficiency()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_flush_eviction.rs b/crates/lsm-tree/tests/tree_flush_eviction.rs index 714092db7..f3e43bc40 100644 --- a/crates/lsm-tree/tests/tree_flush_eviction.rs +++ b/crates/lsm-tree/tests/tree_flush_eviction.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, get_tmp_folder}; use std::sync::Arc; use test_log::test; diff --git a/crates/lsm-tree/tests/tree_guarded_range.rs b/crates/lsm-tree/tests/tree_guarded_range.rs index 40d0623f3..ddc9c4e3e 100644 --- a/crates/lsm-tree/tests/tree_guarded_range.rs +++ b/crates/lsm-tree/tests/tree_guarded_range.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -35,39 +35,3 @@ fn tree_guarded_range() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_tree_guarded_range() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a".as_bytes(), nanoid::nanoid!().as_bytes(), 0); - tree.insert("f".as_bytes(), nanoid::nanoid!().as_bytes(), 1); - tree.insert("g".as_bytes(), nanoid::nanoid!().as_bytes(), 2); - - tree.insert("a".as_bytes(), nanoid::nanoid!().as_bytes(), 3); - tree.insert("f".as_bytes(), nanoid::nanoid!().as_bytes(), 4); - tree.insert("g".as_bytes(), nanoid::nanoid!().as_bytes(), 5); - - assert_eq!( - 2, - tree.range("a"..="f", SeqNo::MAX, None) - .flat_map(Guard::key) - .count(), - ); - assert_eq!( - 2, - tree.range("f"..="g", SeqNo::MAX, None) - .flat_map(Guard::key) - .count(), - ); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_kv.rs b/crates/lsm-tree/tests/tree_kv.rs index af1d4d326..02835a66e 100644 --- a/crates/lsm-tree/tests/tree_kv.rs +++ b/crates/lsm-tree/tests/tree_kv.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_l0_point_read.rs b/crates/lsm-tree/tests/tree_l0_point_read.rs deleted file mode 100644 index 230213a30..000000000 --- a/crates/lsm-tree/tests/tree_l0_point_read.rs +++ /dev/null @@ -1,48 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn tree_l0_point_read() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = lsm_tree::Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - tree.flush_active_memtable(0)?; - - tree.insert("a", "a", 1); - tree.insert("b", "b", 1); - tree.insert("c", "c", 1); - tree.flush_active_memtable(0)?; - - tree.insert("a", "A", 2); - tree.insert("b", "B", 2); - tree.insert("c", "C", 2); - tree.flush_active_memtable(0)?; - - tree.insert("d", "d", 3); - tree.insert("e", "e", 3); - tree.insert("f", "f", 3); - tree.flush_active_memtable(0)?; - - tree.insert("g", "g", 3); - tree.flush_active_memtable(0)?; - - assert_eq!(b"A", &*tree.get("a", SeqNo::MAX)?.unwrap()); - assert_eq!(b"B", &*tree.get("b", SeqNo::MAX)?.unwrap()); - assert_eq!(b"C", &*tree.get("c", SeqNo::MAX)?.unwrap()); - assert_eq!(b"d", &*tree.get("d", SeqNo::MAX)?.unwrap()); - assert_eq!(b"e", &*tree.get("e", SeqNo::MAX)?.unwrap()); - assert_eq!(b"f", &*tree.get("f", SeqNo::MAX)?.unwrap()); - assert_eq!(b"g", &*tree.get("g", SeqNo::MAX)?.unwrap()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_l0_range.rs b/crates/lsm-tree/tests/tree_l0_range.rs deleted file mode 100644 index a8fbc0f12..000000000 --- a/crates/lsm-tree/tests/tree_l0_range.rs +++ /dev/null @@ -1,51 +0,0 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Guard, SeqNo, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn tree_l0_range_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = lsm_tree::Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a", "a", 0); - tree.insert("b", "b", 0); - tree.insert("c", "c", 0); - tree.flush_active_memtable(0)?; - - tree.insert("a", "a", 1); - tree.insert("b", "b", 1); - tree.insert("c", "c", 1); - tree.flush_active_memtable(0)?; - - tree.insert("a", "A", 2); - tree.insert("b", "B", 2); - tree.insert("c", "C", 2); - tree.flush_active_memtable(0)?; - - tree.insert("d", "d", 3); - tree.insert("e", "e", 3); - tree.insert("f", "f", 3); - tree.flush_active_memtable(0)?; - - tree.insert("g", "g".repeat(10_000), 3); - tree.flush_active_memtable(0)?; - - let mut range = tree.range("c"..="e", SeqNo::MAX, None); - assert_eq!(b"C", &*range.next().unwrap().value()?); - assert_eq!(b"d", &*range.next().unwrap().value()?); - assert_eq!(b"e", &*range.next().unwrap().value()?); - assert!(range.next().is_none()); - - let mut range = tree.range("f"..="g", SeqNo::MAX, None).rev(); - assert_eq!(b"g".repeat(10_000), &*range.next().unwrap().value()?); - assert_eq!(b"f", &*range.next().unwrap().value()?); - assert!(range.next().is_none()); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_major_compaction.rs b/crates/lsm-tree/tests/tree_major_compaction.rs index 010efac19..6b49a7568 100644 --- a/crates/lsm-tree/tests/tree_major_compaction.rs +++ b/crates/lsm-tree/tests/tree_major_compaction.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_mvcc_simple.rs b/crates/lsm-tree/tests/tree_mvcc_simple.rs index 4d4853929..820a8a446 100644 --- a/crates/lsm-tree/tests/tree_mvcc_simple.rs +++ b/crates/lsm-tree/tests/tree_mvcc_simple.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_non_disjoint_point_read.rs b/crates/lsm-tree/tests/tree_non_disjoint_point_read.rs index 2cbbe8ccd..63063b243 100644 --- a/crates/lsm-tree/tests/tree_non_disjoint_point_read.rs +++ b/crates/lsm-tree/tests/tree_non_disjoint_point_read.rs @@ -1,5 +1,5 @@ use lsm_tree::{ - config::BlockSizePolicy, get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter, + AbstractTree, Config, SeqNo, SequenceNumberCounter, config::BlockSizePolicy, get_tmp_folder, }; use test_log::test; diff --git a/crates/lsm-tree/tests/tree_range.rs b/crates/lsm-tree/tests/tree_range.rs index 4d6ee2bbc..806b4d79e 100644 --- a/crates/lsm-tree/tests/tree_range.rs +++ b/crates/lsm-tree/tests/tree_range.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -65,69 +65,3 @@ fn tree_range_count() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn blob_tree_range_count() -> lsm_tree::Result<()> { - use std::ops::Bound::{self, Excluded, Unbounded}; - - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - tree.insert("a".as_bytes(), nanoid::nanoid!().as_bytes(), 0); - tree.insert("f".as_bytes(), nanoid::nanoid!().as_bytes(), 1); - tree.insert("g".as_bytes(), b"neptune!".repeat(128_000), 2); - - tree.insert("a".as_bytes(), nanoid::nanoid!().as_bytes(), 3); - tree.insert("f".as_bytes(), b"neptune!".repeat(128_000), 4); - tree.insert("g".as_bytes(), nanoid::nanoid!().as_bytes(), 5); - - assert_eq!(2, tree.range("a"..="f", SeqNo::MAX, None).count()); - assert_eq!(2, tree.range("f"..="g", SeqNo::MAX, None).count()); - - assert_eq!( - 1, - tree.range::, (Bound>, Bound>)>( - (Excluded("f".into()), Unbounded), - SeqNo::MAX, - None - ) - .count() - ); - - tree.flush_active_memtable(0)?; - - assert_eq!(2, tree.range("a"..="f", SeqNo::MAX, None).count()); - assert_eq!( - 1, - tree.range::, (Bound>, Bound>)>( - (Excluded("f".into()), Unbounded), - SeqNo::MAX, - None - ) - .count() - ); - - tree.insert("a".as_bytes(), nanoid::nanoid!().as_bytes(), 6); - tree.insert("f".as_bytes(), nanoid::nanoid!().as_bytes(), 7); - tree.insert("g".as_bytes(), nanoid::nanoid!().as_bytes(), 8); - - assert_eq!(2, tree.range("a"..="f", SeqNo::MAX, None).count()); - assert_eq!( - 1, - tree.range::, (Bound>, Bound>)>( - (Excluded("f".into()), Unbounded), - SeqNo::MAX, - None - ) - .count() - ); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_range_memtable_only.rs b/crates/lsm-tree/tests/tree_range_memtable_only.rs index 19038d7e9..5742cfc26 100644 --- a/crates/lsm-tree/tests/tree_range_memtable_only.rs +++ b/crates/lsm-tree/tests/tree_range_memtable_only.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_recover_counter.rs b/crates/lsm-tree/tests/tree_recover_counter.rs index e8474c627..5f061bb46 100644 --- a/crates/lsm-tree/tests/tree_recover_counter.rs +++ b/crates/lsm-tree/tests/tree_recover_counter.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_recover_large_value.rs b/crates/lsm-tree/tests/tree_recover_large_value.rs index 102d08dc5..4844c2b1a 100644 --- a/crates/lsm-tree/tests/tree_recover_large_value.rs +++ b/crates/lsm-tree/tests/tree_recover_large_value.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_recovery_versions.rs b/crates/lsm-tree/tests/tree_recovery_versions.rs index b832f5554..888c11b95 100644 --- a/crates/lsm-tree/tests/tree_recovery_versions.rs +++ b/crates/lsm-tree/tests/tree_recovery_versions.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_reload.rs b/crates/lsm-tree/tests/tree_reload.rs index 0d396388a..fff0ebbf7 100644 --- a/crates/lsm-tree/tests/tree_reload.rs +++ b/crates/lsm-tree/tests/tree_reload.rs @@ -1,6 +1,4 @@ -use lsm_tree::{ - get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, TreeType, -}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 10_000; @@ -38,79 +36,6 @@ fn tree_reload_smoke_test() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_reload_smoke_test_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let large_value = "a".repeat(10_000); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(0, tree.table_count()); - - tree.insert("a", &large_value, 0); - tree.flush_active_memtable(0)?; - - assert_eq!(1, tree.table_count()); - assert!(tree.contains_key("a", SeqNo::MAX)?); - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(1, tree.table_count()); - assert_eq!(large_value.as_bytes(), tree.get("a", SeqNo::MAX)?.unwrap()); - } - - Ok(()) -} - -#[test] -fn tree_reload_blob_again_without_opts() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - assert_eq!(0, tree.table_count()); - assert_eq!(0, tree.blob_file_count()); - } - - { - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(None) - .open(); - - assert!(matches!(tree, Err(lsm_tree::Error::Unrecoverable))); - } - - Ok(()) -} - #[test] fn tree_reload_empty() -> lsm_tree::Result<()> { let folder = get_tmp_folder(); @@ -132,7 +57,6 @@ fn tree_reload_empty() -> lsm_tree::Result<()> { .count(), 0 ); - assert_eq!(tree.tree_type(), TreeType::Standard); } { @@ -152,7 +76,6 @@ fn tree_reload_empty() -> lsm_tree::Result<()> { .count(), 0 ); - assert_eq!(tree.tree_type(), TreeType::Standard); tree.flush_active_memtable(0)?; } @@ -174,7 +97,6 @@ fn tree_reload_empty() -> lsm_tree::Result<()> { .count(), 0 ); - assert_eq!(tree.tree_type(), TreeType::Standard); tree.flush_active_memtable(0)?; } @@ -196,7 +118,6 @@ fn tree_reload_empty() -> lsm_tree::Result<()> { .count(), 0 ); - assert_eq!(tree.tree_type(), TreeType::Standard); } Ok(()) diff --git a/crates/lsm-tree/tests/tree_reload_pwd.rs b/crates/lsm-tree/tests/tree_reload_pwd.rs index ca0e65805..a89cd5057 100644 --- a/crates/lsm-tree/tests/tree_reload_pwd.rs +++ b/crates/lsm-tree/tests/tree_reload_pwd.rs @@ -1,5 +1,5 @@ use fs_extra::dir::CopyOptions; -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; const ITEM_COUNT: usize = 10_000; diff --git a/crates/lsm-tree/tests/tree_sealed_shadowing.rs b/crates/lsm-tree/tests/tree_sealed_shadowing.rs index d61eed689..f3b70ee65 100644 --- a/crates/lsm-tree/tests/tree_sealed_shadowing.rs +++ b/crates/lsm-tree/tests/tree_sealed_shadowing.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_seqno.rs b/crates/lsm-tree/tests/tree_seqno.rs index b8b0752af..249d41b5b 100644 --- a/crates/lsm-tree/tests/tree_seqno.rs +++ b/crates/lsm-tree/tests/tree_seqno.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_shadowing.rs b/crates/lsm-tree/tests/tree_shadowing.rs index 710bb6a28..1ed15b872 100644 --- a/crates/lsm-tree/tests/tree_shadowing.rs +++ b/crates/lsm-tree/tests/tree_shadowing.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] @@ -36,44 +36,6 @@ fn tree_shadowing_upsert() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_shadowing_upsert_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let key = "1".as_bytes(); - let value = "oldvalue".as_bytes(); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - tree.insert(key, value, 0); - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - tree.flush_active_memtable(0)?; - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - let value = "newvalue".as_bytes(); - - tree.insert(key, value, 1); - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - tree.flush_active_memtable(0)?; - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - Ok(()) -} - #[test] fn tree_shadowing_delete() -> lsm_tree::Result<()> { let folder = get_tmp_folder(); @@ -108,42 +70,6 @@ fn tree_shadowing_delete() -> lsm_tree::Result<()> { Ok(()) } - -#[test] -fn tree_shadowing_delete_blob() -> lsm_tree::Result<()> { - let folder = get_tmp_folder(); - - let tree = Config::new( - &folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .with_kv_separation(Some(Default::default())) - .open()?; - - let key = "1".as_bytes(); - let value = "oldvalue".as_bytes(); - - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - tree.insert(key, value, 0); - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - tree.flush_active_memtable(0)?; - assert_eq!(tree.len(SeqNo::MAX, None)?, 1); - assert_eq!(tree.get(key, SeqNo::MAX)?, Some(value.into())); - - tree.remove(key, 1); - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.get(key, SeqNo::MAX)?.is_none()); - - tree.flush_active_memtable(0)?; - assert_eq!(tree.len(SeqNo::MAX, None)?, 0); - assert!(tree.get(key, SeqNo::MAX)?.is_none()); - - Ok(()) -} - #[test] fn tree_shadowing_range() -> lsm_tree::Result<()> { const ITEM_COUNT: usize = 10_000; @@ -163,9 +89,10 @@ fn tree_shadowing_range() -> lsm_tree::Result<()> { tree.flush_active_memtable(0)?; assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "old".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "old".as_bytes()) + ); for x in 0..ITEM_COUNT as u64 { let key = x.to_be_bytes(); @@ -174,66 +101,21 @@ fn tree_shadowing_range() -> lsm_tree::Result<()> { } assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "new".as_bytes()) + ); tree.flush_active_memtable(0)?; assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "new".as_bytes()) + ); Ok(()) } - -#[test] -fn tree_shadowing_range_blob() -> lsm_tree::Result<()> { - const ITEM_COUNT: usize = 10_000; - - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .with_kv_separation(Some(Default::default())) - .open()?; - - for x in 0..ITEM_COUNT as u64 { - let key = x.to_be_bytes(); - let value = "old".as_bytes(); - tree.insert(key, value, seqno.next()); - } - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "old".as_bytes())); - - for x in 0..ITEM_COUNT as u64 { - let key = x.to_be_bytes(); - let value = "new".as_bytes(); - tree.insert(key, value, seqno.next()); - } - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); - - Ok(()) -} - #[test] fn tree_shadowing_prefix() -> lsm_tree::Result<()> { const ITEM_COUNT: usize = 10_000; @@ -263,9 +145,10 @@ fn tree_shadowing_prefix() -> lsm_tree::Result<()> { tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), ITEM_COUNT ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "old".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "old".as_bytes()) + ); for x in 0..ITEM_COUNT as u64 { let value = "new".as_bytes(); @@ -284,9 +167,10 @@ fn tree_shadowing_prefix() -> lsm_tree::Result<()> { tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), ITEM_COUNT ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "new".as_bytes()) + ); tree.flush_active_memtable(0)?; @@ -299,83 +183,10 @@ fn tree_shadowing_prefix() -> lsm_tree::Result<()> { tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), ITEM_COUNT ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); - - Ok(()) -} - -#[test] -fn tree_shadowing_prefix_blob() -> lsm_tree::Result<()> { - const ITEM_COUNT: usize = 10_000; - - let folder = get_tmp_folder(); - - let seqno = SequenceNumberCounter::default(); - - let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default()) - .with_kv_separation(Some(Default::default())) - .open()?; - - for x in 0..ITEM_COUNT as u64 { - let value = "old".as_bytes(); - let batch_seqno = seqno.next(); - - tree.insert(format!("pre:{x}").as_bytes(), value, batch_seqno); - tree.insert(format!("prefix:{x}").as_bytes(), value, batch_seqno); - } - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT * 2); - assert_eq!( - tree.prefix("pre".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT * 2 - ); - assert_eq!( - tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT - ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "old".as_bytes())); - - for x in 0..ITEM_COUNT as u64 { - let value = "new".as_bytes(); - let batch_seqno = seqno.next(); - - tree.insert(format!("pre:{x}").as_bytes(), value, batch_seqno); - tree.insert(format!("prefix:{x}").as_bytes(), value, batch_seqno); - } - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT * 2); - assert_eq!( - tree.prefix("pre".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT * 2 - ); - assert_eq!( - tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT - ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); - - tree.flush_active_memtable(0)?; - - assert_eq!(tree.len(SeqNo::MAX, None)?, ITEM_COUNT * 2); - assert_eq!( - tree.prefix("pre".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT * 2 - ); - assert_eq!( - tree.prefix("prefix".as_bytes(), SeqNo::MAX, None).count(), - ITEM_COUNT - ); - assert!(tree - .iter(SeqNo::MAX, None) - .all(|x| &*x.value().unwrap() == "new".as_bytes())); + assert!( + tree.iter(SeqNo::MAX, None) + .all(|x| &*x.value().unwrap() == "new".as_bytes()) + ); Ok(()) } diff --git a/crates/lsm-tree/tests/tree_trace.rs b/crates/lsm-tree/tests/tree_trace.rs index 3bdac9dc0..0371dbbf8 100644 --- a/crates/lsm-tree/tests/tree_trace.rs +++ b/crates/lsm-tree/tests/tree_trace.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_v1_load_fixture.rs b/crates/lsm-tree/tests/tree_v1_load_fixture.rs deleted file mode 100644 index ee968bc7c..000000000 --- a/crates/lsm-tree/tests/tree_v1_load_fixture.rs +++ /dev/null @@ -1,34 +0,0 @@ -use lsm_tree::{Config, SequenceNumberCounter}; -use test_log::test; - -#[test] -fn tree_load_v1() -> lsm_tree::Result<()> { - let folder = "test_fixture/v1_tree"; - - let result = Config::new( - folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open(); - - matches!(result, Err(lsm_tree::Error::InvalidVersion(1))); - - Ok(()) -} - -#[test] -fn tree_load_v1_corrupt() -> lsm_tree::Result<()> { - let folder = "test_fixture/v1_tree_corrupt"; - - let result = Config::new( - folder, - SequenceNumberCounter::default(), - SequenceNumberCounter::default(), - ) - .open(); - - matches!(result, Err(lsm_tree::Error::InvalidVersion(1))); - - Ok(()) -} diff --git a/crates/lsm-tree/tests/tree_weak_delete.rs b/crates/lsm-tree/tests/tree_weak_delete.rs index 68fc306a6..c8c573c76 100644 --- a/crates/lsm-tree/tests/tree_weak_delete.rs +++ b/crates/lsm-tree/tests/tree_weak_delete.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_weak_delete_eviction.rs b/crates/lsm-tree/tests/tree_weak_delete_eviction.rs index d36dc09fc..d7ce02ed3 100644 --- a/crates/lsm-tree/tests/tree_weak_delete_eviction.rs +++ b/crates/lsm-tree/tests/tree_weak_delete_eviction.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_weak_delete_queue.rs b/crates/lsm-tree/tests/tree_weak_delete_queue.rs index b03d9d5b4..3b7a1781d 100644 --- a/crates/lsm-tree/tests/tree_weak_delete_queue.rs +++ b/crates/lsm-tree/tests/tree_weak_delete_queue.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Guard, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Guard, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/lsm-tree/tests/tree_write_read.rs b/crates/lsm-tree/tests/tree_write_read.rs index af4e3508d..a3f60379a 100644 --- a/crates/lsm-tree/tests/tree_write_read.rs +++ b/crates/lsm-tree/tests/tree_write_read.rs @@ -1,4 +1,4 @@ -use lsm_tree::{get_tmp_folder, AbstractTree, Config, SeqNo, SequenceNumberCounter}; +use lsm_tree::{AbstractTree, Config, SeqNo, SequenceNumberCounter, get_tmp_folder}; use test_log::test; #[test] diff --git a/crates/rawdb/Cargo.toml b/crates/rawdb/Cargo.toml index 648f226d0..0fc531b3e 100644 --- a/crates/rawdb/Cargo.toml +++ b/crates/rawdb/Cargo.toml @@ -17,7 +17,7 @@ memmap2 = "0.9.11" parking_lot = { workspace = true } rayon = { workspace = true } smallvec = "1.15" -thiserror = "2.0.18" +thiserror = "2.0.19" [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/vecdb/Cargo.toml b/crates/vecdb/Cargo.toml index 070be02d3..fa3fa508a 100644 --- a/crates/vecdb/Cargo.toml +++ b/crates/vecdb/Cargo.toml @@ -28,7 +28,7 @@ log = { workspace = true } parking_lot = { workspace = true } rawdb = { workspace = true } ryu = "1.0.23" -serde_json = { version = "1.0.150", features = ["float_roundtrip"], optional = true } +serde_json = { version = "1.0.151", features = ["float_roundtrip"], optional = true } thiserror = "2" # [optional-dependencies] @@ -38,7 +38,7 @@ schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } sonic-rs = { version = "0.5.8", optional = true } vecdb_derive = { workspace = true, optional = true } -zerocopy = { version = "0.8.54", features = ["zerocopy-derive"], optional = true } +zerocopy = { version = "0.8.55", features = ["zerocopy-derive"], optional = true } zstd = { version = "0.13.3", optional = true } [dev-dependencies] diff --git a/crates/vecdb_derive/Cargo.toml b/crates/vecdb_derive/Cargo.toml index b6eda1a85..6f6fa8f25 100644 --- a/crates/vecdb_derive/Cargo.toml +++ b/crates/vecdb_derive/Cargo.toml @@ -14,7 +14,7 @@ readme.workspace = true proc-macro = true [dependencies] -syn = "2.0" +syn = "3.0" quote = "1.0" [dev-dependencies]

- - CI - - - docs.rs - - - Crates.io - - MSRV - - dependency status - -