mumble_server_runtime_shard/
journal.rs1use std::collections::VecDeque;
22
23use thiserror::Error;
24
25use crate::plan::PlannedOp;
26
27pub const DEPTH: usize = 256;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
35#[error(
36 "cursor {cursor} is below the journal tail {tail}: this connection has missed too much to be \
37 repaired from deltas"
38)]
39pub struct TooFarBehind {
40 pub cursor: u64,
41 pub tail: u64,
42}
43
44#[derive(Debug, Default)]
46pub struct Journal {
47 tail: u64,
48 head: u64,
49 entries: VecDeque<Vec<PlannedOp>>,
50}
51
52impl Journal {
53 #[must_use]
55 pub fn new() -> Journal {
56 Journal {
57 tail: 0,
58 head: 0,
59 entries: VecDeque::new(),
60 }
61 }
62
63 #[must_use]
65 pub fn head(&self) -> u64 {
66 self.head
67 }
68
69 #[must_use]
71 pub fn tail(&self) -> u64 {
72 self.tail
73 }
74
75 pub fn push(&mut self, ops: Vec<PlannedOp>) -> u64 {
80 self.head = self.head.saturating_add(1);
81 self.entries.push_back(ops);
82 while self.entries.len() > DEPTH {
83 let _evicted = self.entries.pop_front();
84 self.tail = self.tail.saturating_add(1);
85 }
86 self.head
87 }
88
89 pub fn replay(&self, from: u64, to: u64) -> Result<Vec<&PlannedOp>, TooFarBehind> {
97 if from < self.tail {
98 return Err(TooFarBehind {
99 cursor: from,
100 tail: self.tail,
101 });
102 }
103 let to = to.min(self.head);
104 if to <= from {
105 return Ok(Vec::new());
106 }
107
108 let start = usize::try_from(from - self.tail).unwrap_or(usize::MAX);
111 let end = usize::try_from(to - self.tail).unwrap_or(usize::MAX);
112 Ok(self
113 .entries
114 .iter()
115 .take(end)
116 .skip(start)
117 .flatten()
118 .collect())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 #![allow(clippy::expect_used)]
125
126 use super::*;
127 use crate::ids::{ChannelId, SessionId};
128 use crate::plan::PlanOp;
129 use crate::scope::Scope;
130
131 fn delta(marker: u32) -> Vec<PlannedOp> {
132 vec![PlannedOp {
133 op: PlanOp::RemoveUser(SessionId(marker)),
134 scope: Scope::ROOT,
135 }]
136 }
137
138 fn markers(ops: &[&PlannedOp]) -> Vec<u32> {
139 ops.iter()
140 .filter_map(|planned| match planned.op {
141 PlanOp::RemoveUser(session) => Some(session.0),
142 _ => None,
143 })
144 .collect()
145 }
146
147 #[test]
148 fn replaying_in_pieces_equals_replaying_at_once() {
149 let mut journal = Journal::new();
150 for marker in 1..=10 {
151 journal.push(delta(marker));
152 }
153
154 let whole = journal.replay(0, 10).expect("within the journal");
155 let mut pieced: Vec<u32> = Vec::new();
156 for (from, to) in [(0, 3), (3, 3), (3, 7), (7, 10)] {
157 pieced.extend(markers(
158 &journal.replay(from, to).expect("within the journal"),
159 ));
160 }
161
162 assert_eq!(markers(&whole), pieced);
163 assert_eq!(markers(&whole), (1..=10).collect::<Vec<u32>>());
164 }
165
166 #[test]
167 fn a_cursor_at_the_head_replays_nothing() {
168 let mut journal = Journal::new();
169 journal.push(delta(1));
170 assert!(
171 journal
172 .replay(journal.head(), journal.head())
173 .expect("within the journal")
174 .is_empty()
175 );
176 }
177
178 #[test]
179 fn a_cursor_below_the_tail_is_detected_rather_than_silently_truncated() {
180 let mut journal = Journal::new();
181 for marker in 0..u32::try_from(DEPTH).unwrap_or(256) + 5 {
182 journal.push(delta(marker));
183 }
184
185 assert_eq!(journal.tail(), 5, "five versions were evicted");
186 assert_eq!(
187 journal.replay(4, journal.head()),
188 Err(TooFarBehind { cursor: 4, tail: 5 })
189 );
190 assert!(
191 journal.replay(5, journal.head()).is_ok(),
192 "the tail itself is still a valid cursor"
193 );
194 }
195
196 #[test]
197 fn the_journal_never_grows_past_its_depth() {
198 let mut journal = Journal::new();
199 for marker in 0..u32::try_from(DEPTH).unwrap_or(256) * 3 {
200 journal.push(delta(marker));
201 }
202 assert_eq!(journal.entries.len(), DEPTH);
203 assert_eq!(journal.head() - journal.tail(), DEPTH as u64);
204 }
205
206 #[test]
207 fn asking_beyond_the_head_stops_at_the_head() {
208 let mut journal = Journal::new();
209 journal.push(delta(1));
210 journal.push(delta(2));
211
212 let ops = journal.replay(0, 999).expect("within the journal");
213 assert_eq!(markers(&ops), vec![1, 2]);
214 }
215
216 #[test]
217 fn an_empty_delta_still_advances_the_version() {
218 let mut journal = Journal::new();
219 let version = journal.push(Vec::new());
220 assert_eq!(version, 1);
221 assert_eq!(journal.head(), 1);
222 assert_eq!(
223 journal.replay(0, 1).expect("within the journal").len(),
224 0,
225 "a version with no operations replays as nothing"
226 );
227 let _ = ChannelId::ROOT;
228 }
229}