Skip to main content

planarian_daemon/
snapshot.rs

1//! Read-only answers while the core is busy.
2//!
3//! The core handles one request at a time, and a generation change can hold
4//! it for many minutes (evaluate, build, activate, check). Before handling a
5//! request that may take that long, the daemon takes a snapshot of what
6//! `status`, `log`, `incidents` and the console would show; until the request
7//! finishes, those are answered from the snapshot, marked as such, instead of
8//! waiting behind it.
9
10use crate::core::{Core, Peer};
11use planarian_ipc::{ConsoleReport, LogLine, Request, Response};
12use planarian_schema::{Backends, Incident};
13use planarian_txn::store::Store;
14use std::time::Instant;
15
16/// Lines of the log kept for `planarian log` while busy.
17const LOG_LINES: usize = 1000;
18
19#[derive(Clone, Debug)]
20pub struct Snapshot {
21    console: ConsoleReport,
22    incidents: Vec<Incident>,
23    log: Vec<LogLine>,
24}
25
26/// What the core is doing instead of answering.
27#[derive(Clone, Debug)]
28pub struct Busy {
29    pub with: String,
30    pub since: Instant,
31}
32
33/// Requests that only read, and so may be answered from a snapshot.
34pub fn read_only(req: &Request) -> bool {
35    matches!(req, Request::Status | Request::Log { .. } | Request::Incidents | Request::Console)
36}
37
38/// A few words for "busy with …".
39pub fn describe(req: &Request) -> String {
40    match req {
41        Request::Apply { change, .. } => format!("the change {}", change.0),
42        Request::Ask { phrase, .. } => format!("\"{phrase}\""),
43        Request::Approve { .. } => "an approved change".into(),
44        Request::Undo => "undo".into(),
45        Request::Gc => "garbage collection".into(),
46        Request::Restart { unit } => format!("restarting {unit}"),
47        Request::Stop { unit } => format!("stopping {unit}"),
48        Request::MarkGood { .. } => "mark-good".into(),
49        Request::Resolve { txn, .. } => format!("resolving transaction {}", txn.0),
50        Request::Diagnose { unit, .. } => format!("diagnosing {unit}"),
51        _ => "a request".into(),
52    }
53}
54
55impl Snapshot {
56    /// One observation: the console report carries the status report.
57    pub fn take<B: Backends + Clone, S: Store>(core: &mut Core<B, S>) -> Option<Snapshot> {
58        let me = Peer::new(0, false);
59        let Response::Console(console) = core.handle(me.clone(), Request::Console) else { return None };
60        let Response::Incidents { incidents } = core.handle(me.clone(), Request::Incidents) else { return None };
61        let Response::Log { lines } = core.handle(me, Request::Log { limit: LOG_LINES }) else { return None };
62        Some(Snapshot { console: *console, incidents, log: lines })
63    }
64
65    pub fn answer(&self, req: &Request, busy: &Busy) -> Option<Response> {
66        let mins = busy.since.elapsed().as_secs() / 60;
67        let note = format!(
68            "busy with {} for {}: this is how things stood when it started",
69            busy.with,
70            if mins == 0 { "under a minute".to_string() } else { format!("{mins} min") }
71        );
72        Some(match req {
73            Request::Status => {
74                let mut s = self.console.status.clone();
75                s.notices.insert(0, note);
76                Response::Status(Box::new(s))
77            }
78            Request::Console => {
79                let mut c = self.console.clone();
80                c.status.notices.insert(0, note);
81                Response::Console(Box::new(c))
82            }
83            Request::Incidents => Response::Incidents { incidents: self.incidents.clone() },
84            Request::Log { limit } => {
85                let skip = self.log.len().saturating_sub(*limit);
86                Response::Log { lines: self.log[skip..].to_vec() }
87            }
88            _ => return None,
89        })
90    }
91}