Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c31d68d
feat: support custom sequence number generators
polaz Mar 14, 2026
205b0b7
docs(builder): clarify seqno_generator affects write sequencing only
polaz Mar 14, 2026
376164a
docs(db): explain why visible_seqno uses separate default counter
polaz Mar 14, 2026
45979fd
docs(builder): show seqno_generator usage in doctest example
polaz Mar 14, 2026
9cdad86
test(seqno): tighten recovery assertion to check state-advancing calls
polaz Mar 14, 2026
8a93045
Merge branch 'main' into feat/#174-custom-sequencenumbergenerator-imp…
polaz Mar 15, 2026
3692338
Merge remote-tracking branch 'origin/main' into feat/#174-custom-sequ…
polaz Mar 15, 2026
c4d16d1
Merge branch 'main' into feat/#174-custom-sequencenumbergenerator-imp…
polaz Mar 16, 2026
85e7f9a
Merge branch 'main' into feat/#174-custom-sequencenumbergenerator-imp…
polaz Mar 16, 2026
251a7d0
fix(deps): use fork lsm-tree with SequenceNumberGenerator trait
polaz Mar 16, 2026
c713a47
refactor(db): extract seqno init helper, clean unused imports
polaz Mar 16, 2026
7d5372e
docs(builder): use fjall re-exports in doc examples and links
polaz Mar 16, 2026
9695eb5
chore(deps): annotate lsm-tree fork as alpha-phase dependency
polaz Mar 16, 2026
29fcab4
chore(deps): pin lsm-tree fork to specific rev
polaz Mar 16, 2026
08c3211
revert: restore branch=main for lsm-tree fork dependency
polaz Mar 16, 2026
7e27512
docs(copilot): add dependency policy for lsm-tree branch=main
polaz Mar 16, 2026
da039eb
chore(deps): deduplicate lsm-tree annotation in Cargo.toml
polaz Mar 16, 2026
db0de32
docs(db): qualify SequenceNumberCounter intra-doc links
polaz Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ This is a **maintained fork** of [fjall-rs/fjall](https://github.com/fjall-rs/fj

**Prefer issue suggestions over code suggestions for out-of-scope findings.** This keeps PRs focused and reviewable.

## Dependency Policy

**`lsm-tree` uses `branch = "main"` intentionally.** This fork is in active alpha development alongside `structured-world/lsm-tree`. The `branch = "main"` git dependency is correct because:
- Both repos are under active co-development with frequent cross-repo changes
- Dependabot tracks and updates git branch dependencies
- Pinning to `rev` would block Dependabot and require manual bumps on every lsm-tree commit
- Will switch to a crates.io version once upstream fjall-rs/lsm-tree#265 is released

**Do NOT suggest** pinning to `rev = "..."` or switching to a crates.io version for `lsm-tree`. This is a deliberate alpha-phase decision.

## Rust Code Standards

- **Unsafe code:** Prefer safe alternatives. `unsafe` requires `// SAFETY:` comment.
Expand Down
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ __internal_whitebox = []
[dependencies]
byteorder = { package = "byteorder-lite", version = "0.1.0" }
byteview = "~0.10.1"
lsm-tree = { version = "~3.1.1", default-features = false, features = [] }
# Fork with SequenceNumberGenerator trait (structured-world/lsm-tree#10).
# branch=main: active development, Dependabot tracks updates.
# Switch to crates.io once fjall-rs/lsm-tree#265 is released.
lsm-tree = { git = "https://github.com/structured-world/lsm-tree.git", branch = "main", default-features = false, features = [] }
log = "0.4.27"
tempfile = "3.20.0"
dashmap = "6.1.0"
Expand Down
39 changes: 38 additions & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// (found in the LICENSE-* files in the repository)

use crate::{db_config::CompactionFilterAssigner, tx::single_writer::Openable, Config};
use lsm_tree::{Cache, CompressionType, DescriptorTable};
use lsm_tree::{Cache, CompressionType, DescriptorTable, SharedSequenceNumberGenerator};
use std::{marker::PhantomData, path::Path, sync::Arc};

/// Database builder
Expand Down Expand Up @@ -188,4 +188,41 @@ impl<O: Openable> Builder<O> {
self.inner.compaction_filter_factory_assigner = Some(f);
self
}

/// Sets a custom sequence number generator for write operations.
///
/// Takes `SharedSequenceNumberGenerator` (`Arc<dyn SequenceNumberGenerator>`)
/// directly because the generator must be shared across all keyspaces and the
/// internal storage layer. Accepting a generic would add a type parameter to
/// `Builder` that propagates through `Config` and `Database`.
///
/// By default, the database uses [`crate::SequenceNumberCounter`],
/// a simple atomic counter. Use this to plug in a custom generator,
/// e.g., a Hybrid Logical Clock (HLC) for distributed databases.
///
/// The generator is shared across all keyspaces and is used for
/// write sequencing (assigning seqnos to new writes). The MVCC
/// visibility watermark is managed internally by the snapshot tracker.
///
/// # Examples
///
/// ```
/// use fjall::Database;
/// use fjall::{SharedSequenceNumberGenerator, SequenceNumberCounter};
/// use std::sync::Arc;
///
/// # let folder = tempfile::tempdir()?;
/// let generator: SharedSequenceNumberGenerator =
/// Arc::new(SequenceNumberCounter::default());
///
/// let db = Database::builder(&folder)
/// .seqno_generator(generator)
/// .open()?;
/// # Ok::<(), fjall::Error>(())
/// ```
#[must_use]
pub fn seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
self.inner.seqno_generator = Some(generator);
self
}
}
38 changes: 28 additions & 10 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
write_buffer_manager::WriteBufferManager,
HashMap, Keyspace, KeyspaceCreateOptions,
};
use lsm_tree::{AbstractTree, SequenceNumberCounter};
use lsm_tree::{AbstractTree, SequenceNumberCounter, SharedSequenceNumberGenerator};
use std::{
fs::remove_dir_all,
path::Path,
Expand Down Expand Up @@ -56,7 +56,7 @@ pub struct DatabaseInner {

pub(crate) stats: Arc<Stats>,

pub(crate) keyspace_id_counter: SequenceNumberCounter,
pub(crate) keyspace_id_counter: SharedSequenceNumberGenerator,

pub worker_pool: WorkerPool,

Expand Down Expand Up @@ -582,6 +582,26 @@ impl Database {
self.supervisor.snapshot_tracker.get()
}

/// Creates the write seqno generator and a separate MVCC visibility watermark.
///
/// The write generator comes from the user-supplied config (or falls back to
/// the default [`SequenceNumberCounter`]). The visibility watermark is always
/// a fresh counter — it tracks "up to which seqno are writes visible", not
/// "generate the next seqno".
fn make_seqno_generators(
config: &Config,
) -> (SharedSequenceNumberGenerator, SharedSequenceNumberGenerator) {
let seqno: SharedSequenceNumberGenerator = config
.seqno_generator
.clone()
.unwrap_or_else(|| Arc::new(SequenceNumberCounter::default()));

let visible_seqno: SharedSequenceNumberGenerator =
Arc::new(SequenceNumberCounter::default());

(seqno, visible_seqno)
}

fn check_version<P: AsRef<Path>>(path: P) -> crate::Result<()> {
let bytes = std::fs::read(path.as_ref().join(VERSION_MARKER))?;

Expand Down Expand Up @@ -631,15 +651,14 @@ impl Database {

let journal_manager = JournalManager::new();

let seqno = SequenceNumberCounter::default();
let visible_seqno = SequenceNumberCounter::default();
let (seqno, visible_seqno) = Self::make_seqno_generators(&config);

let keyspaces = Arc::new(RwLock::new(Keyspaces::with_capacity_and_hasher(
10,
xxhash_rust::xxh3::Xxh3Builder::new(),
)));

let meta_tree = lsm_tree::Config::new(
let meta_tree = lsm_tree::Config::new_with_generators(
config.path.join(KEYSPACES_FOLDER).join("0"),
seqno.clone(),
visible_seqno.clone(),
Expand Down Expand Up @@ -690,7 +709,7 @@ impl Database {
let inner = DatabaseInner {
supervisor,
worker_pool: WorkerPool::prepare(),
keyspace_id_counter: SequenceNumberCounter::new(1),
keyspace_id_counter: Arc::new(SequenceNumberCounter::new(1)),
meta_keyspace: meta_keyspace.clone(),
config,
stop_signal: lsm_tree::stop_signal::StopSignal::default(),
Expand Down Expand Up @@ -877,15 +896,14 @@ impl Database {
fsync_directory(&keyspaces_folder_path)?;
fsync_directory(&config.path)?;

let seqno = SequenceNumberCounter::default();
let visible_seqno = SequenceNumberCounter::default();
let (seqno, visible_seqno) = Self::make_seqno_generators(&config);

let keyspaces = Arc::new(RwLock::new(Keyspaces::with_capacity_and_hasher(
10,
xxhash_rust::xxh3::Xxh3Builder::new(),
)));

let meta_tree = lsm_tree::Config::new(
let meta_tree = lsm_tree::Config::new_with_generators(
config.path.join(KEYSPACES_FOLDER).join("0"),
seqno.clone(),
visible_seqno.clone(),
Expand Down Expand Up @@ -935,7 +953,7 @@ impl Database {
let inner = DatabaseInner {
supervisor,
worker_pool: WorkerPool::prepare(),
keyspace_id_counter: SequenceNumberCounter::new(1),
keyspace_id_counter: Arc::new(SequenceNumberCounter::new(1)),
meta_keyspace,
config,
stop_signal: lsm_tree::stop_signal::StopSignal::default(),
Expand Down
9 changes: 8 additions & 1 deletion src/db_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// (found in the LICENSE-* files in the repository)

use crate::path::absolute_path;
use lsm_tree::{Cache, CompressionType, DescriptorTable};
use lsm_tree::{Cache, CompressionType, DescriptorTable, SharedSequenceNumberGenerator};
use std::{
path::{Path, PathBuf},
sync::Arc,
Expand Down Expand Up @@ -50,6 +50,12 @@ pub struct Config {
// pub(crate) journal_recovery_mode: RecoveryMode,
//
pub(crate) compaction_filter_factory_assigner: Option<CompactionFilterAssigner>,

/// Custom sequence number generator.
///
/// When set, this generator is used instead of the default
/// [`lsm_tree::SequenceNumberCounter`].
pub(crate) seqno_generator: Option<SharedSequenceNumberGenerator>,
}

const DEFAULT_CPU_CORES: usize = 4;
Expand Down Expand Up @@ -92,6 +98,7 @@ impl Config {
cache: Arc::new(Cache::with_capacity_bytes(/* 32 MiB */ 32 * 1_024 * 1_024)),

compaction_filter_factory_assigner: None,
seqno_generator: None,
}
}
}
2 changes: 1 addition & 1 deletion src/keyspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl Keyspace {

std::fs::create_dir_all(&base_folder)?;

let base_config = lsm_tree::Config::new(
let base_config = lsm_tree::Config::new_with_generators(
base_folder,
db.supervisor.seqno.clone(),
db.supervisor.snapshot_tracker.get_ref(),
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ pub use tx::optimistic::{
pub use lsm_tree::{AbstractTree, AnyTree, Error as LsmError, TreeType};

pub use lsm_tree::{
CompressionType, KvPair, KvSeparationOptions, SeqNo, Slice, UserKey, UserValue,
CompressionType, KvPair, KvSeparationOptions, SeqNo, SequenceNumberCounter,
SequenceNumberGenerator, SharedSequenceNumberGenerator, Slice, UserKey, UserValue,
};

/// Utility functions
Expand Down
10 changes: 5 additions & 5 deletions src/meta_keyspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use crate::{db::Keyspaces, keyspace::InternalKeyspaceId, Keyspace};
use byteview::StrView;
use lsm_tree::{AbstractTree, AnyTree, SeqNo, SequenceNumberCounter, UserValue};
use lsm_tree::{AbstractTree, AnyTree, SeqNo, SharedSequenceNumberGenerator, UserValue};
use std::sync::{Arc, RwLock, RwLockWriteGuard};

pub fn encode_config_key(
Expand All @@ -30,16 +30,16 @@ pub struct MetaKeyspace {
#[doc(hidden)]
pub keyspaces: Arc<RwLock<Keyspaces>>,

seqno_generator: SequenceNumberCounter,
visible_seqno: SequenceNumberCounter,
seqno_generator: SharedSequenceNumberGenerator,
visible_seqno: SharedSequenceNumberGenerator,
}

impl MetaKeyspace {
pub(crate) fn new(
inner: AnyTree,
keyspaces: Arc<RwLock<Keyspaces>>,
seqno_generator: SequenceNumberCounter,
visible_seqno: SequenceNumberCounter,
seqno_generator: SharedSequenceNumberGenerator,
visible_seqno: SharedSequenceNumberGenerator,
) -> Self {
Self {
inner,
Expand Down
2 changes: 1 addition & 1 deletion src/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub fn recover_keyspaces(db: &Database, meta_keyspace: &MetaKeyspace) -> crate::
recovered_config = recovered_config.with_compaction_filter_factory(f);
}

let base_config = lsm_tree::Config::new(
let base_config = lsm_tree::Config::new_with_generators(
path,
db.supervisor.seqno.clone(),
db.supervisor.snapshot_tracker.get_ref(),
Expand Down
23 changes: 12 additions & 11 deletions src/snapshot_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

use crate::{snapshot_nonce::SnapshotNonce, SeqNo};
use dashmap::DashMap;
use lsm_tree::SequenceNumberCounter;
use lsm_tree::SharedSequenceNumberGenerator;
use std::sync::{atomic::AtomicU64, Arc, RwLock};

/// Keeps track of open snapshots
pub struct SnapshotTrackerInner {
seqno: SequenceNumberCounter,
seqno: SharedSequenceNumberGenerator,

gc_lock: RwLock<()>,

Expand All @@ -33,7 +33,7 @@ impl std::ops::Deref for SnapshotTracker {
}

impl SnapshotTracker {
pub fn new(seqno: SequenceNumberCounter) -> Self {
pub fn new(seqno: SharedSequenceNumberGenerator) -> Self {
Self(Arc::new(SnapshotTrackerInner {
data: DashMap::default(),
freed_count: AtomicU64::default(),
Expand All @@ -43,7 +43,7 @@ impl SnapshotTracker {
}))
}

pub fn get_ref(&self) -> SequenceNumberCounter {
pub fn get_ref(&self) -> SharedSequenceNumberGenerator {
self.seqno.clone()
}

Expand Down Expand Up @@ -183,6 +183,7 @@ impl SnapshotTracker {
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
use lsm_tree::SequenceNumberCounter;
use test_log::test;

#[test]
Expand Down Expand Up @@ -285,7 +286,7 @@ mod tests {
fn snapshot_tracker_basic() {
let global_seqno = SequenceNumberCounter::default();

let map = SnapshotTracker::new(global_seqno.clone());
let map = SnapshotTracker::new(global_seqno.clone().into());

let nonce = map.open();
assert_eq!(0, nonce.instant);
Expand All @@ -304,7 +305,7 @@ mod tests {
fn snapshot_tracker_increase_watermark() {
let global_seqno = SequenceNumberCounter::default();

let map = SnapshotTracker::new(global_seqno.clone());
let map = SnapshotTracker::new(global_seqno.clone().into());

// Simulates some tx committing
for _ in 0..100_000 {
Expand All @@ -320,7 +321,7 @@ mod tests {
fn snapshot_tracker_prevent_watermark() {
let global_seqno = SequenceNumberCounter::default();

let map = SnapshotTracker::new(global_seqno.clone());
let map = SnapshotTracker::new(global_seqno.clone().into());

// This nonce prevents the watermark from increasing
let _old_nonce = map.open();
Expand All @@ -342,7 +343,7 @@ mod tests {
#[test]
fn snapshot_tracker_close_never_opened_does_not_underflow_or_panic() {
let global_seqno = SequenceNumberCounter::default();
let map = SnapshotTracker::new(global_seqno);
let map = SnapshotTracker::new(global_seqno.into());

assert_eq!(map.len(), 0);
map.close_raw(42);
Expand All @@ -352,7 +353,7 @@ mod tests {
#[test]
fn snapshot_tracker_concurrent_open_same_seqno_counts_correctly() {
let global_seqno = SequenceNumberCounter::default();
let map = SnapshotTracker::new(global_seqno);
let map = SnapshotTracker::new(global_seqno.into());

// make sure seqno doesn't change between two opens
let n1 = map.open();
Expand All @@ -372,7 +373,7 @@ mod tests {
#[test]
fn snapshot_tracker_publish_moves_seqno_forward_and_ignores_older() {
let global_seqno = SequenceNumberCounter::default();
let map = SnapshotTracker::new(global_seqno);
let map = SnapshotTracker::new(global_seqno.into());

let big = 100u64;
map.publish(big);
Expand All @@ -386,7 +387,7 @@ mod tests {
#[test]
fn snapshot_tracker_clone_snapshot_behaves_like_second_open() {
let global_seqno = SequenceNumberCounter::default();
let map = SnapshotTracker::new(global_seqno);
let map = SnapshotTracker::new(global_seqno.into());

let orig = map.open();
let clone = map.clone_snapshot(&orig);
Expand Down
4 changes: 2 additions & 2 deletions src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::SequenceNumberCounter;
use lsm_tree::SharedSequenceNumberGenerator;

use crate::{
db::Keyspaces,
Expand All @@ -23,7 +23,7 @@ pub struct SupervisorInner {
pub(crate) write_buffer_size: WriteBufferManager,
pub(crate) flush_manager: FlushManager,

pub seqno: SequenceNumberCounter,
pub seqno: SharedSequenceNumberGenerator,

pub snapshot_tracker: SnapshotTracker,

Expand Down
Loading
Loading