use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use hermes_core::segment::BpBudget;
use log::{debug, info, warn};
use tokio::sync::Semaphore;
use crate::registry::IndexRegistry;
#[derive(Debug, Clone)]
pub struct OptimizerConfig {
pub threads: usize,
pub scan_interval: Duration,
pub large_segment_docs: u32,
pub time_budget: Duration,
pub partial_min_partition_docs: usize,
pub unconverged_cooldown: Duration,
}
pub fn spawn_optimizer(
registry: Arc<IndexRegistry>,
config: OptimizerConfig,
) -> Option<tokio::task::JoinHandle<()>> {
if config.threads == 0 {
return None;
}
info!(
"Starting background optimizer: {} threads, {:.0}s scan interval",
config.threads,
config.scan_interval.as_secs_f64(),
);
let semaphore = Arc::new(Semaphore::new(config.threads));
let bp_pool_threads = std::thread::available_parallelism()
.map(|c| c.get() / 2)
.unwrap_or(config.threads)
.max(config.threads);
let rayon_pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(bp_pool_threads)
.thread_name(|idx| format!("optimizer-bp-{}", idx))
.build()
.expect("failed to create optimizer rayon pool"),
);
Some(tokio::spawn(async move {
optimizer_loop(registry, semaphore, rayon_pool, config).await;
}))
}
async fn optimizer_loop(
registry: Arc<IndexRegistry>,
semaphore: Arc<Semaphore>,
rayon_pool: Arc<rayon::ThreadPool>,
config: OptimizerConfig,
) {
tokio::time::sleep(Duration::from_secs(5)).await;
let last_partial_pass = Arc::new(AtomicU64::new(0));
loop {
if let Err(e) = scan_and_optimize(
®istry,
&semaphore,
&rayon_pool,
&config,
&last_partial_pass,
)
.await
{
warn!("[optimizer] scan failed: {}", e);
}
tokio::time::sleep(config.scan_interval).await;
}
}
async fn scan_and_optimize(
registry: &IndexRegistry,
semaphore: &Arc<Semaphore>,
rayon_pool: &Arc<rayon::ThreadPool>,
config: &OptimizerConfig,
last_partial_pass: &Arc<AtomicU64>,
) -> Result<(), tonic::Status> {
let index_names = registry.list_indexes().await?;
for name in index_names {
let index = match registry.get_or_open_index(&name).await {
Ok(idx) => idx,
Err(e) => {
debug!("[optimizer] cannot open index '{}': {}", name, e);
continue;
}
};
let writer = match registry.get_writer(&name).await {
Ok(w) => w,
Err(e) => {
debug!("[optimizer] cannot get writer for '{}': {}", name, e);
continue;
}
};
let segment_manager = {
let w = writer.read().await;
Arc::clone(w.segment_manager())
};
match segment_manager.cleanup_orphan_segments().await {
Ok(0) => {}
Ok(n) => warn!(
"[optimizer] swept {} orphan segment(s) in '{}' (failed merge/reorder outputs)",
n, name
),
Err(e) => debug!("[optimizer] orphan sweep failed for '{}': {}", name, e),
}
if !index.schema().has_reorder_fields() {
continue;
}
let fresh = segment_manager.unreordered_segments().await;
let mut candidates: Vec<(String, u32, bool)> = fresh
.into_iter()
.map(|(id, docs)| (id, docs, false))
.collect();
let unconverged = segment_manager.unconverged_segments().await;
if !unconverged.is_empty() {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let last = last_partial_pass.load(Ordering::Relaxed);
if now_ms.saturating_sub(last) >= config.unconverged_cooldown.as_millis() as u64 {
last_partial_pass.store(now_ms, Ordering::Relaxed);
if let Some((id, docs)) = unconverged.into_iter().next() {
debug!(
"[optimizer] index '{}': deepening unconverged segment {} ({} docs)",
name, id, docs,
);
candidates.push((id, docs, true));
}
}
}
if candidates.is_empty() {
continue;
}
debug!(
"[optimizer] index '{}': {} reorder candidate(s)",
name,
candidates.len()
);
for (seg_id, num_docs, is_deepening) in candidates {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => {
return Ok(());
}
};
let sm = Arc::clone(&segment_manager);
let idx_name = name.clone();
let sid = seg_id.clone();
let pool = Arc::clone(rayon_pool);
let budget = if is_deepening {
info!(
"[optimizer] deepening segment {} ({} docs): full depth, time budget {:.0}s",
sid,
num_docs,
config.time_budget.as_secs_f64(),
);
BpBudget {
min_partition_docs: None,
time_budget: Some(config.time_budget),
}
} else if num_docs >= config.large_segment_docs {
info!(
"[optimizer] segment {} ({} docs) exceeds {} docs — budgeted BP pass \
(min_partition={} docs, time budget {:.0}s)",
sid,
num_docs,
config.large_segment_docs,
config.partial_min_partition_docs,
config.time_budget.as_secs_f64(),
);
BpBudget {
min_partition_docs: Some(config.partial_min_partition_docs),
time_budget: Some(config.time_budget),
}
} else {
BpBudget::full()
};
tokio::spawn(async move {
let _permit = permit;
let start = std::time::Instant::now();
match sm.reorder_single_segment(&sid, Some(pool), budget).await {
Ok(true) => {
info!(
"[optimizer] reordered segment {} in index '{}' ({:.1}s)",
sid,
idx_name,
start.elapsed().as_secs_f64(),
);
}
Ok(false) => {
debug!(
"[optimizer] segment {} in index '{}' skipped (in merge)",
sid, idx_name
);
}
Err(e) => {
warn!(
"[optimizer] failed to reorder segment {} in index '{}': {}",
sid, idx_name, e
);
}
}
});
}
}
Ok(())
}