use std::sync::atomic::{AtomicU64, Ordering};
use crate::extract::{FileMapL1, FileMapL2};
use crate::index::{IndexDb, writer::IndexWriter};
use crate::path::RelPath;
use crate::scanner::FileResult;
use crate::store::Store;
const INDEX_COMMIT_BATCH: usize = 256;
const INDEX_COMMIT_BATCH_BYTES: u64 = 8 * 1024 * 1024;
const INDEX_STAGED_BYTES_CEILING: u64 = 64 * 1024 * 1024;
const CEILING_MIN_COMMIT_BYTES: u64 = 1024 * 1024;
pub(crate) struct StagedByteLedger {
in_flight: AtomicU64,
ceiling: u64,
commits: AtomicU64,
}
impl StagedByteLedger {
pub(crate) const fn new(ceiling: u64) -> Self {
Self {
in_flight: AtomicU64::new(0),
ceiling,
commits: AtomicU64::new(0),
}
}
pub(crate) fn charge(&self, bytes: u64) {
self.in_flight.fetch_add(bytes, Ordering::Relaxed);
}
pub(crate) fn release(&self, bytes: u64) {
self.in_flight.fetch_sub(bytes, Ordering::Relaxed);
}
pub(crate) fn in_flight(&self) -> u64 {
self.in_flight.load(Ordering::Relaxed)
}
pub(crate) fn over_ceiling(&self) -> bool {
self.in_flight() >= self.ceiling
}
fn record_commit(&self) {
self.commits.fetch_add(1, Ordering::Relaxed);
}
#[cfg(test)]
fn commits(&self) -> u64 {
self.commits.load(Ordering::Relaxed)
}
}
pub(crate) static SCAN_STAGED_BYTES: StagedByteLedger = StagedByteLedger::new(INDEX_STAGED_BYTES_CEILING);
pub(crate) struct WorkerIndexBatch<'a> {
index: Option<&'a IndexDb>,
writer: Option<IndexWriter>,
staged: usize,
reported_bytes: u64,
byte_budget: u64,
ledger: &'static StagedByteLedger,
results: Vec<FileResult>,
}
impl<'a> WorkerIndexBatch<'a> {
pub(crate) fn new(store: &'a Store) -> Self {
Self::with_budget(store.index_db.as_ref(), INDEX_COMMIT_BATCH_BYTES, &SCAN_STAGED_BYTES)
}
fn with_budget(index: Option<&'a IndexDb>, byte_budget: u64, ledger: &'static StagedByteLedger) -> Self {
Self {
index,
writer: None,
staged: 0,
reported_bytes: 0,
byte_budget,
ledger,
results: Vec::new(),
}
}
pub(crate) fn stage(&mut self, rel: &RelPath, l1: &FileMapL1, l2: Option<&FileMapL2>) -> bool {
let Some(index) = self.index else {
return true;
};
let writer = self.writer.get_or_insert_with(|| index.writer());
let staged_ok = writer.upsert_file(rel, l1, l2).is_ok();
if staged_ok {
self.staged += 1;
}
self.commit_if_full();
staged_ok
}
#[cfg(feature = "code-search")]
pub(crate) fn stage_bm25(&mut self, rel: &RelPath, postings: &[crate::search::bm25::ChunkPosting]) {
let Some(index) = self.index else {
return;
};
let writer = self.writer.get_or_insert_with(|| index.writer());
if writer.upsert_bm25_file(rel, postings).is_err() {
tracing::warn!(rel = %rel, "bm25 upsert failed; keyword search may be incomplete");
}
self.commit_if_full();
}
pub(crate) fn stage_grep_bloom(&mut self, rel: &RelPath, bytes: &[u8], size: u64, mtime_ns: i64) {
let Some(index) = self.index else {
return;
};
let row = crate::index::grep_bloom::build_row(bytes, size, mtime_ns);
self.writer
.get_or_insert_with(|| index.writer())
.upsert_grep_bloom(rel, row);
self.commit_if_full();
}
pub(crate) fn restamp_grep_bloom(&mut self, rel: &RelPath, bytes: &[u8], size: u64, mtime_ns: i64) {
if self
.index
.is_some_and(|index| index.grep_bloom_current(rel, size, mtime_ns))
{
return;
}
self.stage_grep_bloom(rel, bytes, size, mtime_ns);
}
pub(crate) fn backfill_grep_bloom(&mut self, root: &std::path::Path, rel: &RelPath, size: u64, mtime_ns: i64) {
use std::io::Read;
let Some(index) = self.index else {
return;
};
if index.grep_bloom_current(rel, size, mtime_ns) {
return;
}
let Ok(file) = std::fs::File::open(root.join(rel.to_path_buf())) else {
return;
};
let Ok(meta) = file.metadata() else {
return;
};
if !meta.is_file() {
return;
}
let mut bytes = Vec::with_capacity(usize::try_from(meta.len()).unwrap_or(0));
if file.take(meta.len().saturating_add(1)).read_to_end(&mut bytes).is_err() {
return;
}
self.stage_grep_bloom(rel, &bytes, meta.len(), crate::scanner_file::mtime_nanos(&meta));
}
fn commit_if_full(&mut self) {
let staged_bytes = self.writer.as_ref().map_or(0, IndexWriter::staged_bytes);
self.ledger.charge(staged_bytes.saturating_sub(self.reported_bytes));
self.reported_bytes = staged_bytes;
let ceiling_hit = self.ledger.over_ceiling() && staged_bytes >= CEILING_MIN_COMMIT_BYTES;
if self.staged >= INDEX_COMMIT_BATCH || staged_bytes >= self.byte_budget || ceiling_hit {
self.commit();
}
}
fn commit(&mut self) {
if let Some(writer) = self.writer.take() {
self.ledger.record_commit();
if writer.commit().is_err() {
tracing::warn!("index batch commit failed; reference search may be incomplete");
}
}
self.release_reported();
self.staged = 0;
}
fn release_reported(&mut self) {
self.ledger.release(self.reported_bytes);
self.reported_bytes = 0;
}
pub(crate) fn push_result(&mut self, result: FileResult) {
self.results.push(result);
}
pub(crate) fn finish(mut self) -> Vec<FileResult> {
self.commit();
std::mem::take(&mut self.results)
}
}
impl Drop for WorkerIndexBatch<'_> {
fn drop(&mut self) {
self.release_reported();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::{Symbol, SymbolKind};
fn fresh_index() -> (tempfile::TempDir, IndexDb) {
let dir = tempfile::tempdir().expect("tempdir");
let db = IndexDb::open(dir.path()).expect("open index");
(dir, db)
}
fn bulky_l1(symbols: usize) -> FileMapL1 {
FileMapL1 {
schema_ver: crate::extract::SCHEMA_VER,
language: "rust".to_string(),
size_bytes: 0,
had_errors: false,
error_count: 0,
symbols: (0..symbols)
.map(|i| Symbol {
name: format!("generated_symbol_{i:04}"),
kind: SymbolKind::Function,
start_byte: i as u32 * 16,
end_byte: i as u32 * 16 + 8,
start_row: 0,
start_col: 0,
signature: Some("x".repeat(256)),
decorators: Vec::new(),
})
.collect(),
imports: Vec::new(),
implementations: Vec::new(),
rationale: Vec::new(),
extract_epoch: crate::extract::EXTRACT_EPOCH,
}
}
fn dump(keyspace: &fjall::Keyspace) -> Vec<(Vec<u8>, Vec<u8>)> {
keyspace
.iter()
.map(|guard| {
let (k, v) = guard.into_inner().expect("read entry");
((*k).to_vec(), (*v).to_vec())
})
.collect()
}
fn dump_all(db: &IndexDb) -> Vec<Vec<(Vec<u8>, Vec<u8>)>> {
vec![
dump(&db.symbols_by_path),
dump(&db.symbols_by_name),
dump(&db.calls_by_path),
dump(&db.calls_by_callee),
dump(&db.imports_by_module),
dump(&db.imports_by_path),
dump(&db.implementations_by_trait),
dump(&db.implementations_by_path),
dump(&db.code_bm25_postings),
dump(&db.code_bm25_by_path),
]
}
#[test]
fn byte_budget_commits_long_before_the_file_count_bound() {
static LEDGER: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_dir, db) = fresh_index();
let l1 = bulky_l1(32);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), 8 * 1024, &LEDGER);
for i in 0..8u32 {
assert!(batch.stage(&RelPath::from(format!("src/f{i}.rs")), &l1, None));
}
assert!(
batch.staged < INDEX_COMMIT_BATCH,
"the file-count bound must not be what committed here"
);
assert!(
db.symbols_by_path.iter().next().is_some(),
"the byte budget must have forced a commit already"
);
drop(batch);
assert_eq!(LEDGER.in_flight(), 0);
}
#[test]
fn the_file_count_bound_commits_when_neither_byte_bound_is_near() {
static ROOMY: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_dir, db) = fresh_index();
let l1 = bulky_l1(1);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &ROOMY);
for i in 0..INDEX_COMMIT_BATCH - 1 {
assert!(batch.stage(&RelPath::from(format!("src/f{i}.rs")), &l1, None));
}
assert!(
db.symbols_by_path.iter().next().is_none(),
"nothing may commit before the file counter reaches the bound"
);
assert!(batch.stage(&RelPath::from("src/last.rs"), &l1, None));
assert!(
db.symbols_by_path.iter().next().is_some(),
"the {INDEX_COMMIT_BATCH}th file must force the commit"
);
assert_eq!(batch.staged, 0, "the file counter resets at its own commit");
drop(batch);
assert_eq!(ROOMY.in_flight(), 0);
}
#[test]
fn the_production_byte_budget_commits_before_the_file_counter() {
let (_dir, db) = fresh_index();
let l1 = bulky_l1(8192);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), INDEX_COMMIT_BATCH_BYTES, &SCAN_STAGED_BYTES);
let mut files = 0usize;
while db.symbols_by_path.iter().next().is_none() && files < INDEX_COMMIT_BATCH {
assert!(batch.stage(&RelPath::from(format!("src/f{files}.rs")), &l1, None));
files += 1;
}
assert!(
db.symbols_by_path.iter().next().is_some(),
"staging {INDEX_COMMIT_BATCH} bulky files must have committed something"
);
assert!(
files < INDEX_COMMIT_BATCH,
"the production byte budget must force a commit before the {INDEX_COMMIT_BATCH}-file \
counter reaches it, or INDEX_COMMIT_BATCH_BYTES is not a memory bound in a real scan; \
committed only after {files} files"
);
drop(batch);
}
#[cfg(feature = "code-search")]
#[test]
fn bm25_only_staging_hits_the_byte_budget_and_commits() {
use crate::search::bm25::ChunkPosting;
static LEDGER: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_dir, db) = fresh_index();
fn postings(chunks: u32, terms: u32) -> Vec<ChunkPosting> {
(0..chunks)
.map(|chunk| ChunkPosting {
chunk_id: format!("hash:{chunk}"),
doclen: terms,
terms: (0..terms).map(|t| (format!("term_{t:04}"), 1)).collect(),
})
.collect()
}
let (_control_dir, control) = fresh_index();
let mut under_budget = WorkerIndexBatch::with_budget(Some(&control), 8 * 1024 * 1024, &LEDGER);
under_budget.stage_bm25(&RelPath::from("src/small.rs"), &postings(1, 4));
assert!(
control.code_bm25_postings.iter().next().is_none(),
"staging well under the byte budget must leave the batch uncommitted"
);
drop(under_budget);
assert_eq!(LEDGER.in_flight(), 0);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), 8 * 1024, &LEDGER);
batch.stage_bm25(&RelPath::from("src/generated.rs"), &postings(4, 512));
assert_eq!(batch.staged, 0, "bm25 staging must not bump the file counter");
assert!(
db.code_bm25_postings.iter().next().is_some(),
"bm25 staging alone must be able to force a commit"
);
drop(batch);
assert_eq!(LEDGER.in_flight(), 0);
}
#[test]
fn the_shared_ceiling_alone_forces_a_commit() {
static CEILINGED: StagedByteLedger = StagedByteLedger::new(4 * 1024);
let (_dir, db) = fresh_index();
let l1 = bulky_l1(64);
let mut worker = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &CEILINGED);
let mut committed_after = None;
for i in 0..(INDEX_COMMIT_BATCH - 1) {
assert!(worker.stage(&RelPath::from(format!("src/f{i}.rs")), &l1, None));
if CEILINGED.commits() > 0 {
committed_after = Some(i + 1);
break;
}
}
let files = committed_after.expect("a worker over the shared ceiling must commit");
assert!(
files < INDEX_COMMIT_BATCH,
"the file-count bound must not be what committed here"
);
assert!(
db.symbols_by_path.iter().next().is_some(),
"the ceiling commit must have reached disk"
);
worker.finish();
assert_eq!(CEILINGED.in_flight(), 0);
}
#[test]
fn the_ledger_does_not_drift_across_commits_or_a_worker_panic() {
static LEDGER: StagedByteLedger = StagedByteLedger::new(64 * 1024);
let (_dir, db) = fresh_index();
let l1 = bulky_l1(8);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), 4 * 1024, &LEDGER);
for i in 0..64u32 {
assert!(batch.stage(&RelPath::from(format!("src/f{i}.rs")), &l1, None));
assert_eq!(
LEDGER.in_flight(),
batch.reported_bytes,
"the sole worker's contribution must be exactly what the ledger holds"
);
assert!(
batch.reported_bytes < LEDGER.ceiling,
"staged bytes must stay under the shared ceiling"
);
}
batch.finish();
assert_eq!(
LEDGER.in_flight(),
0,
"the ledger must be empty once every batch committed"
);
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut batch = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &LEDGER);
assert!(batch.stage(&RelPath::from("src/pathological.rs"), &l1, None));
assert!(LEDGER.in_flight() > 0, "staging must charge the ledger");
panic!("pathological file");
}));
assert!(caught.is_err());
assert_eq!(LEDGER.in_flight(), 0, "a panicking file must not leak budget");
}
#[test]
fn a_failed_upsert_still_reports_the_bytes_it_staged() {
static LEDGER: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_dir, db) = fresh_index();
let l1 = bulky_l1(8);
let mut batch = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &LEDGER);
assert!(batch.stage(&RelPath::from("src/ok.rs"), &l1, None));
let after_ok = LEDGER.in_flight();
assert!(after_ok > 0, "the successful file must be on the ledger");
batch
.writer
.as_mut()
.expect("writer exists after a stage")
.fail_after_staging = true;
assert!(
!batch.stage(&RelPath::from("src/doomed.rs"), &l1, None),
"the injected failure must be reported to the caller"
);
let staged = batch
.writer
.as_ref()
.expect("no bound can have committed here")
.staged_bytes();
assert!(staged > after_ok, "the failed upsert did stage bytes before failing");
assert_eq!(
LEDGER.in_flight(),
staged,
"bytes staged by a failed upsert stay resident until the next commit, so they must be \
on the ledger and able to force one"
);
drop(batch);
assert_eq!(LEDGER.in_flight(), 0);
}
#[test]
fn a_pinned_ceiling_does_not_degenerate_into_a_commit_per_file() {
static PINNED: StagedByteLedger = StagedByteLedger::new(64 * 1024);
const FILES: u32 = 200;
let (_dir, db) = fresh_index();
let l1 = bulky_l1(32);
PINNED.charge(PINNED.ceiling);
let commits_before = PINNED.commits();
let mut worker = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &PINNED);
assert!(worker.stage(&RelPath::from("src/f0.rs"), &l1, None));
let per_file = worker.reported_bytes;
for i in 1..FILES {
assert!(worker.stage(&RelPath::from(format!("src/f{i}.rs")), &l1, None));
}
assert!(
(FILES as usize) < INDEX_COMMIT_BATCH,
"the file-count bound must not be what commits here"
);
worker.finish();
let commits = PINNED.commits() - commits_before;
let cadence_bound = per_file * u64::from(FILES) / CEILING_MIN_COMMIT_BYTES + 2;
assert!(
commits <= cadence_bound,
"a pinned ceiling must still commit on the {CEILING_MIN_COMMIT_BYTES}-byte floor: \
{commits} commits for {FILES} files ({per_file} B each), expected at most {cadence_bound}"
);
assert!(
commits * 20 <= u64::from(FILES),
"cadence collapsed to {commits} commits for {FILES} files — a floorless ceiling commits per file"
);
PINNED.release(PINNED.ceiling);
assert_eq!(PINNED.in_flight(), 0);
}
#[test]
fn batching_granularity_does_not_change_index_contents() {
static LEDGER: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_eager_dir, eager) = fresh_index();
let (_lazy_dir, lazy) = fresh_index();
let l1 = bulky_l1(6);
let rels: Vec<RelPath> = (0..12u32).map(|i| RelPath::from(format!("src/f{i}.rs"))).collect();
for (db, budget) in [(&eager, 1u64), (&lazy, u64::MAX)] {
let mut batch = WorkerIndexBatch::with_budget(Some(db), budget, &LEDGER);
for rel in &rels {
assert!(batch.stage(rel, &l1, None));
#[cfg(feature = "code-search")]
batch.stage_bm25(
rel,
&[crate::search::bm25::ChunkPosting {
chunk_id: format!("{rel}:0"),
doclen: 3,
terms: vec![("spawn".to_string(), 2), ("task".to_string(), 1)],
}],
);
}
batch.finish();
}
assert_eq!(dump_all(&eager), dump_all(&lazy));
assert_eq!(LEDGER.in_flight(), 0);
}
#[test]
fn grep_bloom_rows_are_budgeted_backfilled_and_not_rewritten_when_current() {
static LEDGER: StagedByteLedger = StagedByteLedger::new(u64::MAX);
let (_index_dir, db) = fresh_index();
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("a.rs"), "fn needle_function() {}\n".repeat(40)).expect("write");
let meta = std::fs::metadata(dir.path().join("a.rs")).expect("stat");
let (size, mtime) = (meta.len(), crate::scanner_file::mtime_nanos(&meta));
let rel = RelPath::from("a.rs");
let mut batch = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &LEDGER);
batch.backfill_grep_bloom(dir.path(), &rel, size, mtime);
assert!(
LEDGER.in_flight() > 0,
"the staged bloom row must count against the byte ledger"
);
batch.finish();
assert_eq!(LEDGER.in_flight(), 0);
assert!(db.grep_bloom_current(&rel, size, mtime), "backfill wrote the row");
let mut again = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &LEDGER);
again.backfill_grep_bloom(dir.path(), &rel, size, mtime);
again.restamp_grep_bloom(&rel, b"ignored", size, mtime);
assert_eq!(LEDGER.in_flight(), 0, "a current row is neither rebuilt nor restaged");
again.finish();
let mut moved = WorkerIndexBatch::with_budget(Some(&db), u64::MAX, &LEDGER);
moved.restamp_grep_bloom(&rel, b"fn needle_function() {}", size, mtime + 1);
assert!(LEDGER.in_flight() > 0, "a moved stamp re-stages the row");
moved.finish();
assert!(db.grep_bloom_current(&rel, size, mtime + 1));
}
}