Skip to content

feat(fs): MemFs — in-memory Fs implementation for testing and in-memory trees#208

Closed
polaz wants to merge 26 commits into
mainfrom
feat/#187-featfs-memfs--in-memory-fs-implementation-for-test
Closed

feat(fs): MemFs — in-memory Fs implementation for testing and in-memory trees#208
polaz wants to merge 26 commits into
mainfrom
feat/#187-featfs-memfs--in-memory-fs-implementation-for-test

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 3, 2026

Summary

  • Implement MemFs + MemFileHashMap<PathBuf, Arc<Mutex<Vec<u8>>>>-backed virtual filesystem implementing the Fs trait
  • Add Config::with_fs() builder method for plugging alternative Fs backends
  • De-generify Config<F: Fs>Config with Arc<dyn Fs> (breaking for Config<CustomFs> users — none exist in practice since the generic was introduced in this fork)
  • Rewrite rewrite_atomic to use Fs trait instead of tempfile crate (fixes refactor(fs): rewrite_atomic — replace tempfile crate with Fs-trait atomic write #188), with PID+seq temp naming, retry on AlreadyExists, and best-effort cleanup
  • Pipe Arc<dyn Fs> through Table::recover, FileAccessor, load_block, and vlog paths so all file opens go through the pluggable Fs
  • Wire correct level-routed Fs to all Table::recover call sites (recovery uses the same backend that wrote the table)
  • Restore table_file_opened_cached/uncached metrics via (fd, cache_hit) return from FileAccessor (pinned FDs are not counted as cache hits)
  • Replace std::fs::read_dir / try_exists in vlog recovery with Fs trait calls
  • Document atomic-replace semantics on Fs::rename trait method
  • Bump rust-toolchain.toml from 1.94.0 → 1.94.1 to match CI rustfmt version

Design Notes

  • Blob recovery NotFound handling: recover_blob_files returns Ok([], []) when the blobs folder doesn't exist and ids is empty (standard non-blob trees). When ids is non-empty (manifest references blob files), a missing folder is unrecoverable corruption and returns Err(Unrecoverable).
  • lock_exclusive no-op in MemFs: In-memory files are not shared across processes. MemFs is a test/ephemeral backend — cross-process exclusivity is not meaningful.

Known Limitations

Test Plan

  • 21 unit tests for MemFs (all Fs/FsFile methods + rename-replace)
  • 7 integration tests (open tree, write+read, flush+read from SST, delete+range, multiple flushes, shared MemFs, vlog recovery)
  • 1065 existing tests pass (0 regressions)
  • Clippy clean (all targets, all features)
  • cargo fmt -- --check clean (rustfmt 1.94.1)

Closes #187
Closes #188

Summary by CodeRabbit

  • New Features

    • In-memory filesystem backend and Config builders to plug shared/custom filesystem backends.
  • Improvements

    • Pluggable filesystem now used consistently for recovery, reads, writes and flushes.
    • Safer atomic file rewrites with deterministic temp files and mandatory directory fsync.
    • Simplified file-access/caching and clarified rename semantics; safer descriptor eviction on delete.
  • Tests

    • New and updated tests exercising MemFs, recovery, multi-flush, and tree behaviors.
  • Chores

    • Rust toolchain bumped to 1.94.1.

polaz added 2 commits April 3, 2026 19:55
…paths

Implement MemFs — a HashMap-backed in-memory Fs implementation for testing
and ephemeral trees. Wire Arc<dyn Fs> through the entire I/O stack so that
all file operations go through the pluggable Fs trait.

MemFs implementation:
- MemFile with Read/Write/Seek/read_at backed by Arc<Mutex<Vec<u8>>>
- Full Fs trait: open, create_dir_all, read_dir, remove_file,
  remove_dir_all, rename, metadata, sync_directory, exists
- Validates parent dirs, rejects dir-file conflicts, mirrors std::fs
  semantics (InvalidInput for truncate/create without write access)
- 20 unit tests + 5 integration tests (open tree, flush, read, range)

Config changes:
- De-generify Config<F: Fs> to Config with Arc<dyn Fs>
- Add Config::with_fs() builder for alternative backends

Fs piping (all file opens now go through Fs):
- rewrite_atomic: Fs-native temp write with PID+seq naming and
  best-effort cleanup on all failure paths (replaces tempfile crate)
- Table::recover accepts Arc<dyn Fs>, opens files through Fs
- FileAccessor::get_or_open_table returns (fd, cache_hit) for metrics
- Wire correct level-routed Fs to all 6 Table::recover call sites
- vlog recovery: Fs-based read_dir, exists, sfa::Reader::from_reader
- Blob recovery: pre-populate DescriptorTable FD cache, skip dirs
  before parse, eliminate TOCTOU with direct read_dir
- Document Fs::rename atomic-replace semantics

Closes #187
Closes #188
Copilot AI review requested due to automatic review settings April 3, 2026 16:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Warning

Rate limit exceeded

@polaz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 23 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 23 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 61ba59d4-fb1c-4279-a508-1e282dc82b69

📥 Commits

Reviewing files that changed from the base of the PR and between e911497 and 093765c.

📒 Files selected for processing (2)
  • src/file_accessor.rs
  • src/table/inner.rs
📝 Walkthrough

Walkthrough

Threads an Arc filesystem abstraction through config, writers, recoveries, accessors, ingestion, compaction, and vlog; adds an in-memory MemFs backend and tests; replaces tempfile-based atomic rewrite with an Fs-backed implementation; updates Table::recover and many call sites to accept and use an explicit Fs handle.

Changes

Cohort / File(s) Summary
Config & Fs surface
src/config/mod.rs, src/fs/mod.rs
De-genericized Config to store Arc<dyn Fs>; added with_fs / with_shared_fs; exported MemFs; updated Fs::rename docs.
In-memory FS & tests
src/fs/mem_fs.rs, tests/mem_fs_tree.rs
Added MemFs (in-memory Fs implementation) and integration/unit tests exercising trees using MemFs and shared-Fs scenarios.
Atomic rewrite & directory sync
src/file.rs
Rewrote rewrite_atomic to use &dyn Fs with deterministic temp paths, create_new loop, write+fsync, rename via fs, and always fsync parent dir; simplified fsync_directory.
File accessor & descriptor cache API
src/file_accessor.rs, src/table/util.rs, src/vlog/accessor.rs
FileAccessor::DescriptorTable changed to struct { table, fs }; removed old cache-only accessors; added get_or_open_*/remove_* APIs and updated call sites to use unified get-or-open flow returning file handle + cache event.
Table surface & inner state
src/table/mod.rs, src/table/inner.rs, src/table/multi_writer.rs, src/table/tests.rs, src/table/util.rs
Table::recover now takes fs: Arc<dyn Fs> and opens via fs.open; Table/Inner store fs and use it for file ops/deletions; tests and MultiWriter updated to pass Arc::new(StdFs) where needed.
Writers, ingestion, and per-level routing
src/tree/..., src/blob_tree/..., src/tree/ingest.rs, src/blob_tree/ingest.rs
Threaded level_fs: Arc<dyn Fs> through MultiWriter, ingestion, and recovery; clone level_fs at call sites to preserve reuse; adjusted docs about Tree::open using std::fs for reopen limitations.
Compaction & vlog flows
src/compaction/flavour.rs, src/vlog/blob_file/multi_writer.rs, src/vlog/mod.rs
Pass Arc<dyn Fs> into consume/recover paths; switch reads and directory ops to fs.open / fs.read_dir; defer descriptor-table cache inserts until parsing succeeds; adjust recover semantics for missing folders.
Benchmarks & toolchain
benches/index_block.rs, rust-toolchain.toml
Updated benchmarks/test call sites to pass Arc::new(StdFs) for new recover signature; bumped Rust toolchain patch.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Table as Table::recover
    participant FileAccessor
    participant Descriptor as DescriptorTable
    participant Fs as Arc("dyn Fs")

    Caller->>Table: recover(table_id, ..., fs: Arc<dyn Fs>)
    Table->>FileAccessor: get_or_open_table(table_id, path)
    FileAccessor->>Descriptor: check descriptor cache
    alt cache miss
        FileAccessor->>Fs: open(path, read)
        Fs-->>FileAccessor: FsFile handle
        FileAccessor->>Descriptor: insert { table, fs: fs.clone() }
    end
    FileAccessor-->>Table: (FsFile, Option<bool>)
    Table->>Fs: read via FsFile (ParsedMeta::load_with_handle(&*file))
    Table-->>Caller: constructed Table (Inner holds Arc<dyn Fs>)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #188: refactor(fs): rewrite_atomic — replace tempfile crate with Fs-trait atomic write — this PR implements the Fs-backed atomic rewrite described.
  • #187: feat(fs): MemFs — in-memory Fs implementation for testing and in-memory trees — this PR adds MemFs and tests matching that objective.
  • refactor(fs): migrate Tree::open recovery path to Fs trait #209 — similar per-level Fs routing and std::fs → Fs trait migration in recovery/read_dir paths.

Possibly related PRs

Poem

🐇 I stitched a soft MemFs den,

bytes tucked snug in RAM again.
Arc I clutch and hop,
temp files rename—no disk to drop.
Hooray, the tree grows light and pop!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the primary feature addition: MemFs, an in-memory Fs implementation for testing and in-memory trees.
Linked Issues check ✅ Passed The PR comprehensively addresses both linked issues #187 (MemFs implementation) and #188 (rewrite_atomic refactor to use Fs trait instead of tempfile), with all key coding requirements met.
Out of Scope Changes check ✅ Passed All changes are scoped to the objectives: MemFs implementation, rewrite_atomic refactoring, Config generics removal, and supporting infrastructure (FileAccessor refactoring, vlog recovery updates) necessary to wire Fs through the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#187-featfs-memfs--in-memory-fs-implementation-for-test

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 80.87986% with 113 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/fs/mem_fs.rs 80.28% 84 Missing ⚠️
src/file_accessor.rs 60.86% 18 Missing ⚠️
src/file.rs 70.83% 7 Missing ⚠️
src/vlog/mod.rs 91.17% 3 Missing ⚠️
src/table/util.rs 88.88% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR completes the “pluggable Fs backend” story by introducing an in-memory filesystem (MemFs) and refactoring core I/O paths to route through Arc<dyn Fs> (including crash-safety critical rewrite_atomic), enabling fully in-memory trees for tests and ephemeral usage.

Changes:

  • Add MemFs/MemFile in-memory Fs implementation and a new integration test suite exercising Tree operations on MemFs.
  • De-generify Config<F: Fs> into Config with Arc<dyn Fs> and add Config::with_fs() to plug alternative backends.
  • Thread Fs through recovery/open paths (tables + vlog), unify file-open caching via FileAccessor::{get_or_open_table,get_or_open_blob_file}, and update metrics counting.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/mem_fs_tree.rs New integration tests for opening/using a Tree on MemFs.
src/vlog/mod.rs Route blob recovery directory scan + opens through Fs; improve handling of missing folder/dirs.
src/vlog/blob_file/multi_writer.rs Open blob files via Fs and pass Arc<dyn Fs> through writer finalization.
src/vlog/accessor.rs Use FileAccessor::get_or_open_blob_file instead of std::fs + manual cache insert.
src/tree/mod.rs Propagate level-routed Fs into table recovery and vlog recovery.
src/tree/ingest.rs Store/propagate level-routed Fs through ingestion path.
src/table/util.rs Switch block loading to get_or_open_table and restore cached/uncached metrics.
src/table/tests.rs Update table test constructors/matches for new Fs plumbing and FileAccessor shape.
src/table/multi_writer.rs Expose fs to crate for compaction finalization to reuse the correct backend.
src/table/mod.rs Open table files via Fs in recovery; store Fs in Table/Inner.
src/table/inner.rs Delete table files in Drop via Fs instead of std::fs.
src/fs/mod.rs Export MemFs and document required atomic-replace semantics for Fs::rename.
src/fs/mem_fs.rs New MemFs implementation + unit tests.
src/file.rs Rewrite rewrite_atomic to avoid tempfile and go through Fs for crash-safe CURRENT updates.
src/file_accessor.rs Extend FileAccessor to hold {table, fs} and open via Fs on cache miss.
src/config/mod.rs Convert Config to store Arc<dyn Fs> and add Config::with_fs().
src/compaction/flavour.rs Ensure compaction table recovery uses the writer’s Fs.
src/blob_tree/mod.rs Propagate level-routed Fs into blob tree table recovery paths.
src/blob_tree/ingest.rs Pass level-routed Fs into blob ingestion recovery path.
rust-toolchain.toml Bump toolchain channel to 1.94.1.
benches/index_block.rs Update bench table construction to pass StdFs.

Comment thread src/file.rs Outdated
Comment thread src/fs/mem_fs.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

1495-1495: ⚠️ Potential issue | 🟠 Major

MemFs reopen still cannot recover persisted state.

Line 1495 still enters version::recovery::recover(tree_path), and Line 1691 still reaches cleanup_orphaned_version(...); both paths use the real filesystem. This file also still chooses between recover() and create_new() via Path::try_exists(). So after a flush/version rewrite, reopening the same virtual path with MemFs still recreates a fresh tree or fails instead of recovering the existing state.

Also applies to: 1679-1691

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tree/mod.rs` at line 1495, The reopen logic currently calls
version::recovery::recover(tree_path), Path::try_exists(), and
cleanup_orphaned_version(...) which operate on the real filesystem; change these
to use the virtual filesystem/MemFs-aware APIs so MemFs can recover persisted
state. Specifically, replace direct calls to recover(...) and create_new(...)
that take a std Path with the VFS-aware variants (or add an overload) that
accept the TreePath or its Vfs handle, replace Path::try_exists() with the
Vfs/TreePath existence check, and ensure cleanup_orphaned_version is invoked
with Vfs-aware paths; update the code paths around the recover/create_new
decision (the block containing recover(tree_path) at line 1495 and the
create_new/recovery branch around 1679-1691) to use those VFS-aware functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust-toolchain.toml`:
- Line 2: The PR updates rust-toolchain.toml's channel value to "1.94.1" without
documenting the security rationale; update the PR and commit message to
explicitly state that the bump includes security fixes (CVE-2026-33055,
CVE-2026-33056 and regression fixes) or split this change into a dedicated
maintenance/security PR. Refer to the rust-toolchain.toml channel change when
editing the PR description and add a short entry in the changelog or release
notes referencing the security CVEs to make the rationale discoverable in git
history.

In `@src/file_accessor.rs`:
- Around line 42-56: get_or_open_table currently returns a bool that conflates
three distinct states (pinned/no-descriptor-table, descriptor-cache hit, and
newly opened file), causing Self::File to be misreported as a cache hit; change
the bool to a small enum (e.g., Pinned | Cached | Opened) and update
get_or_open_table to return that enum: for Self::File return Pinned, when
table.access_for_table(table_id) succeeds return Cached, and when you open via
fs.open(...) and call table.insert_for_table(...) return Opened; then update
callers (e.g., the code that treats the returned boolean as
table_file_opened_cached) to branch on the new enum values.

In `@src/fs/mem_fs.rs`:
- Around line 455-497: Add a unit test to verify MemFs::rename() atomically
replaces an existing destination file: create a test (e.g.,
rename_replaces_existing_destination) in the test module that uses MemFs::new(),
creates /dir, writes "old content" to /dir/dst.txt, writes "new content" to
/dir/src.txt, calls fs.rename(src, dst), asserts src no longer exists via
MemFs::exists, and reads dst to assert it contains "new content"; this confirms
the HashMap::insert overwrite behavior in rename() and safeguards rewrite_atomic
semantics.

---

Outside diff comments:
In `@src/tree/mod.rs`:
- Line 1495: The reopen logic currently calls
version::recovery::recover(tree_path), Path::try_exists(), and
cleanup_orphaned_version(...) which operate on the real filesystem; change these
to use the virtual filesystem/MemFs-aware APIs so MemFs can recover persisted
state. Specifically, replace direct calls to recover(...) and create_new(...)
that take a std Path with the VFS-aware variants (or add an overload) that
accept the TreePath or its Vfs handle, replace Path::try_exists() with the
Vfs/TreePath existence check, and ensure cleanup_orphaned_version is invoked
with Vfs-aware paths; update the code paths around the recover/create_new
decision (the block containing recover(tree_path) at line 1495 and the
create_new/recovery branch around 1679-1691) to use those VFS-aware functions.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a748c4a3-27e4-4b3b-95a4-e2486896bbd0

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1349e and 303361e.

📒 Files selected for processing (21)
  • benches/index_block.rs
  • rust-toolchain.toml
  • src/blob_tree/ingest.rs
  • src/blob_tree/mod.rs
  • src/compaction/flavour.rs
  • src/config/mod.rs
  • src/file.rs
  • src/file_accessor.rs
  • src/fs/mem_fs.rs
  • src/fs/mod.rs
  • src/table/inner.rs
  • src/table/mod.rs
  • src/table/multi_writer.rs
  • src/table/tests.rs
  • src/table/util.rs
  • src/tree/ingest.rs
  • src/tree/mod.rs
  • src/vlog/accessor.rs
  • src/vlog/blob_file/multi_writer.rs
  • src/vlog/mod.rs
  • tests/mem_fs_tree.rs

Comment thread rust-toolchain.toml
Comment thread src/file_accessor.rs Outdated
Comment thread src/fs/mem_fs.rs
…ct cache metrics

- rewrite_atomic retries with next seq on AlreadyExists (handles crash leftovers)
- MemFs::open rejects no-flag opens with InvalidInput (matches std::fs)
- FileAccessor: pinned FDs not counted as descriptor-cache hits
- Add rename-replace atomicity test
- Track Tree::open Fs migration in #209 with inline code comment
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 3, 2026

@coderabbitai Re: outside-diff src/tree/mod.rs:1495 (MemFs reopen) — This is a documented known limitation. Created #209 to track full Tree::open Fs migration. Added inline code comment at the bypass point.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz — Acknowledged, thanks for the follow-up. The inline comment plus #209 tracking the full Tree::open Fs migration is a clean way to handle it. I'll note this for future reviews so it isn't re-flagged.


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/208

Timestamp: 2026-04-03T17:42:38.195Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), around line 1495, `Tree::open` intentionally checks `CURRENT` via `std::fs` instead of the pluggable `Fs` trait. This is a documented known limitation: MemFs-backed trees cannot be fully re-opened (`Tree::open` reopen is unsupported for MemFs). The bypass has an inline code comment and is tracked for resolution in issue `#209`. Do not flag this `std::fs` usage as a missing Fs-trait wiring in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:377-388
Timestamp: 2026-04-03T11:37:54.884Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::rename()` intentionally only supports file renames (not directory renames). lsm-tree only renames files (table files, version pointers like CURRENT), so directory rename support is not needed. The method returns `NotFound` for directories, and this limitation is documented in the code. Do not flag the absence of directory rename support as a bug.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 166
File: src/tree/mod.rs:1628-1659
Timestamp: 2026-03-24T20:01:06.401Z
Learning: In `structured-world/coordinode-lsm-tree` `src/tree/mod.rs`, the `RouteMismatch` check in `recover_levels` is intentionally a best-effort heuristic: it uses `config.level_routes` level-coverage to distinguish a removed route (config error) from a deleted SST (data corruption), but does NOT persist route provenance per-table. Persisting route provenance would require a manifest format change and is deferred to a future PR. Do not re-flag this as a misclassification bug; the limitation is documented in the code comment above the `all_missing_uncovered` logic.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/vlog/mod.rs:37-43
Timestamp: 2026-04-03T15:28:03.829Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` intentionally returns `Ok((vec![], vec![]))` when `fs.read_dir(folder)` yields `ErrorKind::NotFound`. Standard (non-blob) trees never create a blobs folder, so `ids` is empty in that case and the downstream `blob_files.len() < ids.len()` guard (0 < 0 = false) passes correctly. Missing blob files for actual blob trees are handled by callers via orphan detection. This mirrors the pre-PR `folder.try_exists()` early-return and is not a correctness bug. Do not flag this early return as skipping the missing-data guard in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:80-85
Timestamp: 2026-04-03T16:37:25.358Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::lock_exclusive()` intentionally returns `Ok(())` as a no-op. `MemFs` is a test/ephemeral backend where cross-process exclusivity is not meaningful; all in-process access is already serialized through the shared `Arc<RwLock<State>>`. Do not flag the no-op `lock_exclusive()` implementation as a correctness issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:120-135
Timestamp: 2026-03-23T20:55:26.977Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/lsm-tree), `IoUringFile` in append mode tracks EOF via an explicit `AtomicU64` cursor rather than relying on `O_APPEND` kernel semantics. This is intentional: lsm-tree uses single-writer-per-file for SSTs, journals, and blob files, so atomic multi-writer appends are not required. Do not flag this as a bug or semantic violation in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:38:24.388Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), `parse_full` bounds hardening, `#[must_use]` annotations on parser methods, and remaining `value_type` unwrap paths in `parse_full` are pre-existing code outside the diff of PR `#182`. The broader hardening (including a fallible decoder constructor) is tracked in issue `#184`. Do not flag these as unaddressed gaps in reviews of PR `#182`; they are known, tracked follow-ups.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:317-326
Timestamp: 2026-03-23T23:29:57.067Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), `IoUringFile::write()` in append mode intentionally calls `self.file.metadata()?.len()` (fstat) on every write instead of maintaining an in-memory EOF cursor. This is a deliberate correctness trade-off: a separate in-memory append cursor could be invalidated by seek()-before-write() sequences on append-mode files. The ~100ns fstat cost is considered negligible for the journal/SST append patterns used in lsm-tree. Do not flag this as a performance issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 148
File: src/merge.rs:82-100
Timestamp: 2026-03-23T23:04:22.951Z
Learning: In structured-world/coordinode-lsm-tree `src/merge.rs`, the Merger intentionally drops the currently-buffered heap entry when advancing its source iterator returns an `Err`. This matches the original `pop_min()` + `fail_iter!` behavior and is considered pre-existing/out-of-scope for perf PRs. Do not flag it as a regression introduced by replace_min/replace_max refactors.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:751-1217
Timestamp: 2026-03-23T23:31:46.878Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), the maintainer (polaz) has explicitly accepted short/bare test function names (e.g., `rename`, `sync_directory`, `debug_impl`) inside the plain `mod tests` block. The rationale is that the full Rust test path `io_uring_fs::tests::<name>` provides sufficient context in CI output. Do not flag these as needing the `<what>_<condition>_<expected>` naming pattern.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:104-110
Timestamp: 2026-03-27T12:08:02.765Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `assert!(restart_interval > 0, ...)` in `Decoder::new` is an intentional placeholder: the proper fix — a fallible decoder constructor that propagates a corruption error instead of panicking — is a wider refactor tracked in issue `#184`. A `TODO(`#184`)` comment was added at the assert site in PR `#182`. Do not re-flag this assert as a missing fallible validation in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: CR
Repo: structured-world/coordinode-lsm-tree PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-23T21:07:24.715Z
Learning: Applies to **/*test*.rs : Use real on-disk files for storage tests via `tempfile::tempdir()` instead of mocks.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/file.rs:66-67
Timestamp: 2026-04-03T11:37:53.660Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `rewrite_atomic` uses a process-local `static TEMP_SEQ: AtomicU64` to generate unique temp filenames (`.tmp_{seq}`). This is sufficient because lsm-tree acquires an exclusive file lock (`lock_exclusive`) on each data directory, ensuring a single process owns a given directory at any time. Do not flag the absence of a PID or random suffix in the temp filename as a collision risk in future reviews.

Copy link
Copy Markdown

@sw-release-bot sw-release-bot Bot left a comment

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'lsm-tree db_bench'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.15.

Benchmark suite Current: 093765c Previous: 7d1349e Ratio
fillseq 886976.5448707001 ops/sec (normalized) 1153151.9210478074 ops/sec (normalized) 1.30
fillrandom 458644.19596829236 ops/sec (normalized) 653001.1792087426 ops/sec (normalized) 1.42
readrandom 232890.43593753094 ops/sec (normalized) 306750.82456667925 ops/sec (normalized) 1.32
overwrite 484863.1751445867 ops/sec (normalized) 667980.8595552227 ops/sec (normalized) 1.38
readwhilewriting 200053.23668976434 ops/sec (normalized) 257154.28586125583 ops/sec (normalized) 1.29

This comment was automatically generated by workflow using github-action-benchmark.

CC: @polaz

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/file_accessor.rs`:
- Around line 38-41: Update the doc comments and debug label to state that the
returned bool indicates a cache hit (true) or a cache miss (false), and not to
assume false means the descriptor was freshly opened; adjust the comment on the
method that "Returns a cached table FD or opens the file via [`Fs`] on cache
miss" to clarify "false = cache miss (may be opened now or provided by
Self::File), not necessarily freshly opened", and change the
`FileAccessor::Cached` debug label and any other comments around `Self::File`
and the other documented spots (the other docblocks at the indicated ranges) to
use "cache miss" wording instead of "freshly opened".
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa8729b2-f547-4404-8a09-0d09c7f16fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 303361e and 52f701a.

📒 Files selected for processing (4)
  • src/file.rs
  • src/file_accessor.rs
  • src/fs/mem_fs.rs
  • src/tree/mod.rs

Comment thread src/file_accessor.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Comment thread src/file_accessor.rs Outdated
Comment thread src/table/util.rs Outdated
Comment thread src/file.rs
Comment thread src/config/mod.rs
- FileAccessor::get_or_open returns Option<bool>: None for pinned FDs
  (no cache event), Some(true) for hit, Some(false) for miss
- Metrics in load_block only fire on descriptor-table cache events
- rewrite_atomic cleans up temp file on write/flush/sync failure
- Add Config::with_shared_fs(Arc<dyn Fs>) for sharing backends
@polaz polaz requested a review from Copilot April 3, 2026 18:13
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 3, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

@polaz — Sure! Kicking off a full re-review of PR #208 now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-03T17:42:38.195Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), around line 1495, `Tree::open` intentionally checks `CURRENT` via `std::fs` instead of the pluggable `Fs` trait. This is a documented known limitation: MemFs-backed trees cannot be fully re-opened (`Tree::open` reopen is unsupported for MemFs). The bypass has an inline code comment and is tracked for resolution in issue `#209`. Do not flag this `std::fs` usage as a missing Fs-trait wiring in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/file.rs:66-67
Timestamp: 2026-04-03T11:37:53.660Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `rewrite_atomic` uses a process-local `static TEMP_SEQ: AtomicU64` to generate unique temp filenames (`.tmp_{seq}`). This is sufficient because lsm-tree acquires an exclusive file lock (`lock_exclusive`) on each data directory, ensuring a single process owns a given directory at any time. Do not flag the absence of a PID or random suffix in the temp filename as a collision risk in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:120-135
Timestamp: 2026-03-23T20:55:26.977Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/lsm-tree), `IoUringFile` in append mode tracks EOF via an explicit `AtomicU64` cursor rather than relying on `O_APPEND` kernel semantics. This is intentional: lsm-tree uses single-writer-per-file for SSTs, journals, and blob files, so atomic multi-writer appends are not required. Do not flag this as a bug or semantic violation in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:80-85
Timestamp: 2026-04-03T16:37:25.358Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::lock_exclusive()` intentionally returns `Ok(())` as a no-op. `MemFs` is a test/ephemeral backend where cross-process exclusivity is not meaningful; all in-process access is already serialized through the shared `Arc<RwLock<State>>`. Do not flag the no-op `lock_exclusive()` implementation as a correctness issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:317-326
Timestamp: 2026-03-23T23:29:57.067Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), `IoUringFile::write()` in append mode intentionally calls `self.file.metadata()?.len()` (fstat) on every write instead of maintaining an in-memory EOF cursor. This is a deliberate correctness trade-off: a separate in-memory append cursor could be invalidated by seek()-before-write() sequences on append-mode files. The ~100ns fstat cost is considered negligible for the journal/SST append patterns used in lsm-tree. Do not flag this as a performance issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/vlog/mod.rs:37-43
Timestamp: 2026-04-03T15:28:03.829Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` intentionally returns `Ok((vec![], vec![]))` when `fs.read_dir(folder)` yields `ErrorKind::NotFound`. Standard (non-blob) trees never create a blobs folder, so `ids` is empty in that case and the downstream `blob_files.len() < ids.len()` guard (0 < 0 = false) passes correctly. Missing blob files for actual blob trees are handled by callers via orphan detection. This mirrors the pre-PR `folder.try_exists()` early-return and is not a correctness bug. Do not flag this early return as skipping the missing-data guard in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:751-1217
Timestamp: 2026-03-23T23:31:46.878Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), the maintainer (polaz) has explicitly accepted short/bare test function names (e.g., `rename`, `sync_directory`, `debug_impl`) inside the plain `mod tests` block. The rationale is that the full Rust test path `io_uring_fs::tests::<name>` provides sufficient context in CI output. Do not flag these as needing the `<what>_<condition>_<expected>` naming pattern.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 166
File: src/tree/mod.rs:1628-1659
Timestamp: 2026-03-24T20:01:06.401Z
Learning: In `structured-world/coordinode-lsm-tree` `src/tree/mod.rs`, the `RouteMismatch` check in `recover_levels` is intentionally a best-effort heuristic: it uses `config.level_routes` level-coverage to distinguish a removed route (config error) from a deleted SST (data corruption), but does NOT persist route provenance per-table. Persisting route provenance would require a manifest format change and is deferred to a future PR. Do not re-flag this as a misclassification bug; the limitation is documented in the code comment above the `all_missing_uncovered` logic.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/two_level.rs:152-157
Timestamp: 2026-04-02T08:18:35.857Z
Learning: In `src/table/block_index/two_level.rs` (structured-world/coordinode-lsm-tree), the empty-child-partition early-return behavior (where `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` for a trimmed boundary partition causes `next()`/`next_back()` to stop the two-level scan prematurely instead of continuing to walk the TLI) is pre-existing behavior, not introduced by PR `#191`. This is tracked in issue `#194`. Do not flag the absence of a TLI-walking loop for empty child partitions in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:377-388
Timestamp: 2026-04-03T11:37:54.884Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::rename()` intentionally only supports file renames (not directory renames). lsm-tree only renames files (table files, version pointers like CURRENT), so directory rename support is not needed. The method returns `NotFound` for directories, and this limitation is documented in the code. Do not flag the absence of directory rename support as a bug.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-31T14:57:51.527Z
Learning: In `src/table/meta.rs` (structured-world/coordinode-lsm-tree), meta fields (e.g., `table_version`, `filter_hash_type`, `checksum_type`, compression keys) can still panic on corrupt input via `read_u8!`/`read_u64!` macro paths. This is out of scope for PR `#191` (which focuses on trailer field validation). The broader meta-field hardening is tracked in issue `#193`. Do not re-flag this as an unaddressed gap in reviews of PR `#191` or earlier code.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Comment thread src/vlog/mod.rs
Comment thread src/vlog/mod.rs
Comment thread src/vlog/blob_file/multi_writer.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@tests/mem_fs_tree.rs`:
- Around line 131-148: The test memfs_shared_across_trees currently calls
Config::with_fs with a MemFs instance (using MemFs::clone()), which only
exercises the non-shared path; change the test to use Config::with_shared_fs so
the shared Arc<dyn Fs> path is exercised. Concretely, construct or wrap the
MemFs into the shared form expected by Config::with_shared_fs (e.g., an Arc<dyn
Fs> or the type accepted by with_shared_fs) and call Config::with_shared_fs(...)
for both tree1 and tree2 instead of Config::with_fs(...), keeping the same
assertions that verify namespace isolation.
- Around line 73-86: The iterator code currently calls
guard.into_inner().unwrap() and String::from_utf8(...).unwrap(), introducing
panics; instead keep the raw byte vectors and avoid UTF-8 conversion—map each
guard to let (k, v) = guard.into_inner()? and collect into Vec<(Vec<u8>,
Vec<u8>)> (or use k.to_vec()/v.to_vec() without unwrap) and then assert_eq!
against byte vectors like b"a".to_vec()/b"1".to_vec(); update the variables used
(tree.iter, SeqNo::MAX, guard.into_inner(), items, and the assert_eq! target)
accordingly so no unwraps or UTF-8 parsing remain.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 011bb592-42b4-4c84-972e-a55f42b1ae96

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1349e and 3520e08.

📒 Files selected for processing (21)
  • benches/index_block.rs
  • rust-toolchain.toml
  • src/blob_tree/ingest.rs
  • src/blob_tree/mod.rs
  • src/compaction/flavour.rs
  • src/config/mod.rs
  • src/file.rs
  • src/file_accessor.rs
  • src/fs/mem_fs.rs
  • src/fs/mod.rs
  • src/table/inner.rs
  • src/table/mod.rs
  • src/table/multi_writer.rs
  • src/table/tests.rs
  • src/table/util.rs
  • src/tree/ingest.rs
  • src/tree/mod.rs
  • src/vlog/accessor.rs
  • src/vlog/blob_file/multi_writer.rs
  • src/vlog/mod.rs
  • tests/mem_fs_tree.rs

Comment thread tests/mem_fs_tree.rs Outdated
Comment thread tests/mem_fs_tree.rs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread src/fs/mem_fs.rs Outdated
Comment thread src/fs/mem_fs.rs Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz — Thanks for the follow-up! Adding explicit doc comments to document the intentional no-op for the pinned File variant is the right call — it makes the remove_for_table/remove_for_blob_file semantics self-evident at the call site without requiring callers to dig into the implementation to understand why a removal on a pinned descriptor is silently ignored.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 202
File: src/table/tests.rs:2113-2118
Timestamp: 2026-04-02T23:53:51.122Z
Learning: In `src/table/block_index/iter.rs` and `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound` contains an unconditional restore (the `else { true }` branch at lines ~787-788 of decoder.rs) that always re-pushes a covering entry onto `hi_scanner.stack` when the stack would otherwise empty. This means `seek_upper_bound_cursor` always returns `true` for well-formed block data, regardless of `restart_interval`. Consequently, the `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` (empty-child path) cannot be forced from test configuration (e.g., `use_index_block_restart_interval(4)`) on non-corrupt data — it is only reachable via `fill_stack`/`advance_upper_restart_interval` decode failure. Regression tests for the two-level index empty-child skip behavior (PR `#202`, issue `#194`) validate overall iteration correctness defensively, not the exact `Ok(None)` corruption path. Do not suggest forcing this path via `restart_interval` changes in reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-27T07:10:44.958Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the restart-key span validation is intentionally split: strict bounds hardening (buffer/offset checks) was implemented in PR `#182`, while full semantic restart-key span validation (which requires passing the restart-key span through decoder state) is tracked separately in issue `#183`. Do not flag the absence of semantic-span validation in decoder.rs as an unaddressed gap in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:644-703
Timestamp: 2026-03-28T19:57:49.231Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound`'s `should_restore` branch intentionally re-pushes `candidate_offset` (the first popped entry with `cmp == Greater`) back onto `hi_scanner.stack` when the remaining stack top compares `Less` to the needle or the stack is empty. This preserves the covering interval boundary required for correct reverse-iteration (`seek_upper`) semantics across restart intervals greater than 1. Dropping this restore regresses `seek_upper` by losing the upper extent of the current restart interval. Do not flag this re-push as returning an out-of-range entry in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:236-243
Timestamp: 2026-03-30T17:47:54.258Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `get_key_at` calls `Item::parse_full` on every binary-search probe inside `partition_point` / `partition_point_2` as a result of the bounds-hardening in PR `#182` (which replaced the previous unsafe `get_unchecked` + `parse_restart_key` path). Threading `entries_end` into `parse_restart_key()` to restore a lighter bounded fast path is tracked in issue `#190`. Do not flag the full-item decode in `get_key_at` as an unaddressed gap in reviews of PR `#182` or later code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-31T14:37:26.098Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `unwrap!` calls on `read_u8`/`read_u32` inside `Decoder::try_new()` (lines ~139–151) will panic on a truncated trailer slice. This is guarded at a higher level: `Block::from_reader` and checksum validation reject undersized blocks before they reach the decoder, and `Trailer::trailer_offset()` would underflow on undersized blocks — making this a pre-existing pipeline invariant, not introduced by PR `#191`. Making `Trailer::as_slice()` and its downstream reads fallible to handle truncated/undersized blocks gracefully is tracked in issue `#192`. Do not re-flag the `unwrap!` reads in `Decoder::try_new()` as an unaddressed gap in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/two_level.rs:152-157
Timestamp: 2026-04-02T08:18:35.857Z
Learning: In `src/table/block_index/two_level.rs` (structured-world/coordinode-lsm-tree), the empty-child-partition early-return behavior (where `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` for a trimmed boundary partition causes `next()`/`next_back()` to stop the two-level scan prematurely instead of continuing to walk the TLI) is pre-existing behavior, not introduced by PR `#191`. This is tracked in issue `#194`. Do not flag the absence of a TLI-walking loop for empty child partitions in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-28T13:41:55.184Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), the `parse_truncated` function now validates `shared_prefix_len` against both the entry-start boundary AND the restart-key span (`base_key_end`). This was fixed in PR `#182` commit `f64f90e` (originally tracked as follow-up issue `#186`). The function signature was updated to accept a `base_key_end: usize` parameter, and the prefix-end check was changed from `prefix_end > offset` to `prefix_end > base_key_end`. Do not flag the absence of this restart-key span check in reviews of PR `#182` or later code — it is fully implemented.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:366-372
Timestamp: 2026-03-30T13:21:41.943Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `poison_back_cursor()` now delegates to `clamp_upper_to_lo()` instead of clearing `hi_scanner.base_key_offset` to `None`. This preserves a hard forward upper bound (`base_key_offset = Some(0)`) so that a caller switching from `next_back()` to `next()` after corruption cannot resume past the poisoned interval. Regression test `poison_back_cursor_also_stops_forward_next` covers this path. Do not flag `poison_back_cursor` as fail-open in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:19:33.033Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `advance_upper_restart_interval()` fail-closes by calling `clamp_upper_to_lo()` when `fill_stack()` poisons the next interval. This preserves the hard upper bound on the hi_scanner so that forward iteration cannot escape past a corrupted interval boundary. This was implemented in PR `#182`. Do not flag the absence of this fail-close as a gap in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/index_block/iter.rs:80-97
Timestamp: 2026-03-30T22:53:39.583Z
Learning: In `src/table/index_block/iter.rs` (structured-world/coordinode-lsm-tree), `seek_upper_impl` is called with `reset_front=true` by the public `seek_upper` (a full back-cursor re-seek that does not depend on the low cursor) and with `reset_front=false` by `seek_upper_bound_cursor` (which preserves `front_peeked` seeded by a prior `seek_lower_bound_cursor` under the `lo ≤ hi` invariant). Do not flag the absence of a low-cursor rewind in `seek_upper_impl` when `reset_front=true` — the low cursor position is irrelevant for a full back-cursor re-seek, and the bounded path correctly preserves it. Do not re-raise this concern in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/iter.rs:80-83
Timestamp: 2026-04-02T12:06:47.137Z
Learning: In `src/table/block_index/iter.rs` (structured-world/coordinode-lsm-tree), the bound-cursor helpers `seek_upper_bound_cursor` and `seek_lower_bound_cursor` return `bool` (not `Result`), so `from_block_with_bounds` treats a failed/clamped upper-bound cursor as `Ok(None)` (empty range) rather than propagating an error. This is pre-existing API design, out of scope for PR `#191`, and tracked in issue `#196`. Do not re-flag the absence of a fallible Result from these bound-cursor helpers in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-01T21:25:58.275Z
Learning: In `src/table/scanner.rs` (structured-world/coordinode-lsm-tree), after commit d6dde989 in PR `#191`, the `fetch_next_block` error path sets `self.read_count = self.block_count` before returning the error, exhausting the scanner on mid-scan block-fetch failures — consistent with the `try_new` error path (commit d66197d). Both error paths now poison the scanner so callers cannot silently skip corrupt blocks. Do not flag the absence of this scanner-poison behavior on `fetch_next_block` errors in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:104-110
Timestamp: 2026-03-27T12:08:02.765Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `assert!(restart_interval > 0, ...)` in `Decoder::new` is an intentional placeholder: the proper fix — a fallible decoder constructor that propagates a corruption error instead of panicking — is a wider refactor tracked in issue `#184`. A `TODO(`#184`)` comment was added at the assert site in PR `#182`. Do not re-flag this assert as a missing fallible validation in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:19:04.825Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::sync_directory` was hardened in PR `#208` (commit 76f3b676) to distinguish between a path that exists as a file (returns an error) and a path that does not exist (returns `NotFound`). Do not flag the absence of this distinction in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `fsync_directory` no longer has a platform-conditional Windows no-op branch (removed in PR `#208`). It now unconditionally delegates to `Fs::sync_directory`. The Windows no-op behavior is encapsulated inside `StdFs::sync_directory`. Do not flag the absence of a platform-conditional Windows branch in `fsync_directory` in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/vlog/mod.rs:37-43
Timestamp: 2026-04-03T15:28:06.759Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` intentionally returns `Ok((vec![], vec![]))` when `fs.read_dir(folder)` yields `ErrorKind::NotFound`. Standard (non-blob) trees never create a blobs folder, so `ids` is empty in that case and the downstream `blob_files.len() < ids.len()` guard (0 < 0 = false) passes correctly. Missing blob files for actual blob trees are handled by callers via orphan detection. This mirrors the pre-PR `folder.try_exists()` early-return and is not a correctness bug. Do not flag this early return as skipping the missing-data guard in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/config/mod.rs` (structured-world/coordinode-lsm-tree), `all_tables_folders` deduplicates by path only. This is correct: the same filesystem path always refers to the same on-disk directory, so path identity is sufficient for dedup. Routing the same path through different Fs backends is a configuration error that is already prevented upstream by `level_routes` validation (overlapping key ranges are rejected). Do not flag the absence of Fs-backend-aware deduplication in `all_tables_folders` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:120-135
Timestamp: 2026-03-23T20:55:26.977Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/lsm-tree), `IoUringFile` in append mode tracks EOF via an explicit `AtomicU64` cursor rather than relying on `O_APPEND` kernel semantics. This is intentional: lsm-tree uses single-writer-per-file for SSTs, journals, and blob files, so atomic multi-writer appends are not required. Do not flag this as a bug or semantic violation in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/fs/mem_fs.rs:285-300
Timestamp: 2026-04-04T11:18:56.220Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::open()` and `MemFs::rename()` intentionally only check the immediate parent directory, not the full ancestor chain. lsm-tree always calls `create_dir_all` before creating files, so intermediate file-vs-dir conflicts can only arise from external filesystem corruption, which MemFs is not responsible for defending against. A full ancestor walk would add O(depth) cost to every `open()` call. Do not flag the absence of full ancestor-chain validation in MemFs::open() or MemFs::rename() in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:317-326
Timestamp: 2026-03-23T23:29:57.067Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), `IoUringFile::write()` in append mode intentionally calls `self.file.metadata()?.len()` (fstat) on every write instead of maintaining an in-memory EOF cursor. This is a deliberate correctness trade-off: a separate in-memory append cursor could be invalidated by seek()-before-write() sequences on append-mode files. The ~100ns fstat cost is considered negligible for the journal/SST append patterns used in lsm-tree. Do not flag this as a performance issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/file.rs:64-72
Timestamp: 2026-04-03T21:19:43.478Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `rewrite_atomic` is always called with absolute paths (from `Tree::open` and `persist_version`). The `path.parent().expect("should have a parent")` call is intentional and documented; the empty-parent defensive normalization to `"."` is unnecessary. Do not flag the absence of empty-parent normalization in `rewrite_atomic` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-03T17:42:38.208Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), around line 1495, `Tree::open` intentionally checks `CURRENT` via `std::fs` instead of the pluggable `Fs` trait. This is a documented known limitation: MemFs-backed trees cannot be fully re-opened (`Tree::open` reopen is unsupported for MemFs). The bypass has an inline code comment and is tracked for resolution in issue `#209`. Do not flag this `std::fs` usage as a missing Fs-trait wiring in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:08.308Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), empty-path validation is centralized in an `ensure_non_empty_path()` helper that is called at the top of `MemFs::open()`, `MemFs::create_dir_all()`, and `MemFs::rename()`. It returns `InvalidInput` for empty paths before any state mutation. This was added in PR `#208`. Do not flag the absence of inline empty-path checks in those methods in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::write(&[])` returns `Ok(0)` immediately after the writable-access check (before acquiring the data lock or computing the write position). This was fixed in PR `#208`. Do not flag the absence of this early-return optimization in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), parent-directory existence validation is centralized in an `ensure_parent_dir()` helper shared by `MemFs::open()` and `MemFs::rename()`. This was extracted in PR `#208`, eliminating duplicated logic and allowing the `#[expect(clippy::too_many_lines)]` attribute to be removed from `open()`. Do not flag duplicated parent-dir checks between `open()` and `rename()` as a gap in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:80-85
Timestamp: 2026-04-03T16:37:26.531Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::lock_exclusive()` intentionally returns `Ok(())` as a no-op. `MemFs` is a test/ephemeral backend where cross-process exclusivity is not meaningful; all in-process access is already serialized through the shared `Arc<RwLock<State>>`. Do not flag the no-op `lock_exclusive()` implementation as a correctness issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/fs/mem_fs.rs:136-140
Timestamp: 2026-04-04T07:48:08.918Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `Vec::resize` calls in `MemFile::write()` and `MemFile::set_len()` intentionally do not use `try_reserve` before resizing. Using `try_reserve` before `resize` is a crate-wide architectural decision: lsm-tree targets 64-bit platforms where OOM is considered fatal, and adding fallible allocation handling is out of scope for MemFs specifically or any individual PR. Do not flag the absence of `try_reserve` guards on `Vec::resize` calls as a correctness or robustness issue in future reviews of this codebase.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:377-388
Timestamp: 2026-04-03T11:37:54.884Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::rename()` intentionally only supports file renames (not directory renames). lsm-tree only renames files (table files, version pointers like CURRENT), so directory rename support is not needed. The method returns `NotFound` for directories, and this limitation is documented in the code. Do not flag the absence of directory rename support as a bug.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread src/vlog/mod.rs Outdated
Comment thread src/vlog/mod.rs Outdated
… blob files

recover_blob_files now returns Unrecoverable when the blobs folder
does not exist but ids is non-empty (manifest references blob files).
Only treat NotFound as empty when ids is empty (standard non-blob trees).
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai resume and full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

@polaz — Sure! Resuming automatic reviews and kicking off a full re-review of PR #208 now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 202
File: src/table/tests.rs:2113-2118
Timestamp: 2026-04-02T23:53:51.122Z
Learning: In `src/table/block_index/iter.rs` and `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound` contains an unconditional restore (the `else { true }` branch at lines ~787-788 of decoder.rs) that always re-pushes a covering entry onto `hi_scanner.stack` when the stack would otherwise empty. This means `seek_upper_bound_cursor` always returns `true` for well-formed block data, regardless of `restart_interval`. Consequently, the `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` (empty-child path) cannot be forced from test configuration (e.g., `use_index_block_restart_interval(4)`) on non-corrupt data — it is only reachable via `fill_stack`/`advance_upper_restart_interval` decode failure. Regression tests for the two-level index empty-child skip behavior (PR `#202`, issue `#194`) validate overall iteration correctness defensively, not the exact `Ok(None)` corruption path. Do not suggest forcing this path via `restart_interval` changes in reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-27T07:10:44.958Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the restart-key span validation is intentionally split: strict bounds hardening (buffer/offset checks) was implemented in PR `#182`, while full semantic restart-key span validation (which requires passing the restart-key span through decoder state) is tracked separately in issue `#183`. Do not flag the absence of semantic-span validation in decoder.rs as an unaddressed gap in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:644-703
Timestamp: 2026-03-28T19:57:49.231Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound`'s `should_restore` branch intentionally re-pushes `candidate_offset` (the first popped entry with `cmp == Greater`) back onto `hi_scanner.stack` when the remaining stack top compares `Less` to the needle or the stack is empty. This preserves the covering interval boundary required for correct reverse-iteration (`seek_upper`) semantics across restart intervals greater than 1. Dropping this restore regresses `seek_upper` by losing the upper extent of the current restart interval. Do not flag this re-push as returning an out-of-range entry in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:236-243
Timestamp: 2026-03-30T17:47:54.258Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `get_key_at` calls `Item::parse_full` on every binary-search probe inside `partition_point` / `partition_point_2` as a result of the bounds-hardening in PR `#182` (which replaced the previous unsafe `get_unchecked` + `parse_restart_key` path). Threading `entries_end` into `parse_restart_key()` to restore a lighter bounded fast path is tracked in issue `#190`. Do not flag the full-item decode in `get_key_at` as an unaddressed gap in reviews of PR `#182` or later code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-31T14:37:26.098Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `unwrap!` calls on `read_u8`/`read_u32` inside `Decoder::try_new()` (lines ~139–151) will panic on a truncated trailer slice. This is guarded at a higher level: `Block::from_reader` and checksum validation reject undersized blocks before they reach the decoder, and `Trailer::trailer_offset()` would underflow on undersized blocks — making this a pre-existing pipeline invariant, not introduced by PR `#191`. Making `Trailer::as_slice()` and its downstream reads fallible to handle truncated/undersized blocks gracefully is tracked in issue `#192`. Do not re-flag the `unwrap!` reads in `Decoder::try_new()` as an unaddressed gap in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-28T13:41:55.184Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), the `parse_truncated` function now validates `shared_prefix_len` against both the entry-start boundary AND the restart-key span (`base_key_end`). This was fixed in PR `#182` commit `f64f90e` (originally tracked as follow-up issue `#186`). The function signature was updated to accept a `base_key_end: usize` parameter, and the prefix-end check was changed from `prefix_end > offset` to `prefix_end > base_key_end`. Do not flag the absence of this restart-key span check in reviews of PR `#182` or later code — it is fully implemented.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/two_level.rs:152-157
Timestamp: 2026-04-02T08:18:35.857Z
Learning: In `src/table/block_index/two_level.rs` (structured-world/coordinode-lsm-tree), the empty-child-partition early-return behavior (where `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` for a trimmed boundary partition causes `next()`/`next_back()` to stop the two-level scan prematurely instead of continuing to walk the TLI) is pre-existing behavior, not introduced by PR `#191`. This is tracked in issue `#194`. Do not flag the absence of a TLI-walking loop for empty child partitions in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:366-372
Timestamp: 2026-03-30T13:21:41.943Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `poison_back_cursor()` now delegates to `clamp_upper_to_lo()` instead of clearing `hi_scanner.base_key_offset` to `None`. This preserves a hard forward upper bound (`base_key_offset = Some(0)`) so that a caller switching from `next_back()` to `next()` after corruption cannot resume past the poisoned interval. Regression test `poison_back_cursor_also_stops_forward_next` covers this path. Do not flag `poison_back_cursor` as fail-open in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:19:33.033Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `advance_upper_restart_interval()` fail-closes by calling `clamp_upper_to_lo()` when `fill_stack()` poisons the next interval. This preserves the hard upper bound on the hi_scanner so that forward iteration cannot escape past a corrupted interval boundary. This was implemented in PR `#182`. Do not flag the absence of this fail-close as a gap in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/index_block/iter.rs:80-97
Timestamp: 2026-03-30T22:53:39.583Z
Learning: In `src/table/index_block/iter.rs` (structured-world/coordinode-lsm-tree), `seek_upper_impl` is called with `reset_front=true` by the public `seek_upper` (a full back-cursor re-seek that does not depend on the low cursor) and with `reset_front=false` by `seek_upper_bound_cursor` (which preserves `front_peeked` seeded by a prior `seek_lower_bound_cursor` under the `lo ≤ hi` invariant). Do not flag the absence of a low-cursor rewind in `seek_upper_impl` when `reset_front=true` — the low cursor position is irrelevant for a full back-cursor re-seek, and the bounded path correctly preserves it. Do not re-raise this concern in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/iter.rs:80-83
Timestamp: 2026-04-02T12:06:47.137Z
Learning: In `src/table/block_index/iter.rs` (structured-world/coordinode-lsm-tree), the bound-cursor helpers `seek_upper_bound_cursor` and `seek_lower_bound_cursor` return `bool` (not `Result`), so `from_block_with_bounds` treats a failed/clamped upper-bound cursor as `Ok(None)` (empty range) rather than propagating an error. This is pre-existing API design, out of scope for PR `#191`, and tracked in issue `#196`. Do not re-flag the absence of a fallible Result from these bound-cursor helpers in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-01T21:25:58.275Z
Learning: In `src/table/scanner.rs` (structured-world/coordinode-lsm-tree), after commit d6dde989 in PR `#191`, the `fetch_next_block` error path sets `self.read_count = self.block_count` before returning the error, exhausting the scanner on mid-scan block-fetch failures — consistent with the `try_new` error path (commit d66197d). Both error paths now poison the scanner so callers cannot silently skip corrupt blocks. Do not flag the absence of this scanner-poison behavior on `fetch_next_block` errors in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:19:04.825Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::sync_directory` was hardened in PR `#208` (commit 76f3b676) to distinguish between a path that exists as a file (returns an error) and a path that does not exist (returns `NotFound`). Do not flag the absence of this distinction in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `fsync_directory` no longer has a platform-conditional Windows no-op branch (removed in PR `#208`). It now unconditionally delegates to `Fs::sync_directory`. The Windows no-op behavior is encapsulated inside `StdFs::sync_directory`. Do not flag the absence of a platform-conditional Windows branch in `fsync_directory` in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/vlog/mod.rs:37-43
Timestamp: 2026-04-03T15:28:06.759Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` intentionally returns `Ok((vec![], vec![]))` when `fs.read_dir(folder)` yields `ErrorKind::NotFound`. Standard (non-blob) trees never create a blobs folder, so `ids` is empty in that case and the downstream `blob_files.len() < ids.len()` guard (0 < 0 = false) passes correctly. Missing blob files for actual blob trees are handled by callers via orphan detection. This mirrors the pre-PR `folder.try_exists()` early-return and is not a correctness bug. Do not flag this early return as skipping the missing-data guard in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/config/mod.rs` (structured-world/coordinode-lsm-tree), `all_tables_folders` deduplicates by path only. This is correct: the same filesystem path always refers to the same on-disk directory, so path identity is sufficient for dedup. Routing the same path through different Fs backends is a configuration error that is already prevented upstream by `level_routes` validation (overlapping key ranges are rejected). Do not flag the absence of Fs-backend-aware deduplication in `all_tables_folders` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:120-135
Timestamp: 2026-03-23T20:55:26.977Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/lsm-tree), `IoUringFile` in append mode tracks EOF via an explicit `AtomicU64` cursor rather than relying on `O_APPEND` kernel semantics. This is intentional: lsm-tree uses single-writer-per-file for SSTs, journals, and blob files, so atomic multi-writer appends are not required. Do not flag this as a bug or semantic violation in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/fs/mem_fs.rs:285-300
Timestamp: 2026-04-04T11:18:56.220Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::open()` and `MemFs::rename()` intentionally only check the immediate parent directory, not the full ancestor chain. lsm-tree always calls `create_dir_all` before creating files, so intermediate file-vs-dir conflicts can only arise from external filesystem corruption, which MemFs is not responsible for defending against. A full ancestor walk would add O(depth) cost to every `open()` call. Do not flag the absence of full ancestor-chain validation in MemFs::open() or MemFs::rename() in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:317-326
Timestamp: 2026-03-23T23:29:57.067Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), `IoUringFile::write()` in append mode intentionally calls `self.file.metadata()?.len()` (fstat) on every write instead of maintaining an in-memory EOF cursor. This is a deliberate correctness trade-off: a separate in-memory append cursor could be invalidated by seek()-before-write() sequences on append-mode files. The ~100ns fstat cost is considered negligible for the journal/SST append patterns used in lsm-tree. Do not flag this as a performance issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/file.rs:64-72
Timestamp: 2026-04-03T21:19:43.478Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `rewrite_atomic` is always called with absolute paths (from `Tree::open` and `persist_version`). The `path.parent().expect("should have a parent")` call is intentional and documented; the empty-parent defensive normalization to `"."` is unnecessary. Do not flag the absence of empty-parent normalization in `rewrite_atomic` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-03T17:42:38.208Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), around line 1495, `Tree::open` intentionally checks `CURRENT` via `std::fs` instead of the pluggable `Fs` trait. This is a documented known limitation: MemFs-backed trees cannot be fully re-opened (`Tree::open` reopen is unsupported for MemFs). The bypass has an inline code comment and is tracked for resolution in issue `#209`. Do not flag this `std::fs` usage as a missing Fs-trait wiring in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:08.308Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), empty-path validation is centralized in an `ensure_non_empty_path()` helper that is called at the top of `MemFs::open()`, `MemFs::create_dir_all()`, and `MemFs::rename()`. It returns `InvalidInput` for empty paths before any state mutation. This was added in PR `#208`. Do not flag the absence of inline empty-path checks in those methods in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::write(&[])` returns `Ok(0)` immediately after the writable-access check (before acquiring the data lock or computing the write position). This was fixed in PR `#208`. Do not flag the absence of this early-return optimization in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), parent-directory existence validation is centralized in an `ensure_parent_dir()` helper shared by `MemFs::open()` and `MemFs::rename()`. This was extracted in PR `#208`, eliminating duplicated logic and allowing the `#[expect(clippy::too_many_lines)]` attribute to be removed from `open()`. Do not flag duplicated parent-dir checks between `open()` and `rename()` as a gap in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:80-85
Timestamp: 2026-04-03T16:37:26.531Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::lock_exclusive()` intentionally returns `Ok(())` as a no-op. `MemFs` is a test/ephemeral backend where cross-process exclusivity is not meaningful; all in-process access is already serialized through the shared `Arc<RwLock<State>>`. Do not flag the no-op `lock_exclusive()` implementation as a correctness issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:104-110
Timestamp: 2026-03-27T12:08:02.765Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `assert!(restart_interval > 0, ...)` in `Decoder::new` is an intentional placeholder: the proper fix — a fallible decoder constructor that propagates a corruption error instead of panicking — is a wider refactor tracked in issue `#184`. A `TODO(`#184`)` comment was added at the assert site in PR `#182`. Do not re-flag this assert as a missing fallible validation in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:38:24.388Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), `parse_full` bounds hardening, `#[must_use]` annotations on parser methods, and remaining `value_type` unwrap paths in `parse_full` are pre-existing code outside the diff of PR `#182`. The broader hardening (including a fallible decoder constructor) is tracked in issue `#184`. Do not flag these as unaddressed gaps in reviews of PR `#182`; they are known, tracked follow-ups.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:377-388
Timestamp: 2026-04-03T11:37:54.884Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::rename()` intentionally only supports file renames (not directory renames). lsm-tree only renames files (table files, version pointers like CURRENT), so directory rename support is not needed. The method returns `NotFound` for directories, and this limitation is documented in the code. Do not flag the absence of directory rename support as a bug.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/fs/mem_fs.rs:136-140
Timestamp: 2026-04-04T07:48:08.918Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `Vec::resize` calls in `MemFile::write()` and `MemFile::set_len()` intentionally do not use `try_reserve` before resizing. Using `try_reserve` before `resize` is a crate-wide architectural decision: lsm-tree targets 64-bit platforms where OOM is considered fatal, and adding fallible allocation handling is out of scope for MemFs specifically or any individual PR. Do not flag the absence of `try_reserve` guards on `Vec::resize` calls as a correctness or robustness issue in future reviews of this codebase.
✅ Actions performed

Reviews resumed.


Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread src/table/inner.rs
Comment thread src/table/inner.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/vlog/mod.rs`:
- Around line 114-124: The code currently calls dt.insert_for_blob_file
immediately (in the block that constructs FileAccessor::DescriptorTable), which
publishes file descriptors before recovery completes; instead, remove the
immediate dt.insert_for_blob_file call and defer inserting into the descriptor
table until the recovery/commit path successfully finishes (i.e., after the
open/parse and missing-file guard succeed). Locate the construction around
descriptor_table / dt and FileAccessor::DescriptorTable and change it to create
the FileAccessor without mutating dt, then perform
dt.insert_for_blob_file(global_id, Arc::from(file)) only from the successful
recovery commit/finish code path (or via an explicit commit callback) so failed
opens do not leak handles.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 845e4024-68d9-4cf3-bce8-e6046637dd6e

📥 Commits

Reviewing files that changed from the base of the PR and between c20521e and 1cc66b4.

📒 Files selected for processing (1)
  • src/vlog/mod.rs

Comment thread src/vlog/mod.rs Outdated
- Inner::Drop: evict descriptor-table entry before fs.remove_file so
  Windows doesn't fail on open-file removal
- recover_blob_files: defer insert_for_blob_file until all blobs parse
  successfully, preventing partial FD leaks on recovery failure
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai Re: #54 (deferred blob cache inserts) — Fixed. insert_for_blob_file is now deferred into pending_cache_inserts and only committed after the missing-file guard passes. Partial recovery no longer leaks FDs.

Also fixed Copilot #52/#53: Inner::Drop now evicts the descriptor-table entry before calling fs.remove_file, so Windows doesn't fail on open-file removal.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz — Thanks for both updates!

#54 (deferred blob cache inserts): Deferring insert_for_blob_file into pending_cache_inserts and committing only after the missing-file guard passes is the correct fix — it ensures that a partial recovery (where some blob files are absent) doesn't leave dangling FDs in the descriptor-table cache. The old eager-insert path could leave the cache in an inconsistent state on error return.

#52/#53 (Inner::Drop evict-before-remove): Evicting the descriptor-table entry before fs.remove_file is the right ordering on Windows, where a file cannot be deleted while it has open handles. Doing the eviction first closes the cached FD, clearing the way for the filesystem-level removal to succeed.


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/208

Timestamp: 2026-04-04T20:34:35.847Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` defers all `insert_for_blob_file` calls into a `pending_cache_inserts` buffer and commits them only after the missing-file guard (`blob_files.len() < ids.len()`) passes. This was fixed in PR `#208` to prevent leaking FDs into the descriptor-table cache on partial/failed recovery. Do not flag the absence of this deferral as a gap in future reviews of PR `#208` or later code.

Learnt from: polaz
URL: https://github.com/structured-world/coordinode-lsm-tree/pull/208

Timestamp: 2026-04-04T20:34:35.847Z
Learning: In `src/table/inner.rs` (structured-world/coordinode-lsm-tree), `Inner::Drop` evicts the descriptor-table cache entry (via `FileAccessor::remove_for_table`) before calling `fs.remove_file` when the `is_deleted` guard fires. This ensures the cached FD is closed before the filesystem-level removal, which is required on Windows (open-file deletion fails). This was fixed in PR `#208`. Do not flag the absence of this evict-before-remove ordering in future reviews of PR `#208` or later code.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 202
File: src/table/tests.rs:2113-2118
Timestamp: 2026-04-02T23:53:51.122Z
Learning: In `src/table/block_index/iter.rs` and `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound` contains an unconditional restore (the `else { true }` branch at lines ~787-788 of decoder.rs) that always re-pushes a covering entry onto `hi_scanner.stack` when the stack would otherwise empty. This means `seek_upper_bound_cursor` always returns `true` for well-formed block data, regardless of `restart_interval`. Consequently, the `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` (empty-child path) cannot be forced from test configuration (e.g., `use_index_block_restart_interval(4)`) on non-corrupt data — it is only reachable via `fill_stack`/`advance_upper_restart_interval` decode failure. Regression tests for the two-level index empty-child skip behavior (PR `#202`, issue `#194`) validate overall iteration correctness defensively, not the exact `Ok(None)` corruption path. Do not suggest forcing this path via `restart_interval` changes in reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-27T07:10:44.958Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the restart-key span validation is intentionally split: strict bounds hardening (buffer/offset checks) was implemented in PR `#182`, while full semantic restart-key span validation (which requires passing the restart-key span through decoder state) is tracked separately in issue `#183`. Do not flag the absence of semantic-span validation in decoder.rs as an unaddressed gap in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:644-703
Timestamp: 2026-03-28T19:57:49.231Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `trim_back_to_upper_bound`'s `should_restore` branch intentionally re-pushes `candidate_offset` (the first popped entry with `cmp == Greater`) back onto `hi_scanner.stack` when the remaining stack top compares `Less` to the needle or the stack is empty. This preserves the covering interval boundary required for correct reverse-iteration (`seek_upper`) semantics across restart intervals greater than 1. Dropping this restore regresses `seek_upper` by losing the upper extent of the current restart interval. Do not flag this re-push as returning an out-of-range entry in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T22:48:06.079Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the "binary search trusts semantically-corrupted binary index" concern (lines 245-299) is explicitly out of scope for PR `#182`. The scenario requires a corrupted binary index that passes the block checksum (encoder bug or checksum collision). Adding O(n) monotonicity validation per seek call would regress the hot path for a case already caught by checksums. This hardening is tracked in issue `#184` (fallible decoder constructor). Do not flag the absence of monotonicity validation on the binary-index seek path in reviews of PR `#182` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:236-243
Timestamp: 2026-03-30T17:47:54.258Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `get_key_at` calls `Item::parse_full` on every binary-search probe inside `partition_point` / `partition_point_2` as a result of the bounds-hardening in PR `#182` (which replaced the previous unsafe `get_unchecked` + `parse_restart_key` path). Threading `entries_end` into `parse_restart_key()` to restore a lighter bounded fast path is tracked in issue `#190`. Do not flag the full-item decode in `get_key_at` as an unaddressed gap in reviews of PR `#182` or later code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-31T14:37:26.098Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `unwrap!` calls on `read_u8`/`read_u32` inside `Decoder::try_new()` (lines ~139–151) will panic on a truncated trailer slice. This is guarded at a higher level: `Block::from_reader` and checksum validation reject undersized blocks before they reach the decoder, and `Trailer::trailer_offset()` would underflow on undersized blocks — making this a pre-existing pipeline invariant, not introduced by PR `#191`. Making `Trailer::as_slice()` and its downstream reads fallible to handle truncated/undersized blocks gracefully is tracked in issue `#192`. Do not re-flag the `unwrap!` reads in `Decoder::try_new()` as an unaddressed gap in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-28T13:41:55.184Z
Learning: In `src/table/index_block/block_handle.rs` (structured-world/coordinode-lsm-tree), the `parse_truncated` function now validates `shared_prefix_len` against both the entry-start boundary AND the restart-key span (`base_key_end`). This was fixed in PR `#182` commit `f64f90e` (originally tracked as follow-up issue `#186`). The function signature was updated to accept a `base_key_end: usize` parameter, and the prefix-end check was changed from `prefix_end > offset` to `prefix_end > base_key_end`. Do not flag the absence of this restart-key span check in reviews of PR `#182` or later code — it is fully implemented.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/two_level.rs:152-157
Timestamp: 2026-04-02T08:18:35.857Z
Learning: In `src/table/block_index/two_level.rs` (structured-world/coordinode-lsm-tree), the empty-child-partition early-return behavior (where `OwnedIndexBlockIter::from_block_with_bounds` returning `Ok(None)` for a trimmed boundary partition causes `next()`/`next_back()` to stop the two-level scan prematurely instead of continuing to walk the TLI) is pre-existing behavior, not introduced by PR `#191`. This is tracked in issue `#194`. Do not flag the absence of a TLI-walking loop for empty child partitions in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:366-372
Timestamp: 2026-03-30T13:21:41.943Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `poison_back_cursor()` now delegates to `clamp_upper_to_lo()` instead of clearing `hi_scanner.base_key_offset` to `None`. This preserves a hard forward upper bound (`base_key_offset = Some(0)`) so that a caller switching from `next_back()` to `next()` after corruption cannot resume past the poisoned interval. Regression test `poison_back_cursor_also_stops_forward_next` covers this path. Do not flag `poison_back_cursor` as fail-open in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-30T04:19:33.033Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), `advance_upper_restart_interval()` fail-closes by calling `clamp_upper_to_lo()` when `fill_stack()` poisons the next interval. This preserves the hard upper bound on the hi_scanner so that forward iteration cannot escape past a corrupted interval boundary. This was implemented in PR `#182`. Do not flag the absence of this fail-close as a gap in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/index_block/iter.rs:80-97
Timestamp: 2026-03-30T22:53:39.583Z
Learning: In `src/table/index_block/iter.rs` (structured-world/coordinode-lsm-tree), `seek_upper_impl` is called with `reset_front=true` by the public `seek_upper` (a full back-cursor re-seek that does not depend on the low cursor) and with `reset_front=false` by `seek_upper_bound_cursor` (which preserves `front_peeked` seeded by a prior `seek_lower_bound_cursor` under the `lo ≤ hi` invariant). Do not flag the absence of a low-cursor rewind in `seek_upper_impl` when `reset_front=true` — the low cursor position is irrelevant for a full back-cursor re-seek, and the bounded path correctly preserves it. Do not re-raise this concern in reviews of PR `#182` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/block_index/iter.rs:80-83
Timestamp: 2026-04-02T12:06:47.137Z
Learning: In `src/table/block_index/iter.rs` (structured-world/coordinode-lsm-tree), the bound-cursor helpers `seek_upper_bound_cursor` and `seek_lower_bound_cursor` return `bool` (not `Result`), so `from_block_with_bounds` treats a failed/clamped upper-bound cursor as `Ok(None)` (empty range) rather than propagating an error. This is pre-existing API design, out of scope for PR `#191`, and tracked in issue `#196`. Do not re-flag the absence of a fallible Result from these bound-cursor helpers in reviews of PR `#191` or earlier code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-01T21:25:58.275Z
Learning: In `src/table/scanner.rs` (structured-world/coordinode-lsm-tree), after commit d6dde989 in PR `#191`, the `fetch_next_block` error path sets `self.read_count = self.block_count` before returning the error, exhausting the scanner on mid-scan block-fetch failures — consistent with the `try_new` error path (commit d66197d). Both error paths now poison the scanner so callers cannot silently skip corrupt blocks. Do not flag the absence of this scanner-poison behavior on `fetch_next_block` errors in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:19:04.825Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::sync_directory` was hardened in PR `#208` (commit 76f3b676) to distinguish between a path that exists as a file (returns an error) and a path that does not exist (returns `NotFound`). Do not flag the absence of this distinction in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `fsync_directory` no longer has a platform-conditional Windows no-op branch (removed in PR `#208`). It now unconditionally delegates to `Fs::sync_directory`. The Windows no-op behavior is encapsulated inside `StdFs::sync_directory`. Do not flag the absence of a platform-conditional Windows branch in `fsync_directory` in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/vlog/mod.rs:37-43
Timestamp: 2026-04-03T15:28:06.759Z
Learning: In `src/vlog/mod.rs` (structured-world/coordinode-lsm-tree), `recover_blob_files` intentionally returns `Ok((vec![], vec![]))` when `fs.read_dir(folder)` yields `ErrorKind::NotFound`. Standard (non-blob) trees never create a blobs folder, so `ids` is empty in that case and the downstream `blob_files.len() < ids.len()` guard (0 < 0 = false) passes correctly. Missing blob files for actual blob trees are handled by callers via orphan detection. This mirrors the pre-PR `folder.try_exists()` early-return and is not a correctness bug. Do not flag this early return as skipping the missing-data guard in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T07:48:17.153Z
Learning: In `src/config/mod.rs` (structured-world/coordinode-lsm-tree), `all_tables_folders` deduplicates by path only. This is correct: the same filesystem path always refers to the same on-disk directory, so path identity is sufficient for dedup. Routing the same path through different Fs backends is a configuration error that is already prevented upstream by `level_routes` validation (overlapping key ranges are rejected). Do not flag the absence of Fs-backend-aware deduplication in `all_tables_folders` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:120-135
Timestamp: 2026-03-23T20:55:26.977Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/lsm-tree), `IoUringFile` in append mode tracks EOF via an explicit `AtomicU64` cursor rather than relying on `O_APPEND` kernel semantics. This is intentional: lsm-tree uses single-writer-per-file for SSTs, journals, and blob files, so atomic multi-writer appends are not required. Do not flag this as a bug or semantic violation in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/fs/mem_fs.rs:285-300
Timestamp: 2026-04-04T11:18:56.220Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::open()` and `MemFs::rename()` intentionally only check the immediate parent directory, not the full ancestor chain. lsm-tree always calls `create_dir_all` before creating files, so intermediate file-vs-dir conflicts can only arise from external filesystem corruption, which MemFs is not responsible for defending against. A full ancestor walk would add O(depth) cost to every `open()` call. Do not flag the absence of full ancestor-chain validation in MemFs::open() or MemFs::rename() in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 106
File: src/fs/io_uring_fs.rs:317-326
Timestamp: 2026-03-23T23:29:57.067Z
Learning: In `src/fs/io_uring_fs.rs` (structured-world/coordinode-lsm-tree), `IoUringFile::write()` in append mode intentionally calls `self.file.metadata()?.len()` (fstat) on every write instead of maintaining an in-memory EOF cursor. This is a deliberate correctness trade-off: a separate in-memory append cursor could be invalidated by seek()-before-write() sequences on append-mode files. The ~100ns fstat cost is considered negligible for the journal/SST append patterns used in lsm-tree. Do not flag this as a performance issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 208
File: src/file.rs:64-72
Timestamp: 2026-04-03T21:19:43.478Z
Learning: In `src/file.rs` (structured-world/coordinode-lsm-tree), `rewrite_atomic` is always called with absolute paths (from `Tree::open` and `persist_version`). The `path.parent().expect("should have a parent")` call is intentional and documented; the empty-parent defensive normalization to `"."` is unnecessary. Do not flag the absence of empty-parent normalization in `rewrite_atomic` in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-03T17:42:38.208Z
Learning: In `src/tree/mod.rs` (structured-world/coordinode-lsm-tree), around line 1495, `Tree::open` intentionally checks `CURRENT` via `std::fs` instead of the pluggable `Fs` trait. This is a documented known limitation: MemFs-backed trees cannot be fully re-opened (`Tree::open` reopen is unsupported for MemFs). The bypass has an inline code comment and is tracked for resolution in issue `#209`. Do not flag this `std::fs` usage as a missing Fs-trait wiring in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T19:18:08.308Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), empty-path validation is centralized in an `ensure_non_empty_path()` helper that is called at the top of `MemFs::open()`, `MemFs::create_dir_all()`, and `MemFs::rename()`. It returns `InvalidInput` for empty paths before any state mutation. This was added in PR `#208`. Do not flag the absence of inline empty-path checks in those methods in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::write(&[])` returns `Ok(0)` immediately after the writable-access check (before acquiring the data lock or computing the write position). This was fixed in PR `#208`. Do not flag the absence of this early-return optimization in future reviews of PR `#208` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-04T16:44:00.075Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), parent-directory existence validation is centralized in an `ensure_parent_dir()` helper shared by `MemFs::open()` and `MemFs::rename()`. This was extracted in PR `#208`, eliminating duplicated logic and allowing the `#[expect(clippy::too_many_lines)]` attribute to be removed from `open()`. Do not flag duplicated parent-dir checks between `open()` and `rename()` as a gap in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:80-85
Timestamp: 2026-04-03T16:37:26.531Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFile::lock_exclusive()` intentionally returns `Ok(())` as a no-op. `MemFs` is a test/ephemeral backend where cross-process exclusivity is not meaningful; all in-process access is already serialized through the shared `Arc<RwLock<State>>`. Do not flag the no-op `lock_exclusive()` implementation as a correctness issue in future reviews.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 182
File: src/table/block/decoder.rs:104-110
Timestamp: 2026-03-27T12:08:02.765Z
Learning: In `src/table/block/decoder.rs` (structured-world/coordinode-lsm-tree), the `assert!(restart_interval > 0, ...)` in `Decoder::new` is an intentional placeholder: the proper fix — a fallible decoder constructor that propagates a corruption error instead of panicking — is a wider refactor tracked in issue `#184`. A `TODO(`#184`)` comment was added at the assert site in PR `#182`. Do not re-flag this assert as a missing fallible validation in reviews of PR `#182` or earlier code; it is a known, tracked follow-up.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 191
File: src/table/iter.rs:92-97
Timestamp: 2026-04-02T16:28:24.715Z
Learning: In `src/table/iter.rs` and `src/table/util.rs` (structured-world/coordinode-lsm-tree), the `cache.get_block(...)` fast path inside `load_block` (util.rs:57-76) returns cached blocks without validating `block_type`. This is a pre-existing pattern not introduced by PR `#191`; the `create_data_block_reader` helper in `iter.rs` inherits the same gap. Block-type validation on the cache-hit path is tracked in issue `#198`. Do not re-flag the absence of `BlockType::Data` validation on the cache-hit path in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 0
File: :0-0
Timestamp: 2026-04-02T15:52:05.196Z
Learning: In `src/table/block_index/volatile.rs` (structured-world/coordinode-lsm-tree), after commit b8dbd1cd in PR `#191`, a private `init_inner()` helper was extracted to deduplicate the lazy-init load/build/poison flow shared between `next()` and `next_back()`. It also marks the iterator exhausted when `OwnedIndexBlockIter::from_block_with_bounds` returns `Ok(None)` (empty/trimmed boundary range), preventing repeated `load_block` I/O on subsequent iteration calls. Do not flag the absence of this deduplication or the repeated-I/O issue in reviews of PR `#191` or later code.

Learnt from: polaz
Repo: structured-world/coordinode-lsm-tree PR: 207
File: src/fs/mem_fs.rs:377-388
Timestamp: 2026-04-03T11:37:54.884Z
Learning: In `src/fs/mem_fs.rs` (structured-world/coordinode-lsm-tree), `MemFs::rename()` intentionally only supports file renames (not directory renames). lsm-tree only renames files (table files, version pointers like CURRENT), so directory rename support is not needed. The method returns `NotFound` for directories, and this limitation is documented in the code. Do not flag the absence of directory rename support as a bug.

@polaz polaz requested a review from Copilot April 4, 2026 20:41
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Comment thread src/fs/mem_fs.rs
Comment thread src/fs/mem_fs.rs Outdated
Comment thread src/fs/mem_fs.rs Outdated
Comment thread src/fs/mem_fs.rs Outdated
MemFile::read, write, set_len, and read_at used unchecked `as usize`
casts on u64 cursor/offset/size values. On 32-bit targets these
silently truncate, corrupting file state. Now use usize::try_from
with InvalidInput errors. Removed all cast_possible_truncation expects.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comment thread src/table/inner.rs Outdated
Inner::Drop now moves the FileAccessor out via std::mem::replace
(with a Closed sentinel) and drops it before calling fs.remove_file.
This ensures both pinned File handles and descriptor-table cached FDs
are released before deletion, fixing remove_file failures on Windows
where open handles prevent file removal.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/fs/mem_fs.rs`:
- Around line 311-323: The code currently rejects append+truncate
unconditionally; change the checks so truncate+append is only an error when
create_new is not set. Specifically, update the first check around opts.truncate
&& opts.append to allow the combination when opts.create_new is true (e.g., only
return the InvalidInput error if opts.truncate && opts.append &&
!opts.create_new). Likewise, update the truncate vs write check to not fail when
opts.create_new is set (i.e., only error if opts.truncate && !opts.write &&
!opts.create_new) so create_new+append+truncate follows std::fs::OpenOptions
behavior; locate and modify these checks around the opts variable in the
function that handles FsOpenOptions/wants_write in mem_fs.rs.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f7d89a8-3d8b-4e26-aee9-1676d695eb4c

📥 Commits

Reviewing files that changed from the base of the PR and between 5662b67 and e911497.

📒 Files selected for processing (1)
  • src/fs/mem_fs.rs

Comment thread src/fs/mem_fs.rs
Comment on lines +311 to +323
if opts.truncate && opts.append {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"truncate and append cannot be used together",
));
}
if opts.truncate && !opts.write {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"truncate requires write access",
));
}
if (opts.create || opts.create_new) && !wants_write {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -330 src/fs/mem_fs.rs | tail -40

Repository: structured-world/coordinode-lsm-tree

Length of output: 1642


🌐 Web query:

std::fs::OpenOptions create_new truncate append combination behavior

💡 Result:

When using std::fs::OpenOptions with create_new(true), the create and truncate options are ignored, regardless of whether append(true) is also set. If the file does not exist: - The file is created as new (empty). - The file is opened successfully with the specified access modes (must include write or append). - If append(true) is set, all writes will append to the end of the file (which starts empty). - truncate(true) has no effect since it's ignored and the file is new anyway. If the file already exists: - open fails with io::ErrorKind::AlreadyExists. This combination is explicitly allowed (no InvalidInput error). Note that read-append with create_new is permitted per the design in RFC 1252. Example that succeeds (file created empty, writes append): use std::fs::OpenOptions; let mut opts = OpenOptions::new; opts.write(true).append(true).create_new(true).truncate(true).open("foo.txt")?; Since create_new succeeds only for new files, truncate is irrelevant, and append ensures writes go to EOF. Official Rust documentation confirms: "If .create_new(true) is set, .create and .truncate are ignored." Sources also list allowed combinations including create_new with append.

Citations:


Allow create_new + append + truncate to reach the create-new path.

FsOpenOptions is documented as mirroring std::fs::OpenOptions, but the checks at lines 311–316 reject append(true).create_new(true).truncate(true) unconditionally. In std, create_new(true) ignores truncate(true), so this combination is explicitly allowed and should be accepted here.

Proposed fix
         if opts.truncate && opts.append {
+            if opts.create_new {
+                // create_new(true) ignores truncate(true), so this is allowed
+            } else {
                 return Err(io::Error::new(
                     io::ErrorKind::InvalidInput,
                     "truncate and append cannot be used together",
                 ));
+            }
         }
         if opts.truncate && !opts.write {
             return Err(io::Error::new(
                 io::ErrorKind::InvalidInput,
-                "truncate requires write access",
+                "truncate requires write or append access",
             ));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fs/mem_fs.rs` around lines 311 - 323, The code currently rejects
append+truncate unconditionally; change the checks so truncate+append is only an
error when create_new is not set. Specifically, update the first check around
opts.truncate && opts.append to allow the combination when opts.create_new is
true (e.g., only return the InvalidInput error if opts.truncate && opts.append
&& !opts.create_new). Likewise, update the truncate vs write check to not fail
when opts.create_new is set (i.e., only error if opts.truncate && !opts.write &&
!opts.create_new) so create_new+append+truncate follows std::fs::OpenOptions
behavior; locate and modify these checks around the opts variable in the
function that handles FsOpenOptions/wants_write in mem_fs.rs.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comment thread src/file_accessor.rs
Comment on lines 15 to 32
///
/// This is used in case file descriptor cache is `None` (to skip cache lookups)
File(Arc<dyn FsFile>),

/// Access to file descriptor cache
DescriptorTable(Arc<DescriptorTable>),
/// Access to file descriptor cache with [`Fs`]-based fallback for
/// cache misses.
DescriptorTable {
/// The FD cache.
table: Arc<DescriptorTable>,
/// Filesystem backend for opening files on cache miss.
fs: Arc<dyn Fs>,
},

/// Sentinel used during [`Drop`] to move ownership of the file handle
/// before deleting the underlying file. Not constructed outside `Drop`.
#[doc(hidden)]
Closed,
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

FileAccessor is a publicly exported enum (#[doc(hidden)] pub mod file_accessor), and this change adds a new Closed variant. Adding variants to a public enum is a semver-breaking API change and also allows external callers to construct FileAccessor::Closed, which is meant to be an internal sentinel (per the doc comment). Consider making FileAccessor pub(crate) (or moving it to a private module), or marking it #[non_exhaustive] and providing a crate-private sentinel type so external code cannot construct the internal-only state.

Copilot uses AI. Check for mistakes.
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

Closing to squash history and reopen with clean commits.

@polaz polaz closed this Apr 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants