use std::time::Instant;
use rusqlite::Connection;
use super::args::{EnrichArgs, EnrichMode, EnrichOperation};
use super::events::ConcurrencyEvent;
use super::queue::{
count_eligible_pending, enqueue_candidate, item_type_for, item_type_for_key, open_queue_db,
skipped_item_keys,
};
use super::scheduler;
use super::DEFAULT_RATE_LIMIT_WAIT;
use crate::errors::AppError;
use crate::output::emit_json_line as emit_json;
use crate::paths::AppPaths;
use crate::storage::connection::{ensure_db_ready, open_rw};
mod budget;
mod dry_run;
mod finalize;
mod guards;
mod provider;
mod queue_prep;
mod scan_phase;
pub fn run(
args: &EnrichArgs,
llm_backend: crate::cli::LlmBackendChoice,
embedding_backend: crate::cli::EmbeddingBackendChoice,
) -> Result<(), AppError> {
if guards::handle_pre_db_guards(args, llm_backend, embedding_backend)? {
return Ok(());
}
let started = Instant::now();
let paths = AppPaths::resolve(args.db.as_deref())?;
ensure_db_ready(&paths)?;
let conn = open_rw(&paths.db)?;
let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
let _singleton = crate::lock::acquire_job_singleton(
crate::lock::JobType::Enrich,
&namespace,
&paths.db,
args.wait_job_singleton,
args.force_job_singleton,
)?;
let provider_binary = provider::resolve_provider_binary(args)?;
provider::check_system_load(args)?;
provider::run_preflight(args)?;
let budget = budget::resolve(args);
if args.dry_run {
let scan = scan_phase::run_scan(&conn, &paths.db, &namespace, args, &budget)?;
dry_run::emit_preview(args, &scan.keys, started);
return Ok(());
}
let queue_path = crate::paths::sidecar_path(&paths.db, ".enrich-queue.sqlite");
let mut queue_conn = open_queue_db(&queue_path)?;
let op_label = format!("{:?}", args.operation());
queue_prep::prepare_queue(&queue_conn, &conn, args, &namespace, &op_label, &[])?;
let item_type = item_type_for(&args.operation());
let force_redescribe =
args.force_redescribe && matches!(args.operation(), EnrichOperation::EntityDescriptions);
let backlog_degree0_proxy = if budget.pair_scan_ops {
scan_phase::emit_scan_start(
&conn,
&namespace,
args,
&budget,
super::events::enrich_operation_cli_name(&args.operation()),
)
} else {
None
};
let scan_started = Instant::now();
let total = super::scan::scan_operation_for_each(&conn, &namespace, args, |page| {
if force_redescribe {
let _ = queue_prep::reopen_force_redescribe_page(&queue_conn, &namespace, &page);
}
queue_prep::enqueue_page(
&mut queue_conn,
&conn,
&namespace,
&page,
item_type,
&op_label,
)?;
Ok(())
})?;
let scan_elapsed_ms = scan_started.elapsed().as_millis() as u64;
emit_json(&super::events::PhaseEvent {
phase: "scan",
binary_path: None,
version: None,
items_total: Some(total),
items_pending: Some(total),
llm_parallelism: args.llm_parallelism,
});
if budget.pair_scan_ops {
let op_cli = super::events::enrich_operation_cli_name(&args.operation());
emit_json(&serde_json::json!({
"phase": "scan_meta",
"operation": op_cli,
"pair_algorithm": "cooccurrence+hub_island",
"items_total": total,
"pairs_enqueued_this_scan": total,
"backlog_degree0_proxy": backlog_degree0_proxy,
"scan_elapsed_ms": scan_elapsed_ms,
"scan_aborted_reason": serde_json::Value::Null,
}));
}
queue_prep::log_enqueue_result(&queue_conn, &op_label, &namespace, total);
let parallelism = super::events::resolve_drain_parallelism(args);
let mut counters = super::drain_parallel::DrainCounters::default();
let backoff_secs = DEFAULT_RATE_LIMIT_WAIT;
let rate_limit_deadline = Instant::now() + crate::runtime_config::rate_limit_deadline_secs();
let enrich_started = Instant::now();
let provider_timeout = match args.mode() {
EnrichMode::OpenRouter => args.openrouter_chat_timeout_secs(),
};
let provider_model: Option<&str> = match args.mode() {
EnrichMode::OpenRouter => args.openrouter_model.as_deref(),
};
let backoff_clause: &str = if args.ignore_backoff {
""
} else {
"AND (next_retry_at IS NULL OR next_retry_at <= datetime('now'))"
};
emit_json(&ConcurrencyEvent {
phase: "concurrency",
scan_parallelism: 1,
drain_parallelism: parallelism as u32,
});
let mut until_empty_iter: u32 = 0;
let yield_every = scheduler::resolve_yield_every_n(args.yield_every_n_items);
let mut yield_count: u64 = 0;
let mut items_since_yield: usize = 0;
let mut preempted_for_gate = false;
loop {
if args.until_empty {
until_empty_iter = until_empty_iter.saturating_add(1);
if until_empty_iter > 1 {
let mut rescan = super::events::scan_operation_with_deadline(
&conn,
&namespace,
args,
Some(budget.until_deadline),
)?;
if matches!(args.operation(), EnrichOperation::BodyEnrich) {
if let Ok(vetoed) = skipped_item_keys(&queue_conn, &op_label) {
rescan.retain(|k| !vetoed.contains(k));
}
}
{
let tx = queue_conn.transaction()?;
let tx_conn: &Connection = &tx;
for key in &rescan {
let it = item_type_for_key(key, item_type);
enqueue_candidate(tx_conn, &conn, &namespace, key, it, &op_label);
}
tx.commit()?;
}
}
}
let completed_before = counters.completed;
if parallelism > 1 {
super::drain_parallel::drain_parallel(
args,
&paths,
&queue_path,
&namespace,
provider_binary.as_deref(),
provider_model,
provider_timeout,
&op_label,
backoff_clause,
parallelism,
total,
llm_backend,
embedding_backend,
&mut counters,
)?;
} else {
super::drain_serial::drain_serial(
args,
&paths,
&conn,
&queue_conn,
&namespace,
provider_binary.as_deref(),
provider_model,
provider_timeout,
&op_label,
backoff_clause,
item_type,
total,
llm_backend,
embedding_backend,
yield_every,
&mut counters,
&mut items_since_yield,
&mut yield_count,
&mut preempted_for_gate,
enrich_started,
budget.until_deadline,
rate_limit_deadline,
backoff_secs,
)?;
}
if !args.until_empty {
break;
}
let eligible_remaining =
count_eligible_pending(&queue_conn, &op_label, &namespace, backoff_clause);
let progressed = counters.completed > completed_before;
if Instant::now() >= budget.until_deadline {
tracing::info!(target: "enrich", "until-empty: max-runtime reached, stopping");
break;
}
if !progressed && eligible_remaining == 0 {
tracing::info!(target: "enrich", "until-empty: converged (no eligible items remain)");
break;
}
if eligible_remaining == 0 {
std::thread::sleep(std::time::Duration::from_secs(
crate::constants::ENRICH_UNTIL_EMPTY_IDLE_NAP_SECS,
));
}
}
finalize::finish(
&conn,
&queue_conn,
&queue_path,
args,
&op_label,
&namespace,
finalize::FinalTally {
counters: &counters,
items_total: total,
started,
until_deadline: budget.until_deadline,
pair_scan_ops: budget.pair_scan_ops,
backlog_degree0_proxy,
yield_count,
preempted_for_gate,
},
);
Ok(())
}