use super::types::{BackendStatus, CacheHealth, HealthResponse, StorageHealth, SyncHealth};
use crate::monitoring::MonitoringSystem;
use do_memory_core::SelfLearningMemory;
use do_memory_core::storage::StorageBackend;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
pub const PROBE_TIMEOUT: Duration = Duration::from_millis(1_500);
pub const SYNC_NOT_CONFIGURED: &str = "no synchronizer configured";
pub async fn probe_outcome<F, Fut>(configured: bool, probe: F, budget: Duration) -> BackendStatus
where
F: FnOnce() -> Fut,
Fut: Future<Output = do_memory_core::Result<()>>,
{
if !configured {
return BackendStatus::NotConfigured;
}
match tokio::time::timeout(budget, probe()).await {
Ok(Ok(())) => BackendStatus::Healthy,
Ok(Err(_)) => BackendStatus::Unavailable,
Err(_) => BackendStatus::Degraded,
}
}
pub async fn probe_backend(
backend: Option<&Arc<dyn StorageBackend>>,
budget: Duration,
) -> BackendStatus {
match backend {
None => BackendStatus::NotConfigured,
Some(backend) => probe_outcome(true, || backend.health_check(), budget).await,
}
}
#[must_use]
pub fn overall_status(turso: BackendStatus, redb: BackendStatus) -> &'static str {
let configured = [turso, redb]
.into_iter()
.filter(|status| *status != BackendStatus::NotConfigured)
.collect::<Vec<_>>();
if configured.is_empty() {
return "degraded";
}
if configured
.iter()
.all(|status| matches!(status, BackendStatus::Healthy))
{
"healthy"
} else if configured
.iter()
.any(|status| matches!(status, BackendStatus::Healthy))
{
"degraded"
} else {
"unhealthy"
}
}
pub async fn build_health_response(
memory: &SelfLearningMemory,
monitoring: &MonitoringSystem,
budget: Duration,
) -> HealthResponse {
let (turso_status, redb_status) = tokio::join!(
probe_backend(memory.turso_storage(), budget),
probe_backend(memory.cache_storage(), budget)
);
let cache = memory.get_cache_metrics();
monitoring.update_uptime();
HealthResponse {
status: overall_status(turso_status, redb_status).to_string(),
storage: StorageHealth {
turso_connected: turso_status.connected(),
turso_status,
turso_details: Some(turso_status.detail().to_string()),
redb_connected: redb_status.connected(),
redb_status,
redb_details: Some(redb_status.detail().to_string()),
},
cache: CacheHealth {
enabled: cache.capacity > 0,
hits: cache.hits,
misses: cache.misses,
hit_rate: cache.hit_rate() * 100.0,
size: cache.size,
max_size: cache.capacity,
},
sync: SyncHealth {
last_sync_timestamp: None,
status: SYNC_NOT_CONFIGURED.to_string(),
seconds_since_sync: None,
},
uptime_seconds: monitoring.get_stats().uptime_seconds,
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn probe_ok() -> do_memory_core::Result<()> {
Ok(())
}
async fn probe_err() -> do_memory_core::Result<()> {
Err(do_memory_core::Error::Storage(
"driver said: see url".to_string(),
))
}
#[tokio::test]
async fn unconfigured_backend_is_reported_as_such() {
let status = probe_outcome(false, probe_ok, Duration::from_millis(50)).await;
assert_eq!(status, BackendStatus::NotConfigured);
assert!(!status.connected());
}
#[tokio::test]
async fn answered_probe_is_healthy() {
let status = probe_outcome(true, probe_ok, Duration::from_millis(50)).await;
assert_eq!(status, BackendStatus::Healthy);
assert!(status.connected());
}
#[tokio::test]
async fn failed_probe_is_unavailable_not_connected() {
let status = probe_outcome(true, probe_err, Duration::from_millis(50)).await;
assert_eq!(status, BackendStatus::Unavailable);
assert!(
!status.connected(),
"a failed probe must not be reported as a connection"
);
}
#[tokio::test]
async fn slow_probe_times_out_as_degraded() {
let started = std::time::Instant::now();
let status = probe_outcome(
true,
|| async {
tokio::time::sleep(Duration::from_secs(30)).await;
Ok(())
},
Duration::from_millis(60),
)
.await;
assert_eq!(status, BackendStatus::Degraded);
assert!(
started.elapsed() < Duration::from_secs(5),
"the probe ran {:.0?} instead of returning at its deadline",
started.elapsed()
);
}
#[tokio::test]
async fn detail_strings_never_quote_the_backend_error() {
for status in [
BackendStatus::Healthy,
BackendStatus::Degraded,
BackendStatus::Unavailable,
BackendStatus::NotConfigured,
] {
let detail = status.detail();
assert!(
!detail.contains("driver") && !detail.contains("url"),
"status {status:?} leaked a backend detail: {detail}"
);
assert!(!detail.is_empty(), "status {status:?} has no summary");
}
assert!(
probe_err().await.is_err(),
"the fixture must fail like a probe"
);
}
#[test]
fn overall_status_requires_every_backend() {
assert_eq!(
overall_status(BackendStatus::Healthy, BackendStatus::Healthy),
"healthy"
);
assert_eq!(
overall_status(BackendStatus::Healthy, BackendStatus::Unavailable),
"degraded"
);
assert_eq!(
overall_status(BackendStatus::Degraded, BackendStatus::Degraded),
"unhealthy"
);
assert_eq!(
overall_status(BackendStatus::Healthy, BackendStatus::NotConfigured),
"healthy"
);
assert_eq!(
overall_status(BackendStatus::NotConfigured, BackendStatus::NotConfigured),
"degraded"
);
}
#[tokio::test]
async fn response_reports_live_cache_counters_and_uptime() {
let memory = SelfLearningMemory::new();
let monitoring = MonitoringSystem::new(crate::monitoring::MonitoringConfig::default());
let metrics = memory.get_cache_metrics();
let response = build_health_response(&memory, &monitoring, PROBE_TIMEOUT).await;
assert_eq!(
(response.cache.hits, response.cache.misses),
(metrics.hits, metrics.misses),
"the cache section is not bound to the live retrieval cache"
);
assert_eq!(response.cache.size, metrics.size);
assert_eq!(
response.cache.max_size, metrics.capacity,
"the reported capacity is not the live cache's"
);
assert!(
response.cache.max_size > 0,
"a live retrieval cache always advertises its capacity"
);
assert_eq!(response.cache.enabled, metrics.capacity > 0);
assert_eq!(
response.cache.hit_rate,
metrics.hit_rate() * 100.0,
"hit rate must be the live percentage"
);
assert_eq!(response.sync.status, SYNC_NOT_CONFIGURED);
assert!(
response.sync.last_sync_timestamp.is_none()
&& response.sync.seconds_since_sync.is_none(),
"a server without a synchronizer must not invent sync times: {:?}",
response.sync
);
assert_eq!(
response.uptime_seconds,
monitoring.get_stats().uptime_seconds
);
assert_eq!(
response.status, "degraded",
"a server with no configured backend cannot claim to be healthy"
);
assert_eq!(
response.storage.turso_status,
BackendStatus::NotConfigured,
"no durable backend was configured, so the probe must not claim one"
);
}
}