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 rayon_pool = Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(config.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;
}
};
if !index.schema().has_reorder_fields() {
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())
};
let mut candidates = segment_manager.unreordered_segments().await;
if candidates.is_empty() {
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);
candidates = unconverged.into_iter().take(1).collect();
debug!(
"[optimizer] index '{}': deepening 1 unconverged segment",
name,
);
}
}
}
if candidates.is_empty() {
continue;
}
debug!(
"[optimizer] index '{}': {} reorder candidate(s)",
name,
candidates.len()
);
for (seg_id, num_docs) 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 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(())
}