Skip to content

Bloom filter - #7

Draft
cole-h wants to merge 1 commit into
mainfrom
bloom
Draft

Bloom filter#7
cole-h wants to merge 1 commit into
mainfrom
bloom

Conversation

@cole-h

@cole-h cole-h commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added optional Bloom-filter support for snapshot creation, validation, encoding, decoding, and concurrent updates.
    • Added source-shard indexing with generation, range, watermark, and timestamp validation.
    • Added live Bloom update events, including path-created and heartbeat records.
    • Added snapshot manifests, integrity checks, statistics, and compatibility validation.
  • Tests

    • Added comprehensive coverage for snapshots, source shards, wire events, validation, corruption handling, and concurrency.
  • Benchmarks

    • Added performance benchmarks for snapshot construction, decoding, validation, installation, and streaming.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58962edf-c1b5-40f9-83f6-641a49131669

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

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

Assisted-by: Amp <amp@ampcode.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
benches/snapshot_build.rs (1)

15-30: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Measure construction inside a benchmark iteration.

snapshot_fixture creates the BloomSnapshot and inserts all 7,642 hashes before Criterion starts timing. The snapshot_build_256_mib group therefore measures snapshot preparation, not snapshot construction.

Add a separate timed case for BloomSnapshot::new plus insertion. Alternatively, rename this group to snapshot preparation. This prevents preparation timings from being reported as construction timings.

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

In `@benches/snapshot_build.rs` around lines 15 - 30, Update the benchmark around
snapshot_build and snapshot_fixture so BloomSnapshot::new plus all hash
insertions execute inside a Criterion-timed iteration, rather than being fully
prepared before timing starts. Keep the existing fixture for any separate
preparation benchmark, or rename the current group if it intentionally measures
preparation; ensure the reported snapshot_build_256_mib measurement accurately
represents construction.
src/bloom/source/tests.rs (1)

54-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the extra borrow.

Path::new(key) already returns &Path. The extra & creates &&Path and relies on deref coercion. clippy::needless_borrow flags this pattern.

♻️ Proposed change
-        assert!(ShardKey::parse(&Path::new(key)).is_err(), "{key}");
+        assert!(ShardKey::parse(Path::new(key)).is_err(), "{key}");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bloom/source/tests.rs` at line 54, Remove the redundant borrow in the
ShardKey::parse assertion by passing Path::new(key) directly, preserving the
existing error assertion and message.
src/bloom/source/mod.rs (1)

220-227: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

decode allocates the full record vector before the size check.

serde_json::from_slice builds the whole Vec<SourceRecord> first. validate_records then rejects a shard with more than SOURCE_SHARD_FLUSH_ROWS rows. A corrupt or oversized object therefore allocates before rejection.

The shards are written by this system, so the risk is limited. If you want a hard bound, check encoded.len() against a maximum byte size before parsing.

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

In `@src/bloom/source/mod.rs` around lines 220 - 227, Update SourceRecord::decode
to enforce a maximum encoded byte size against SOURCE_SHARD_FLUSH_ROWS before
calling serde_json::from_slice, returning the existing size/validation error
when the bound is exceeded; retain validate_records for row-count validation
after parsing.
src/bloom/mod.rs (2)

366-366: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Resolve the FIXME before this leaves draft state.

BloomSnapshot and ConcurrentBloomFilter duplicate the bit-address arithmetic in insert and contains, and both duplicate the byte/bit comment block. One shared helper that maps a position to (byte_pos, bit_pos) removes the duplication without changing the two storage representations.

Do you want me to open an issue to track this consolidation?

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

In `@src/bloom/mod.rs` at line 366, Resolve the FIXME by extracting the shared
position-to-(byte_pos, bit_pos) bit-address calculation from BloomSnapshot and
ConcurrentBloomFilter insert and contains implementations into one helper. Reuse
that helper in all four methods, retain each type’s existing storage
representation, and remove the duplicated byte/bit comment blocks.

445-490: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Each statistics accessor scans the full body.

fill_ratio, estimated_false_positive_rate, and estimated_distinct_items each call stats(), and stats() counts the set bits of the whole body. At the current policy the body is 256 MiB. A caller that reads all three values performs three full scans.

Consider documenting that callers should call stats() once and read the three fields, or cache the set-bit count in BloomSnapshot and invalidate it on insert.

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

In `@src/bloom/mod.rs` around lines 445 - 490, Avoid repeated full-body scans from
the statistics accessors by caching the set-bit count in BloomSnapshot and
invalidating or updating that cache whenever insert mutates the bits; ensure
stats() and the accessors reuse the cached count while preserving correct values
after inserts. Alternatively, document in the public accessor methods that
callers should invoke stats() once and read its fields, if caching is not
appropriate.
src/bloom/tests.rs (1)

74-93: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case for an event that carries more positions than k.

The tests cover an event with fewer positions than k (rejected) and an event from a smaller publisher (rejected). No test covers an event whose position count is larger than this filter's k. That path currently sets extra bits; see the insert_positions comment in src/bloom/mod.rs at Line 748.

Also, at Line 83 M_BITS is already u64, so u64::from(M_BITS) is a redundant conversion.

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

In `@src/bloom/tests.rs` around lines 74 - 93, Add a test alongside
positions_with_fewer_probes_than_the_filter_are_rejected covering positions with
more entries than the filter’s k, and assert insert_positions handles the extra
positions as specified by its comment without incorrectly setting extra bits. In
the existing test, replace the redundant u64::from(M_BITS) conversion with
M_BITS.
Cargo.toml (1)

36-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a feature-off check job.

criterion 0.8.2 exists and provides cargo_bench_support.

Because cargo test builds dev-dependencies, the self dependency enables bloom; cargo test --no-default-features does not provide feature-off coverage. Use cargo check --no-default-features without --all-targets, or isolate the dev dependency.

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

In `@Cargo.toml` around lines 36 - 38, Add a feature-off validation job or
workflow step that runs cargo check --no-default-features without --all-targets,
ensuring dev-dependency feature unification does not enable bloom during the
check. Keep the existing criterion and flakehub-cache-types dev-dependency
declarations unchanged unless isolating the self-dependency is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bloom/mod.rs`:
- Around line 729-760: Update insert_positions to iterate only over the prefix
of positions whose length is self.dims.k, while retaining the existing
minimum-position validation and bit-setting logic. Use the first k positions so
streamed events with larger k match this filter’s insert behavior.

---

Nitpick comments:
In `@benches/snapshot_build.rs`:
- Around line 15-30: Update the benchmark around snapshot_build and
snapshot_fixture so BloomSnapshot::new plus all hash insertions execute inside a
Criterion-timed iteration, rather than being fully prepared before timing
starts. Keep the existing fixture for any separate preparation benchmark, or
rename the current group if it intentionally measures preparation; ensure the
reported snapshot_build_256_mib measurement accurately represents construction.

In `@Cargo.toml`:
- Around line 36-38: Add a feature-off validation job or workflow step that runs
cargo check --no-default-features without --all-targets, ensuring dev-dependency
feature unification does not enable bloom during the check. Keep the existing
criterion and flakehub-cache-types dev-dependency declarations unchanged unless
isolating the self-dependency is required.

In `@src/bloom/mod.rs`:
- Line 366: Resolve the FIXME by extracting the shared position-to-(byte_pos,
bit_pos) bit-address calculation from BloomSnapshot and ConcurrentBloomFilter
insert and contains implementations into one helper. Reuse that helper in all
four methods, retain each type’s existing storage representation, and remove the
duplicated byte/bit comment blocks.
- Around line 445-490: Avoid repeated full-body scans from the statistics
accessors by caching the set-bit count in BloomSnapshot and invalidating or
updating that cache whenever insert mutates the bits; ensure stats() and the
accessors reuse the cached count while preserving correct values after inserts.
Alternatively, document in the public accessor methods that callers should
invoke stats() once and read its fields, if caching is not appropriate.

In `@src/bloom/source/mod.rs`:
- Around line 220-227: Update SourceRecord::decode to enforce a maximum encoded
byte size against SOURCE_SHARD_FLUSH_ROWS before calling serde_json::from_slice,
returning the existing size/validation error when the bound is exceeded; retain
validate_records for row-count validation after parsing.

In `@src/bloom/source/tests.rs`:
- Line 54: Remove the redundant borrow in the ShardKey::parse assertion by
passing Path::new(key) directly, preserving the existing error assertion and
message.

In `@src/bloom/tests.rs`:
- Around line 74-93: Add a test alongside
positions_with_fewer_probes_than_the_filter_are_rejected covering positions with
more entries than the filter’s k, and assert insert_positions handles the extra
positions as specified by its comment without incorrectly setting extra bits. In
the existing test, replace the redundant u64::from(M_BITS) conversion with
M_BITS.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51677987-eafc-452c-9d93-f53cbfffcf8a

📥 Commits

Reviewing files that changed from the base of the PR and between 6acc899 and 730f789.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml
  • benches/snapshot_build.rs
  • src/bloom/mod.rs
  • src/bloom/source/mod.rs
  • src/bloom/source/tests.rs
  • src/bloom/tests.rs
  • src/bloom/wire/mod.rs
  • src/bloom/wire/tests.rs
  • src/lib.rs

Comment thread src/bloom/mod.rs
Comment on lines +729 to +760
pub fn insert_positions(&self, positions: &wire::ProbePositions) -> Result<(), BloomError> {
if positions.m_bits() < self.dims.m_bits {
return Err(BloomError::IncompatiblePositions {
event_m_bits: positions.m_bits(),
filter_m_bits: self.dims.m_bits,
});
}

// Position `i` depends only on `i` (`h1 + i*h2`), so a filter with a smaller `k` probes a
// prefix of the event's positions. An event with fewer positions than this filter's `k`
// leaves required probe bits unset, which would create a false negative.
if positions.positions().len() < usize::from(self.dims.k) {
return Err(BloomError::InsufficientPositions {
count: positions.positions().len(),
k: self.dims.k,
});
}

let mask = self.dims.m_bits - 1;
for position in positions.positions() {
// A bit position points to byte `position / 8`, and to bit `position % 8` in that byte.
// The bit order is least-significant bit first. `1 << (position % 8)` makes a mask that
// has one bit set. The OR operation sets that bit. The other bits in the byte do not
// change.
let position = position & mask;
let byte_pos = (position / 8) as usize;
let bit_pos = position % 8;
self.bits[byte_pos].fetch_or(1 << bit_pos, Ordering::Relaxed);
}

Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Truncate streamed positions to this filter's k.

The loop applies every position in the event. If the publisher uses a larger k than this filter, the filter sets more bits per record than its own insert would set. The extra bits raise the fill ratio and the false-positive rate, but contains still checks only the first self.dims.k positions, so the extra bits give no benefit.

Position i depends only on i, so the first k positions are exactly the ones this filter probes. Take only that prefix.

🐛 Proposed fix
         let mask = self.dims.m_bits - 1;
-        for position in positions.positions() {
+        for position in positions
+            .positions()
+            .iter()
+            .take(usize::from(self.dims.k))
+        {
             // A bit position points to byte `position / 8`, and to bit `position % 8` in that byte.
             // The bit order is least-significant bit first. `1 << (position % 8)` makes a mask that
             // has one bit set. The OR operation sets that bit. The other bits in the byte do not
             // change.
             let position = position & mask;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn insert_positions(&self, positions: &wire::ProbePositions) -> Result<(), BloomError> {
if positions.m_bits() < self.dims.m_bits {
return Err(BloomError::IncompatiblePositions {
event_m_bits: positions.m_bits(),
filter_m_bits: self.dims.m_bits,
});
}
// Position `i` depends only on `i` (`h1 + i*h2`), so a filter with a smaller `k` probes a
// prefix of the event's positions. An event with fewer positions than this filter's `k`
// leaves required probe bits unset, which would create a false negative.
if positions.positions().len() < usize::from(self.dims.k) {
return Err(BloomError::InsufficientPositions {
count: positions.positions().len(),
k: self.dims.k,
});
}
let mask = self.dims.m_bits - 1;
for position in positions.positions() {
// A bit position points to byte `position / 8`, and to bit `position % 8` in that byte.
// The bit order is least-significant bit first. `1 << (position % 8)` makes a mask that
// has one bit set. The OR operation sets that bit. The other bits in the byte do not
// change.
let position = position & mask;
let byte_pos = (position / 8) as usize;
let bit_pos = position % 8;
self.bits[byte_pos].fetch_or(1 << bit_pos, Ordering::Relaxed);
}
Ok(())
}
pub fn insert_positions(&self, positions: &wire::ProbePositions) -> Result<(), BloomError> {
if positions.m_bits() < self.dims.m_bits {
return Err(BloomError::IncompatiblePositions {
event_m_bits: positions.m_bits(),
filter_m_bits: self.dims.m_bits,
});
}
// Position `i` depends only on `i` (`h1 + i*h2`), so a filter with a smaller `k` probes a
// prefix of the event's positions. An event with fewer positions than this filter's `k`
// leaves required probe bits unset, which would create a false negative.
if positions.positions().len() < usize::from(self.dims.k) {
return Err(BloomError::InsufficientPositions {
count: positions.positions().len(),
k: self.dims.k,
});
}
let mask = self.dims.m_bits - 1;
for position in positions
.positions()
.iter()
.take(usize::from(self.dims.k))
{
// A bit position points to byte `position / 8`, and to bit `position % 8` in that byte.
// The bit order is least-significant bit first. `1 << (position % 8)` makes a mask that
// has one bit set. The OR operation sets that bit. The other bits in the byte do not
// change.
let position = position & mask;
let byte_pos = (position / 8) as usize;
let bit_pos = position % 8;
self.bits[byte_pos].fetch_or(1 << bit_pos, Ordering::Relaxed);
}
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bloom/mod.rs` around lines 729 - 760, Update insert_positions to iterate
only over the prefix of positions whose length is self.dims.k, while retaining
the existing minimum-position validation and bit-setting logic. Use the first k
positions so streamed events with larger k match this filter’s insert behavior.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant