Skip to content

Commit c87f85e

Browse files
committed
feat(fts): split compaction into dedicated module and add crash-safe LSM merge
The monolithic compact.rs handler and inverted.rs grew beyond the 500-line limit and mixed unrelated concerns. Both are replaced with proper directory modules: - handlers/compact/ — budget tracking, FTS merge logic, maintenance orchestration, segment-level helpers, stats, and runner are now separate files. The module covers vector, CSR, and FTS compaction in a single coordinated handler so the maintenance cycle has one entry point. - engine/sparse/inverted/ — InvertedIndex is split into core, indexing, search, compaction, synonyms, and error sub-modules following the one-concern-per-file rule. Two new operations are added to the redb FTS backend: - compact_commit: writes the merged segment and removes all source segments inside a single redb write transaction. A crash mid-compaction leaves the original segments intact; the maintenance cycle retries on the next pass. Double-counted postings from a partial commit are no longer possible. - list_all_fts_collections: scans the SEGMENTS table to enumerate every (tenant, collection) pair that has at least one segment, so maintenance can discover compaction candidates without a separate registry. The remove_segment path propagates the redb error instead of silently swallowing Ok(None); a corrupt index or aborted transaction now surfaces at the call site. Two executor accessors are added so integration tests and bootstrapping code can prime segments and resolve tenant→database mappings without going through the normal indexing path: - CoreLoop::fts_write_segment — writes a raw segment blob directly into the LSM segment store, bypassing the memtable flush path. - CoreLoop::fts_list_segments — returns the segment IDs for a collection. - ExecutorMaintenance::set_tenant_database — primes the tenant→database map so the maintenance budget tracker can resolve per-database CPU caps for tenants that have not yet been seen in a request. The new fts_compaction_budget integration test verifies that the per-database maintenance CPU budget gates FTS LSM compaction: a database under its cap has segments merged, while a database with its budget pre-exhausted defers compaction without touching its segments.
1 parent ac2d047 commit c87f85e

23 files changed

Lines changed: 2228 additions & 1298 deletions

File tree

nodedb/src/data/executor/core_loop/accessors.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,40 @@ impl CoreLoop {
163163
})
164164
.unwrap_or(0)
165165
}
166+
167+
/// Write a raw segment blob directly into the FTS LSM segment store for
168+
/// a given `(tenant, collection)`.
169+
///
170+
/// This bypasses the memtable flush path and is intended for maintenance
171+
/// tests and bootstrapping code that need to pre-populate a known number
172+
/// of L0 segments without indexing documents. In production code,
173+
/// segments are written automatically when the FTS memtable crosses its
174+
/// flush threshold.
175+
pub fn fts_write_segment(
176+
&self,
177+
tenant: crate::types::TenantId,
178+
collection: &str,
179+
segment_id: &str,
180+
data: &[u8],
181+
) -> crate::Result<()> {
182+
use nodedb_fts::backend::FtsBackend;
183+
self.inverted
184+
.backend()
185+
.write_segment(tenant.as_u64(), collection, segment_id, data)
186+
}
187+
188+
/// Return the list of FTS LSM segment IDs for a `(tenant, collection)`.
189+
///
190+
/// Used by maintenance tests to verify that compaction reduced the
191+
/// segment count at a given level.
192+
pub fn fts_list_segments(
193+
&self,
194+
tenant: crate::types::TenantId,
195+
collection: &str,
196+
) -> crate::Result<Vec<String>> {
197+
use nodedb_fts::backend::FtsBackend;
198+
self.inverted
199+
.backend()
200+
.list_segments(tenant.as_u64(), collection)
201+
}
166202
}

nodedb/src/data/executor/core_loop/maintenance.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ impl CoreLoop {
5252
self.tenant_database_map.entry(tenant).or_insert(db);
5353
}
5454

55+
/// Register a `TenantId → DatabaseId` mapping.
56+
///
57+
/// In production the mapping is populated lazily from request headers.
58+
/// This method allows tests and bootstrapping code to prime the mapping
59+
/// explicitly so the maintenance budget tracker can resolve per-database
60+
/// caps for tenants that have not yet processed any requests.
61+
pub fn set_tenant_database(
62+
&mut self,
63+
tenant: crate::types::TenantId,
64+
db: nodedb_types::DatabaseId,
65+
) {
66+
self.tenant_database_map.insert(tenant, db);
67+
}
68+
5569
/// Resolve the `DatabaseId` for a `TenantId`. Falls back to
5670
/// `DatabaseId::DEFAULT` when the mapping has not been seen yet.
5771
pub(in crate::data::executor) fn database_for_tenant(

0 commit comments

Comments
 (0)