use crate::backend::FtsBackend;
use crate::index::FtsIndexError;
use crate::scope::IndexScope;
use super::merge;
use super::query::LiveSegment;
use super::segment::{reader::SegmentReader, writer};
use super::segment_deletes::SegmentDeletes;
use std::sync::Arc;
use nodedb_mem::MemoryGovernor;
use crate::mem_scope::fts_scope;
#[derive(Debug, Clone, Copy)]
pub struct CompactionConfig {
pub max_levels: usize,
pub max_segments_per_level: usize,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
max_levels: 8,
max_segments_per_level: 8,
}
}
}
#[derive(Debug, Clone)]
pub struct SegmentMeta {
pub segment_id: String,
pub level: u32,
pub size: u64,
}
pub fn needs_compaction(segments: &[SegmentMeta], config: &CompactionConfig) -> Option<u32> {
let mut counts = vec![0usize; config.max_levels];
for seg in segments {
let level = seg.level as usize;
if level < config.max_levels {
counts[level] += 1;
}
}
for (level, &count) in counts.iter().enumerate() {
if count >= config.max_segments_per_level && level + 1 < config.max_levels {
return Some(level as u32);
}
}
None
}
pub type CompactionResult = (Vec<u8>, Vec<String>);
#[derive(Debug)]
pub enum CompactError<E: std::fmt::Display> {
Backend(E),
Budget(nodedb_mem::MemError),
Index(FtsIndexError<E>),
}
impl<E: std::fmt::Display> std::fmt::Display for CompactError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompactError::Backend(e) => write!(f, "compaction backend error: {e}"),
CompactError::Budget(e) => write!(f, "compaction budget exhausted: {e}"),
CompactError::Index(e) => write!(f, "compaction index error: {e}"),
}
}
}
impl<E: std::fmt::Display> From<FtsIndexError<E>> for CompactError<E> {
fn from(e: FtsIndexError<E>) -> Self {
match e {
FtsIndexError::Backend(inner) => CompactError::Backend(inner),
FtsIndexError::BudgetExhausted(inner) => CompactError::Budget(inner),
other => CompactError::Index(other),
}
}
}
pub struct CompactLevelParams<'a, B: FtsBackend> {
pub backend: &'a B,
pub database_id: u64,
pub tid: u64,
pub index: IndexScope<'a>,
pub segments: &'a [SegmentMeta],
pub level: u32,
pub governor: &'a Arc<MemoryGovernor>,
}
pub fn compact_level<B: FtsBackend>(
params: CompactLevelParams<'_, B>,
) -> Result<Option<CompactionResult>, CompactError<B::Error>> {
let CompactLevelParams {
backend,
database_id,
tid,
index,
segments,
level,
governor,
} = params;
let to_merge: Vec<&SegmentMeta> = segments.iter().filter(|s| s.level == level).collect();
if to_merge.len() < 2 {
return Ok(None);
}
let memory = fts_scope(governor, database_id, tid);
let _readers_guard = memory
.reserve(to_merge.len() * size_of::<LiveSegment>())
.map_err(CompactError::Budget)?;
let mut sources = Vec::with_capacity(to_merge.len());
let _ids_guard = memory
.reserve(to_merge.len() * size_of::<String>())
.map_err(CompactError::Budget)?;
let mut merged_ids = Vec::with_capacity(to_merge.len());
let merge_ids: Vec<String> = to_merge.iter().map(|m| m.segment_id.clone()).collect();
let mut deletes = SegmentDeletes::load(backend, database_id, tid, index, &merge_ids)?;
for segment_id in merge_ids {
let Some(data) = backend
.read_segment(database_id, tid, index, &segment_id)
.map_err(CompactError::Backend)?
else {
return Err(CompactError::Index(FtsIndexError::MissingSegment {
segment_id,
}));
};
let reader = SegmentReader::open(data).map_err(|source| {
CompactError::Index(FtsIndexError::CorruptSegment {
segment_id: segment_id.clone(),
source,
})
})?;
let deleted = deletes.take(&segment_id);
merged_ids.push(segment_id.clone());
sources.push(LiveSegment {
segment_id,
reader,
deleted,
});
}
let merged_term_blocks = merge::merge_segments::<B::Error>(&sources, &memory)?;
let new_segment = writer::build_from_blocks(&merged_term_blocks)
.map_err(|e| CompactError::Index(FtsIndexError::from(e)))?;
Ok(Some((new_segment, merged_ids)))
}
pub fn segment_id(segment_id: u64, level: u32) -> String {
format!("L{level}:{segment_id:016x}")
}
pub fn parse_level(id: &str) -> u32 {
id.strip_prefix('L')
.and_then(|s| s.split(':').next())
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
pub fn parse_segment_number(id: &str) -> u64 {
id.rsplit(':')
.next()
.and_then(|s| u64::from_str_radix(s, 16).ok())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_meta(id: &str, level: u32) -> SegmentMeta {
SegmentMeta {
segment_id: id.to_string(),
level,
size: 1000,
}
}
#[test]
fn needs_compaction_basic() {
let config = CompactionConfig {
max_levels: 4,
max_segments_per_level: 3,
};
let segments = vec![make_meta("s1", 0), make_meta("s2", 0), make_meta("s3", 0)];
assert_eq!(needs_compaction(&segments, &config), Some(0));
}
#[test]
fn no_compaction_under_threshold() {
let config = CompactionConfig {
max_levels: 4,
max_segments_per_level: 3,
};
let segments = vec![make_meta("s1", 0), make_meta("s2", 0)];
assert_eq!(needs_compaction(&segments, &config), None);
}
#[test]
fn compaction_at_higher_level() {
let config = CompactionConfig {
max_levels: 4,
max_segments_per_level: 2,
};
let segments = vec![make_meta("s1", 0), make_meta("s2", 1), make_meta("s3", 1)];
assert_eq!(needs_compaction(&segments, &config), Some(1));
}
#[test]
fn segment_id_format() {
let id = segment_id(42, 0);
assert!(id.starts_with("L0:"));
assert_eq!(parse_level(&id), 0);
assert_eq!(parse_segment_number(&id), 42);
}
#[test]
fn parse_level_and_id() {
assert_eq!(parse_level("L2:000000000000002a"), 2);
assert_eq!(parse_segment_number("L2:000000000000002a"), 42);
}
}