Skip to main content

systemprompt_loader/bundle/
degraded.rs

1//! The on-disk record that a boot served a fallback services tree.
2//!
3//! A refresh that falls back to the last-good or the bundled tree reports it
4//! in the process that fell back, which is gone by the time an operator runs
5//! `cloud doctor`. The record outlives the process: it is written when a
6//! fallback is served and removed by the next composition that loads.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::fs;
12
13use serde::{Deserialize, Serialize};
14
15use super::cache::BundleCache;
16use super::error::{BundleError, BundleResult};
17use systemprompt_manifest::services::root::ServicesProvenance;
18
19const DEGRADED_FILE: &str = "degraded.json";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct DegradedRecord {
23    pub serving: String,
24    pub error: String,
25    pub recorded_at: String,
26}
27
28impl DegradedRecord {
29    #[must_use]
30    pub fn for_provenance(provenance: &ServicesProvenance) -> Option<Self> {
31        let (serving, error) = match provenance {
32            ServicesProvenance::LastGood { error, .. } => ("last_good", error),
33            ServicesProvenance::BundledFallback { error } => ("bundled", error),
34            ServicesProvenance::Bundled | ServicesProvenance::Fetched { .. } => return None,
35        };
36        Some(Self {
37            serving: serving.to_owned(),
38            error: error.clone(),
39            recorded_at: chrono::Utc::now().to_rfc3339(),
40        })
41    }
42}
43
44impl BundleCache {
45    #[must_use]
46    pub fn read_degraded(&self) -> Option<DegradedRecord> {
47        let raw = fs::read_to_string(self.root().join(DEGRADED_FILE)).ok()?;
48        serde_json::from_str(&raw).ok()
49    }
50
51    pub fn write_degraded(&self, record: &DegradedRecord) -> BundleResult<()> {
52        self.prepare()?;
53        let body = serde_json::to_vec_pretty(record)
54            .map_err(|e| BundleError::policy_context("degraded record is not serialisable", e))?;
55        let tmp = self
56            .root()
57            .join(format!("{DEGRADED_FILE}.tmp-{}", std::process::id()));
58        fs::write(&tmp, body)?;
59        fs::rename(&tmp, self.root().join(DEGRADED_FILE))?;
60        Ok(())
61    }
62
63    pub fn clear_degraded(&self) -> BundleResult<()> {
64        match fs::remove_file(self.root().join(DEGRADED_FILE)) {
65            Ok(()) => Ok(()),
66            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
67            Err(error) => Err(error.into()),
68        }
69    }
70}