use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use tokio::sync::Notify;
use tokio::time::Instant;
use crate::core::update_check::StalenessReport;
use crate::daemon::state::DaemonState;
pub(crate) const MAX_AGE: Duration = Duration::from_secs(30);
pub(crate) const REFRESH_WAIT: Duration = Duration::from_millis(100);
pub(crate) const STUCK_REFRESH_BOUND: Duration = Duration::from_secs(60);
#[derive(Debug, Default)]
struct Inner {
report: Option<StalenessReport>,
computed_at: Option<Instant>,
refreshing: bool,
refresh_started_at: Option<Instant>,
}
#[derive(Debug)]
pub(crate) struct CatalogReportCache {
inner: Mutex<Inner>,
refreshed: Notify,
max_age: Duration,
}
fn unknown_report() -> StalenessReport {
StalenessReport {
unknown: true,
..StalenessReport::default()
}
}
impl CatalogReportCache {
pub(crate) fn new(max_age: Duration) -> Self {
Self {
inner: Mutex::new(Inner::default()),
refreshed: Notify::new(),
max_age,
}
}
pub(crate) async fn get<F>(self: &Arc<Self>, compute: F, wait: Duration) -> StalenessReport
where
F: FnOnce() -> StalenessReport + Send + 'static,
{
let notified = self.refreshed.notified();
tokio::pin!(notified);
let previous = {
let mut inner = self.inner.lock();
if inner
.computed_at
.is_some_and(|at| at.elapsed() < self.max_age)
{
return inner.report.clone().unwrap_or_else(unknown_report);
}
if inner.refreshing
&& inner
.refresh_started_at
.is_some_and(|at| at.elapsed() >= STUCK_REFRESH_BOUND)
{
return unknown_report();
}
notified.as_mut().enable();
if !inner.refreshing {
self.start_refresh(&mut inner, compute);
}
inner.report.clone()
};
if tokio::time::timeout(wait, notified).await.is_ok()
&& let Some(report) = self.inner.lock().report.clone()
{
return report;
}
previous.unwrap_or_else(unknown_report)
}
fn start_refresh<F>(self: &Arc<Self>, inner: &mut Inner, compute: F)
where
F: FnOnce() -> StalenessReport + Send + 'static,
{
inner.refreshing = true;
inner.refresh_started_at = Some(Instant::now());
let cache = Arc::clone(self);
tokio::spawn(async move {
let report = match tokio::task::spawn_blocking(compute).await {
Ok(report) => report,
Err(e) => {
tracing::warn!(
"#7968: the catalog staleness walk did not complete ({e}); \
/health reports catalog_unknown until the next refresh"
);
unknown_report()
}
};
{
let mut inner = cache.inner.lock();
inner.report = Some(report);
inner.computed_at = Some(Instant::now());
inner.refreshing = false;
inner.refresh_started_at = None;
}
cache.refreshed.notify_waiters();
});
}
}
fn detector(state: &DaemonState) -> impl FnOnce() -> StalenessReport + Send + 'static {
let root = state.framework_root().to_path_buf();
move || {
let fw = crate::core::paths::FrameworkPaths::from_root(&root);
crate::core::update_check::detect_for_framework(&fw, &root)
}
}
pub(crate) async fn report(state: &Arc<DaemonState>) -> StalenessReport {
report_within(state, REFRESH_WAIT).await
}
pub(crate) async fn report_within(state: &Arc<DaemonState>, wait: Duration) -> StalenessReport {
state
.catalog_report_cache()
.get(detector(state), wait)
.await
}
#[cfg(test)]
#[path = "health_catalog_tests.rs"]
mod health_catalog_tests;