subc_daemon/
terminal_ring.rs1use std::{collections::VecDeque, sync::Arc};
2
3use crate::terminal_journal::{ring_history, TerminalJournal};
4
5use subc_control::{TerminalDisposition, TerminalExitKind};
6
7const DEFAULT_MAX_ENTRIES: usize = 32;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct TerminalRingConfig {
12 max_entries: usize,
13}
14
15impl TerminalRingConfig {
16 pub const fn new(max_entries: usize) -> Self {
19 Self {
20 max_entries: if max_entries == 0 { 1 } else { max_entries },
21 }
22 }
23}
24
25impl Default for TerminalRingConfig {
26 fn default() -> Self {
27 Self::new(DEFAULT_MAX_ENTRIES)
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33pub struct TerminalRecord {
34 pub exit_code: Option<i32>,
35 pub exit_signal: Option<i32>,
36 pub at_ms: u64,
37 pub disposition: TerminalDisposition,
38 pub exit_kind: TerminalExitKind,
39 pub disposition_detail: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct TerminalHistorySnapshot {
50 pub daemon_started_at_ms: u64,
51 pub entries: Vec<TerminalRecord>,
52 pub dropped: u64,
53}
54
55#[derive(Debug)]
60pub struct TerminalRing {
61 journal: Option<Arc<TerminalJournal>>,
62 config: TerminalRingConfig,
63 daemon_started_at_ms: u64,
64 start_clock: Option<crate::clock::StartClock>,
65 entries: VecDeque<TerminalRecord>,
66 dropped: u64,
67}
68
69impl TerminalRing {
70 pub fn new(config: TerminalRingConfig, daemon_started_at_ms: u64) -> Self {
71 Self {
72 journal: None,
73 config,
74 daemon_started_at_ms,
75 start_clock: None,
76 entries: VecDeque::new(),
77 dropped: 0,
78 }
79 }
80
81 pub(crate) fn with_start_clock(mut self, clock: crate::clock::StartClock) -> Self {
82 self.start_clock = Some(clock);
83 self
84 }
85
86 pub(crate) fn with_journal(mut self, journal: Option<Arc<TerminalJournal>>) -> Self {
87 self.journal = journal;
88 self
89 }
90
91 pub(crate) fn append_journal(&self, module_id: &str, entry: &TerminalRecord) {
92 if let Some(journal) = &self.journal {
93 journal.append(module_id, entry);
94 }
95 }
96
97 pub(crate) fn durable_history(&self, module_id: &str) -> subc_control::TerminalHistory {
98 match &self.journal {
99 Some(journal) => journal.merge(module_id, self.snapshot()),
100 None => ring_history(self.snapshot(), None),
101 }
102 }
103
104 pub fn push(&mut self, entry: TerminalRecord) {
105 self.entries.push_back(entry);
106 while self.entries.len() > self.config.max_entries {
107 self.entries.pop_front();
108 self.dropped = self.dropped.saturating_add(1);
109 }
110 }
111
112 pub fn snapshot(&self) -> TerminalHistorySnapshot {
113 TerminalHistorySnapshot {
114 daemon_started_at_ms: self
115 .start_clock
116 .map_or(self.daemon_started_at_ms, |clock| clock.started_at_ms()),
117 entries: self.entries.iter().cloned().collect(),
118 dropped: self.dropped,
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::{TerminalRecord, TerminalRing, TerminalRingConfig};
126 use subc_control::{TerminalDisposition, TerminalExitKind};
127
128 fn record(at_ms: u64) -> TerminalRecord {
129 TerminalRecord {
130 exit_code: Some(1),
131 exit_signal: None,
132 at_ms,
133 disposition: TerminalDisposition::Restarting,
134 exit_kind: TerminalExitKind::Crash,
135 disposition_detail: None,
136 }
137 }
138
139 #[test]
140 fn the_ring_evicts_oldest_exits_and_counts_them() {
141 let mut ring = TerminalRing::new(TerminalRingConfig::new(2), 10);
142 ring.push(record(11));
143 ring.push(record(12));
144 ring.push(record(13));
145
146 let snapshot = ring.snapshot();
147 assert_eq!(snapshot.daemon_started_at_ms, 10);
148 assert_eq!(snapshot.dropped, 1);
149 assert_eq!(
150 snapshot
151 .entries
152 .iter()
153 .map(|entry| entry.at_ms)
154 .collect::<Vec<_>>(),
155 vec![12, 13]
156 );
157 }
158
159 #[test]
160 fn an_incoherent_zero_capacity_keeps_one_terminal() {
161 let mut ring = TerminalRing::new(TerminalRingConfig::new(0), 10);
162 ring.push(record(11));
163 ring.push(record(12));
164
165 let snapshot = ring.snapshot();
166 assert_eq!(snapshot.dropped, 1);
167 assert_eq!(snapshot.entries.len(), 1);
168 assert_eq!(snapshot.entries[0].at_ms, 12);
169 }
170}