subc_daemon/
terminal_ring.rs1use std::{collections::VecDeque, sync::Arc};
2
3use crate::{
4 child_roster::DaemonShutdownFlag,
5 terminal_journal::{ring_history, JournalRead, TerminalJournal},
6};
7
8use subc_control::{TerminalDisposition, TerminalExitKind};
9
10const DEFAULT_MAX_ENTRIES: usize = 32;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct TerminalRingConfig {
15 max_entries: usize,
16}
17
18impl TerminalRingConfig {
19 pub const fn new(max_entries: usize) -> Self {
22 Self {
23 max_entries: if max_entries == 0 { 1 } else { max_entries },
24 }
25 }
26}
27
28impl Default for TerminalRingConfig {
29 fn default() -> Self {
30 Self::new(DEFAULT_MAX_ENTRIES)
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub struct TerminalRecord {
37 pub exit_code: Option<i32>,
38 pub exit_signal: Option<i32>,
39 pub at_ms: u64,
40 pub disposition: TerminalDisposition,
41 pub exit_kind: TerminalExitKind,
42 pub disposition_detail: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct TerminalHistorySnapshot {
53 pub daemon_started_at_ms: u64,
54 pub entries: Vec<TerminalRecord>,
55 pub dropped: u64,
56}
57
58pub(crate) enum DurableHistoryRead {
60 RingOnly(TerminalHistorySnapshot),
61 Journal(JournalRead),
62}
63
64impl DurableHistoryRead {
65 pub(crate) fn read(self, module_id: &str) -> subc_control::TerminalHistory {
67 match self {
68 Self::RingOnly(snapshot) => ring_history(snapshot, None),
69 Self::Journal(read) => read.read(module_id),
70 }
71 }
72}
73
74#[derive(Debug)]
79pub struct TerminalRing {
80 journal: Option<Arc<TerminalJournal>>,
81 daemon_shutdown: Option<DaemonShutdownFlag>,
82 config: TerminalRingConfig,
83 daemon_started_at_ms: u64,
84 start_clock: Option<crate::clock::StartClock>,
85 entries: VecDeque<TerminalRecord>,
86 dropped: u64,
87}
88
89impl TerminalRing {
90 pub fn new(config: TerminalRingConfig, daemon_started_at_ms: u64) -> Self {
91 Self {
92 journal: None,
93 daemon_shutdown: None,
94 config,
95 daemon_started_at_ms,
96 start_clock: None,
97 entries: VecDeque::new(),
98 dropped: 0,
99 }
100 }
101
102 pub(crate) fn with_start_clock(mut self, clock: crate::clock::StartClock) -> Self {
103 self.start_clock = Some(clock);
104 self
105 }
106
107 pub(crate) fn with_journal(mut self, journal: Option<Arc<TerminalJournal>>) -> Self {
108 self.journal = journal;
109 self
110 }
111
112 pub(crate) fn with_daemon_shutdown(mut self, flag: DaemonShutdownFlag) -> Self {
113 self.daemon_shutdown = Some(flag);
114 self
115 }
116
117 pub(crate) fn daemon_shutting_down(&self) -> bool {
119 self.daemon_shutdown
120 .as_ref()
121 .is_some_and(DaemonShutdownFlag::is_set)
122 }
123
124 pub(crate) fn record_exit(&mut self, module_id: &str, mut entry: TerminalRecord) {
132 if self.daemon_shutting_down() {
133 entry.disposition = TerminalDisposition::DaemonShutdown;
134 }
135 self.append_journal(module_id, &entry);
136 self.push(entry);
137 }
138
139 pub(crate) fn append_journal(&self, module_id: &str, entry: &TerminalRecord) {
140 if let Some(journal) = &self.journal {
141 journal.append(module_id, entry);
142 }
143 }
144
145 #[cfg(test)]
147 pub(crate) fn durable_history(&self, module_id: &str) -> subc_control::TerminalHistory {
148 self.capture_durable_history().read(module_id)
149 }
150
151 pub(crate) fn capture_durable_history(&self) -> DurableHistoryRead {
155 match &self.journal {
156 Some(journal) => DurableHistoryRead::Journal(journal.capture_read(self.snapshot())),
157 None => DurableHistoryRead::RingOnly(self.snapshot()),
158 }
159 }
160
161 pub fn push(&mut self, entry: TerminalRecord) {
162 self.entries.push_back(entry);
163 while self.entries.len() > self.config.max_entries {
164 self.entries.pop_front();
165 self.dropped = self.dropped.saturating_add(1);
166 }
167 }
168
169 pub fn snapshot(&self) -> TerminalHistorySnapshot {
170 TerminalHistorySnapshot {
171 daemon_started_at_ms: self
172 .start_clock
173 .map_or(self.daemon_started_at_ms, |clock| clock.started_at_ms()),
174 entries: self.entries.iter().cloned().collect(),
175 dropped: self.dropped,
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{TerminalRecord, TerminalRing, TerminalRingConfig};
183 use subc_control::{TerminalDisposition, TerminalExitKind};
184
185 fn record(at_ms: u64) -> TerminalRecord {
186 TerminalRecord {
187 exit_code: Some(1),
188 exit_signal: None,
189 at_ms,
190 disposition: TerminalDisposition::Restarting,
191 exit_kind: TerminalExitKind::Crash,
192 disposition_detail: None,
193 }
194 }
195
196 #[test]
197 fn the_ring_evicts_oldest_exits_and_counts_them() {
198 let mut ring = TerminalRing::new(TerminalRingConfig::new(2), 10);
199 ring.push(record(11));
200 ring.push(record(12));
201 ring.push(record(13));
202
203 let snapshot = ring.snapshot();
204 assert_eq!(snapshot.daemon_started_at_ms, 10);
205 assert_eq!(snapshot.dropped, 1);
206 assert_eq!(
207 snapshot
208 .entries
209 .iter()
210 .map(|entry| entry.at_ms)
211 .collect::<Vec<_>>(),
212 vec![12, 13]
213 );
214 }
215
216 #[test]
217 fn an_incoherent_zero_capacity_keeps_one_terminal() {
218 let mut ring = TerminalRing::new(TerminalRingConfig::new(0), 10);
219 ring.push(record(11));
220 ring.push(record(12));
221
222 let snapshot = ring.snapshot();
223 assert_eq!(snapshot.dropped, 1);
224 assert_eq!(snapshot.entries.len(), 1);
225 assert_eq!(snapshot.entries[0].at_ms, 12);
226 }
227}