Skip to content

use the SuperVersion seqno for get_highest_persisted_seqno if the tree is totally empty#293

Open
svix-jbrown wants to merge 4 commits into
fjall-rs:mainfrom
svix-jbrown:jbrown/always-store-seqno
Open

use the SuperVersion seqno for get_highest_persisted_seqno if the tree is totally empty#293
svix-jbrown wants to merge 4 commits into
fjall-rs:mainfrom
svix-jbrown:jbrown/always-store-seqno

Conversation

@svix-jbrown

@svix-jbrown svix-jbrown commented Jun 1, 2026

Copy link
Copy Markdown

I talked to @marvin-j97 in discord and they suggest that this would be a simpler/better alternative to #291.

If a tree is totally empty (no tables on disk, nothing in the memtables; e.g., the state right after a clear), then we use the seqno of the SuperVersion as the highest persisted seqno.

I believe that this will fix fjall-rs/fjall#288

Summary by CodeRabbit

  • Bug Fixes

    • More reliable tracking of a tree's highest persisted sequence number so open, insert, clear, and reopen report consistent persisted-seqno state.
  • New Features

    • Trees now record and restore an optional creation seqno and initialize from the configured start seqno.
    • Drop/compaction now records explicit seqno markers for persisted history.
  • Tests

    • Strengthened tests validating persisted-seqno transitions across insert, clear, persist, and reopen.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56b6ae5d-4d14-4f6b-9b78-0d2c59d44cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 4de9b2d and 915f80f.

📒 Files selected for processing (8)
  • src/compaction/worker.rs
  • src/tree/inner.rs
  • src/tree/mod.rs
  • src/version/mod.rs
  • src/version/recovery.rs
  • src/version/super_version.rs
  • tests/tree_clear.rs
  • tests/tree_seqno.rs

📝 Walkthrough

Walkthrough

Adds a Version.created_seqno propagated from recovery/creation and preserved across version transforms; introduces SuperVersion::is_empty and updates persisted-seqno logic and Tree.clear to use created_seqno when appropriate; tests assert persisted-seqno transitions and persistence across reopen.

Changes

Idle keyspace persisted seqno fix

Layer / File(s) Summary
Version imports and created_seqno field
src/version/mod.rs
Add SeqNo import and VersionInner::created_seqno: Option<SeqNo> to track an optional creation seqno for persisted versions.
Version constructors and wiring
src/version/mod.rs, src/tree/inner.rs
Introduce Version::new_at_seqno(..., created_seqno), make Version::new delegate to it, and initialize the first persisted Version with config.seqno.get() on tree creation.
Recovery plumbing
src/version/recovery.rs, src/version/mod.rs
Read optional created_seqno from manifest TOC into Recovery, forward recovery.created_seqno into Version::from_levels/materialization so constructed VersionInner stores it.
Version transform helpers and encoding
src/version/mod.rs
Propagate created_seqno through with_new_l0_run, with_moved, and add with_dropped_at/with_merge_at variants that accept seqno: Option<SeqNo>; when transforms produce an empty version, set created_seqno = self.created_seqno.max(seqno); encode created_seqno conditionally in manifest.
SuperVersion emptiness and persisted-seqno computation
src/version/super_version.rs, src/tree/mod.rs
Add SuperVersion::is_empty(). Change Tree::get_highest_persisted_seqno() to read version_history.latest_version() into a SuperVersion: return version.created_seqno when the SuperVersion is empty, otherwise return max Table::get_highest_seqno() across tables.
Tree clear and compaction drop timestamping
src/tree/mod.rs, src/tree/inner.rs, src/compaction/worker.rs
Tree::clear and initial tree creation now use Version::new_at_seqno(..., Some(config.seqno.get())). compaction::worker::drop_tables uses with_dropped_at(..., Some(opts.global_seqno.get())) to record drop-at seqno.
Tests: persisted-seqno expectations
tests/tree_clear.rs, tests/tree_seqno.rs
Update tests to expect get_highest_persisted_seqno() reflects Some(0) on newly-opened empty trees, to observe transitions to None/Some(...) around memtable and clear, and to persist the created_seqno across reopen.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #288 — The changes address the reported idle-keyspace/journal accumulation by storing and using a version-born seqno so persisted seqno can advance even when SuperVersion has no table files.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly describes the main change: using SuperVersion seqno for get_highest_persisted_seqno when the tree is empty, which is the core objective of the PR.
Linked Issues check ✅ Passed The PR addresses the root cause identified in issue #288 by modifying get_highest_persisted_seqno to return SuperVersion seqno when the tree is empty, enabling sealed journals to advance past empty keyspaces.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the SuperVersion seqno tracking mechanism: Version now tracks created_seqno, SuperVersion checks emptiness, recovery loads persisted seqno, and tree operations preserve/update it appropriately.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tree/mod.rs`:
- Around line 632-643: get_highest_persisted_seqno currently returns
super_version.seqno for empty super versions, but that seqno is only in-memory
and not persisted so recovery resets it to 0; modify the persistence path so the
empty-tree seqno is stored in the manifest (or other durable metadata) when
upgrading versions (update upgrade_version_with_seqno to write the seqno
alongside next_version.version) and restore it during recovery (ensure
SuperVersions::new/recover reads the persisted seqno and sets the SuperVersion
field accordingly) or alternatively derive the durable seqno from existing
on-disk artifacts (e.g., sealed journals) before returning in
get_highest_persisted_seqno.

In `@tests/tree_clear.rs`:
- Around line 27-30: After calling tree.clear(), add a reopen/recovery step to
exercise the post-recovery bug: close or drop the current tree, reopen it (so
recover() runs), then re-check tree.get_highest_persisted_seqno() and assert it
still returns Some(1); this ensures the test exercises the recovery path and
will catch loss of seqno state after clear()/recover().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db28bf42-9030-45b7-87db-72a3901071ee

📥 Commits

Reviewing files that changed from the base of the PR and between ae11d85 and 3a43369.

📒 Files selected for processing (3)
  • src/tree/mod.rs
  • src/version/super_version.rs
  • tests/tree_clear.rs

Comment thread tests/tree_clear.rs Outdated
@svix-jbrown svix-jbrown marked this pull request as draft June 1, 2026 23:35
@svix-jbrown svix-jbrown force-pushed the jbrown/always-store-seqno branch from 3a43369 to f0d2a93 Compare June 1, 2026 23:57
@svix-jbrown svix-jbrown marked this pull request as ready for review June 2, 2026 00:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/tree/mod.rs`:
- Around line 636-647: The method get_highest_persisted_seqno uses
version_history.read().expect("lock is poisoned") but is missing the
#[expect(clippy::expect_used)] attribute that other similar methods (e.g.,
get_highest_memtable_seqno) include; add the #[expect(clippy::expect_used)]
attribute immediately above the fn get_highest_persisted_seqno declaration so
the use of expect for the lock is consistent with the rest of the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a639750c-a4ca-44f6-9bee-d55c1b5304ae

📥 Commits

Reviewing files that changed from the base of the PR and between 3a43369 and f0d2a93.

📒 Files selected for processing (6)
  • src/tree/inner.rs
  • src/tree/mod.rs
  • src/version/mod.rs
  • src/version/recovery.rs
  • src/version/super_version.rs
  • tests/tree_clear.rs

Comment thread src/tree/mod.rs
Comment thread src/tree/mod.rs Outdated
Comment thread src/version/mod.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/version/mod.rs (1)

402-410: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Recompute created_seqno when a derived version becomes empty.

These helpers always carry forward the old self.created_seqno. That breaks the new empty-tree contract once a later transform removes the last persisted table: with_dropped() can drop the final table, and with_merge() can legally produce new_tables = [], leaving an empty version that still reports the seqno from the last create/clear instead of the seqno that was just persisted. Since Tree::get_highest_persisted_seqno() now returns version.created_seqno for empty SuperVersions, persisted seqno can move backwards after compaction/drop-to-empty. Please thread the new persisted seqno into the transforms that can yield an empty version instead of always cloning the old field.

Also applies to: 487-495, 569-577, 618-626

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/version/mod.rs` around lines 402 - 410, The new Version construction is
incorrectly copying created_seqno from self (created_seqno: self.created_seqno)
even when transforms produce an empty version; update the constructors that
build VersionInner (the blocks setting created_seqno) to compute and thread the
correct persisted seqno when new_tables/new_levels are empty by calling/getting
the current highest persisted seqno (e.g. via
Tree::get_highest_persisted_seqno() or a persisted_seqno argument) instead of
blindly cloning self.created_seqno; apply the same change in the other similar
VersionInner construction sites referenced (the blocks around the ranges
487-495, 569-577, 618-626) and ensure with_dropped() and with_merge() pass the
recomputed persisted_seqno into the VersionInner created_seqno field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/version/mod.rs`:
- Around line 402-410: The new Version construction is incorrectly copying
created_seqno from self (created_seqno: self.created_seqno) even when transforms
produce an empty version; update the constructors that build VersionInner (the
blocks setting created_seqno) to compute and thread the correct persisted seqno
when new_tables/new_levels are empty by calling/getting the current highest
persisted seqno (e.g. via Tree::get_highest_persisted_seqno() or a
persisted_seqno argument) instead of blindly cloning self.created_seqno; apply
the same change in the other similar VersionInner construction sites referenced
(the blocks around the ranges 487-495, 569-577, 618-626) and ensure
with_dropped() and with_merge() pass the recomputed persisted_seqno into the
VersionInner created_seqno field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21924293-7cd7-450b-bd81-53f764225db7

📥 Commits

Reviewing files that changed from the base of the PR and between f0d2a93 and 6de5416.

📒 Files selected for processing (3)
  • src/tree/mod.rs
  • src/version/mod.rs
  • tests/tree_seqno.rs

@svix-jbrown svix-jbrown force-pushed the jbrown/always-store-seqno branch from 4de9b2d to c967fda Compare June 2, 2026 23:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/version/mod.rs`:
- Around line 499-505: The created_seqno calculation uses Option::max which
wrongly preserves an older created_seqno when the tree becomes empty; in the
block computing created_seqno (variables: table_count, levels,
self.created_seqno, seqno) replace the use of self.created_seqno.max(seqno) with
logic that prefers the provided drop seqno when present and falls back to the
existing created_seqno (i.e., use seqno.or(self.created_seqno)); this ensures
when table_count == 0 we record the most recent emptying seqno and otherwise
keep self.created_seqno unchanged.
- Around line 610-616: The reset logic for created_seqno incorrectly uses
self.created_seqno.max(seqno) which can keep a stale higher value when the tree
becomes empty; change the assignment to prefer the new seqno when present by
using seqno.or(self.created_seqno) instead (i.e., set created_seqno =
seqno.or(self.created_seqno)) so the new seqno is used if Some, otherwise fall
back to the existing self.created_seqno; update the expression around
table_count, created_seqno and seqno accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8119489b-7dfb-410d-aa2c-3a8859d8bfec

📥 Commits

Reviewing files that changed from the base of the PR and between 4de9b2d and c967fda.

📒 Files selected for processing (8)
  • src/compaction/worker.rs
  • src/tree/inner.rs
  • src/tree/mod.rs
  • src/version/mod.rs
  • src/version/recovery.rs
  • src/version/super_version.rs
  • tests/tree_clear.rs
  • tests/tree_seqno.rs

Comment thread src/version/mod.rs
Comment thread src/version/mod.rs
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.18072% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/version/mod.rs 94.00% 3 Missing ⚠️
src/compaction/worker.rs 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@svix-jbrown svix-jbrown requested a review from marvin-j97 June 10, 2026 15:58
Comment thread src/version/mod.rs
Comment on lines 216 to +219
/// Creates a new empty version.
pub fn new(id: VersionId, tree_type: TreeType) -> Self {
Self::new_at_seqno(id, tree_type, None)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be needed anymore - we always build Versions with seqno now anyway?

Comment thread src/version/mod.rs
}

/// Creates a new empty version at the given sequence number
pub fn new_at_seqno(id: VersionId, tree_type: TreeType, created_seqno: Option<SeqNo>) -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_seqno: Option<SeqNo> -> created_seqno: SeqNo

Comment thread src/compaction/worker.rs
copy.version = copy.version.with_dropped_at(
ids_to_drop,
&mut dropped_blob_files,
Some(opts.global_seqno.get()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be opts.global_seqno.next(). Otherwise two compactions being installed in quick succession could, technically, have the same seqno.

Comment thread src/tree/inner.rs
} else {
crate::TreeType::Standard
},
Some(config.seqno.get()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Comment thread src/tree/mod.rs
copy.version = Version::new_at_seqno(
v.version.id() + 1,
self.tree_type(),
Some(config.seqno.get()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here.

Comment thread src/version/mod.rs
// reset the created_seqno if the tree has newly become empty
let table_count: usize = levels.iter().map(|x| x.table_count()).sum();
let created_seqno = if table_count == 0 {
self.created_seqno.max(seqno)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this even possible? Why should the persisted seqno be possibly higher than the next, newer version?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

idle keyspaces leads to unbounded sealed journals

2 participants