use std::sync::Arc;
use crate::core::registry::{IndexHandle, IndexId};
use crate::service::persistence::PersistedIndex;
use crate::service::persistence_loader::build_indexer_from_entry;
use crate::service::warm_boot::{
canonicalize_best_effort, derive_warm_boot_stages, WarmBootInputs,
};
use crate::service::SearchAppState;
pub(crate) fn try_locate_moved_root(
entry: &PersistedIndex,
all_entries: &[PersistedIndex],
) -> Option<std::path::PathBuf> {
use crate::service::colocated_storage::COLOCATED_DIR_NAME;
use crate::service::fs_discovery::{scan_roots_for_colocated_indexes, DEFAULT_SCAN_DEPTH};
use crate::service::roots_registry::load_roots;
if !entry.colocated || entry.root_path.exists() {
return None;
}
let claimed: std::collections::HashSet<std::path::PathBuf> = all_entries
.iter()
.filter(|e| e.id != entry.id && e.root_path.exists())
.map(|e| e.root_path.clone())
.collect();
let tracked_roots: Vec<std::path::PathBuf> = match load_roots() {
Ok(r) => r.into_iter().map(|r| r.path).collect(),
Err(_) => return None,
};
if tracked_roots.is_empty() {
return None;
}
let discovered = scan_roots_for_colocated_indexes(&tracked_roots, DEFAULT_SCAN_DEPTH);
let candidates: Vec<std::path::PathBuf> = discovered
.into_iter()
.filter(|c| {
if claimed.contains(&c.root_path) {
return false;
}
let redb = c.root_path.join(COLOCATED_DIR_NAME).join("index.redb");
std::fs::metadata(&redb)
.map(|m| m.is_file() && m.len() > 0)
.unwrap_or(false)
})
.map(|c| c.root_path)
.collect();
match candidates.len() {
1 => {
let raw_root = candidates.into_iter().next().expect("len==1");
let new_root = canonicalize_best_effort(&raw_root);
tracing::info!(
"warm-boot: index '{}' root_path moved: {} → {} (auto-relink, issue #484)",
entry.id,
entry.root_path.display(),
new_root.display(),
);
let updated = PersistedIndex {
root_path: new_root.clone(),
..entry.clone()
};
if let Err(e) = crate::service::persistence::upsert_index_registry_entry(updated) {
tracing::warn!(
"warm-boot: could not persist relocated root_path for '{}': {e}",
entry.id
);
}
Some(new_root)
}
0 => {
tracing::warn!(
"warm-boot: skipping index '{}' — root_path {} no longer exists and no \
unique candidate found in tracked roots",
entry.id,
entry.root_path.display(),
);
None
}
n => {
tracing::warn!(
"warm-boot: skipping index '{}' — root_path {} no longer exists and {} \
ambiguous candidates found (manual `trusty-search index <path>` required)",
entry.id,
entry.root_path.display(),
n,
);
None
}
}
}
pub(crate) async fn restore_one_index(
state: &SearchAppState,
embedder: &Arc<dyn crate::core::Embedder>,
mut entry: PersistedIndex,
) {
let id = IndexId::new(entry.id.clone());
if state.registry.get(&id).is_some() {
return;
}
if !entry.root_path.exists() {
if entry.colocated {
let all_entries =
crate::service::persistence::load_index_registry().unwrap_or_default();
match try_locate_moved_root(&entry, &all_entries) {
Some(new_root) => {
entry.root_path = new_root;
}
None => {
return;
}
}
} else {
tracing::warn!(
"warm-boot: skipping index '{}' — root_path {} no longer exists \
(run `trusty-search prune-orphans` to clean up or \
`trusty-search index <path>` to re-register at the new location)",
entry.id,
entry.root_path.display(),
);
return;
}
}
let canonical_root = canonicalize_best_effort(&entry.root_path);
if canonical_root != entry.root_path {
tracing::info!(
"warm-boot: index '{}' root_path canonicalized: {} → {} (issue #541, persisting)",
entry.id,
entry.root_path.display(),
canonical_root.display(),
);
entry.root_path = canonical_root;
let updated = PersistedIndex {
root_path: entry.root_path.clone(),
..entry.clone()
};
if let Err(e) = crate::service::persistence::upsert_index_registry_entry(updated) {
tracing::warn!(
"warm-boot: could not persist canonicalized root_path for '{}': {e}",
entry.id,
);
}
}
let mut indexer = match build_indexer_from_entry(&entry, embedder).await {
Ok(idx) => idx,
Err(e) => {
tracing::error!(
"warm-boot: skipping index '{}' — HNSW allocator failed: {e} \
(closes #954; daemon will restart on next boot via systemd Restart=on-failure)",
entry.id
);
return;
}
};
let include_paths: Vec<std::path::PathBuf> = entry
.include_paths
.iter()
.filter(|p| !p.trim().is_empty() && p.trim() != ".")
.map(|p| entry.root_path.join(p.trim()))
.collect();
let extensions: Vec<String> = entry
.extensions
.iter()
.map(|e| e.trim_start_matches('.').to_string())
.filter(|e| !e.is_empty())
.collect();
indexer.set_domain_terms(entry.domain_terms.clone());
let indexed_head_sha = crate::core::git::head_sha(&entry.root_path);
let lexical_only = entry.lexical_only;
let skip_kg = entry.skip_kg;
let defer_embed = entry.defer_embed;
let corpus_open_failed = indexer.corpus_open_failed;
let chunk_count = indexer
.corpus_store()
.and_then(|c| c.chunk_count().ok())
.unwrap_or(0);
let hnsw_snapshot_ready = crate::service::persistence::hnsw_path_for_entry(&entry)
.map(|p| crate::service::persistence::has_persisted_hnsw(&p))
.unwrap_or(false);
let graph_node_count = indexer.snapshot_symbol_graph().await.node_count();
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count,
hnsw_snapshot_ready,
graph_node_count,
lexical_only,
skip_kg,
corpus_open_failed,
});
tracing::info!(
"warm-boot: index '{}' restored (colocated={}) — chunks={} hnsw_snapshot={} \
graph_nodes={} lexical_only={} skip_kg={} corpus_open_failed={} → \
stages(lexical={:?}, semantic={:?}, graph={:?})",
entry.id,
entry.colocated,
chunk_count,
hnsw_snapshot_ready,
graph_node_count,
lexical_only,
skip_kg,
corpus_open_failed,
stages.lexical.status,
stages.semantic.status,
stages.graph.status,
);
let handle = IndexHandle {
id: id.clone(),
indexer: Arc::new(tokio::sync::RwLock::new(indexer)),
root_path: entry.root_path,
include_paths,
exclude_globs: entry.exclude_globs,
extensions,
domain_terms: entry.domain_terms,
include_docs: entry.include_docs,
respect_gitignore: entry.respect_gitignore,
extra_skip_dirs: entry.extra_skip_dirs,
data_file_max_bytes: crate::service::persistence::resolve_data_file_max_bytes(
entry.data_file_max_bytes,
),
path_filter: entry.path_filter,
context_embedding: Arc::new(tokio::sync::RwLock::new(None)),
context_summary: Arc::new(tokio::sync::RwLock::new(None)),
indexed_head_sha: Arc::new(tokio::sync::RwLock::new(indexed_head_sha)),
last_indexed_at: Arc::new(tokio::sync::RwLock::new(None)),
lexical_only,
skip_kg,
defer_embed,
stages: Arc::new(tokio::sync::RwLock::new(stages)),
search_pressure: Arc::new(tokio::sync::Notify::new()),
walk_diagnostics: Arc::new(tokio::sync::RwLock::new(
crate::core::registry::WalkDiagnostics::default(),
)),
};
state.registry.register(handle);
}