pub(crate) fn prior_index_count_path() -> Option<std::path::PathBuf> {
crate::service::daemon::daemon_lock_path()
.ok()
.and_then(|p| p.parent().map(|d| d.join("prior_index_count.txt")))
}
pub(crate) fn save_prior_index_count(count: usize) {
let Some(path) = prior_index_count_path() else {
return;
};
let content = format!("{count}\n");
if let Err(e) = std::fs::write(&path, content) {
tracing::debug!(
"warm-boot: could not save prior index count to {}: {e}",
path.display()
);
}
}
pub(crate) fn load_prior_index_count() -> usize {
let Some(path) = prior_index_count_path() else {
return 0;
};
std::fs::read_to_string(&path)
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or(0)
}
pub(crate) fn record_warm_boot_result(
state: &crate::service::SearchAppState,
total: usize,
total_skipped_tcc: usize,
total_skipped_timeout: usize,
indexes_lazy: usize,
) {
let prior_count = state
.prior_index_count
.load(std::sync::atomic::Ordering::Relaxed);
let degraded_by_tcc = total_skipped_tcc > 0;
let degraded_by_timeout = total_skipped_timeout > 0;
let degraded_by_count = prior_count > 0 && total < prior_count * 4 / 5;
let warm_boot_degraded = degraded_by_tcc || degraded_by_timeout || degraded_by_count;
if let Ok(mut summary) = state.warmboot_summary.lock() {
*summary = crate::service::server::WarmBootSummary {
indexes_loaded: total,
indexes_skipped_tcc: total_skipped_tcc,
indexes_skipped_timeout: total_skipped_timeout,
warm_boot_degraded,
indexes_lazy,
indexes_failed: 0,
};
}
if degraded_by_timeout {
tracing::error!(
loaded = total,
skipped_timeout = total_skipped_timeout,
"warm-boot DEGRADED: {total_skipped_timeout} index(es) TIMED OUT during restore \
and were NOT loaded. These indexes are missing from search results and /health. \
The skipped indexes may have been on a slow or temporarily inaccessible filesystem. \
Increase TRUSTY_WARMBOOT_INDEX_TIMEOUT_SECS (default 10s) if the filesystem is \
legitimately slow, or investigate the per-index warn! logs above for root causes. \
(issue #1091)"
);
}
if degraded_by_count {
tracing::error!(
loaded = total,
prior = prior_count,
skipped_tcc = total_skipped_tcc,
"warm-boot DEGRADED: only {total}/{prior_count} indexes loaded (< 80% of prior). \
If you just ran `cargo install trusty-search`, macOS TCC likely revoked \
Full Disk Access because the new binary has a different cdhash. \
ACTION REQUIRED: re-grant Full Disk Access in \
System Settings → Privacy & Security → Full Disk Access → \
remove and re-add ~/.cargo/bin/trusty-search. \
This is NOT data loss — all on-disk indexes are intact. (issue #873)"
);
}
if total > 0 {
save_prior_index_count(total);
state
.prior_index_count
.store(total, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::registry::IndexRegistry;
use crate::service::SearchAppState;
fn make_state() -> SearchAppState {
SearchAppState::new(IndexRegistry::new())
}
#[test]
fn warmboot_summary_timeout_sets_degraded_flag() {
let state = make_state();
record_warm_boot_result(&state, 5, 0, 1, 0);
let summary = state.warmboot_summary.lock().unwrap().clone();
assert_eq!(summary.indexes_loaded, 5, "loaded count must be recorded");
assert_eq!(
summary.indexes_skipped_timeout, 1,
"timeout count must be recorded"
);
assert_eq!(summary.indexes_skipped_tcc, 0, "tcc count must be 0");
assert!(
summary.warm_boot_degraded,
"warm_boot_degraded must be true when at least one index timed out (issue #1091)"
);
}
#[test]
fn warmboot_summary_tcc_skip_sets_degraded_flag() {
let state = make_state();
record_warm_boot_result(&state, 5, 1, 0, 0);
let summary = state.warmboot_summary.lock().unwrap().clone();
assert!(
summary.warm_boot_degraded,
"warm_boot_degraded must be true when TCC skips occurred"
);
}
#[test]
fn warmboot_summary_clean_boot_not_degraded() {
let state = make_state();
state
.prior_index_count
.store(5, std::sync::atomic::Ordering::Relaxed);
record_warm_boot_result(&state, 5, 0, 0, 0);
let summary = state.warmboot_summary.lock().unwrap().clone();
assert!(
!summary.warm_boot_degraded,
"warm_boot_degraded must be false on a clean boot with no skips"
);
}
#[test]
fn warmboot_summary_count_drop_sets_degraded_flag() {
let state = make_state();
state
.prior_index_count
.store(10, std::sync::atomic::Ordering::Relaxed);
record_warm_boot_result(&state, 7, 0, 0, 0);
let summary = state.warmboot_summary.lock().unwrap().clone();
assert!(
summary.warm_boot_degraded,
"warm_boot_degraded must be true when loaded < 80% of prior count"
);
}
}