1use std::collections::BTreeMap;
8use std::fs::{File, OpenOptions};
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex};
12
13use crate::{JournalReplyBody, JournalWireEntry};
14
15use crate::buffer::BufferedEvent;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct JournalEntry {
20 pub seq: u64,
21 pub channel_id: u32,
22 pub payload: Vec<u8>,
23}
24
25pub trait NodeJournal: Send + Sync {
27 fn replay_after(
29 &self,
30 node_id: &str,
31 channel_id: u32,
32 after: u64,
33 limit: usize,
34 ) -> Result<Vec<JournalEntry>, JournalError>;
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum JournalError {
39 NodeUnknown,
40 NodeUnavailable,
41 Io(String),
42 Corrupt,
43 Other(String),
44}
45
46impl std::fmt::Display for JournalError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::NodeUnknown => write!(f, "node unknown"),
50 Self::NodeUnavailable => write!(f, "node unavailable"),
51 Self::Io(msg) => write!(f, "journal io: {msg}"),
52 Self::Corrupt => write!(f, "journal corrupt"),
53 Self::Other(msg) => write!(f, "{msg}"),
54 }
55 }
56}
57
58impl std::error::Error for JournalError {}
59
60#[derive(Clone, Default)]
62pub struct MemoryJournal {
63 inner: Arc<Mutex<BTreeMap<String, Vec<JournalEntry>>>>,
64}
65
66impl MemoryJournal {
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn append(&self, node_id: &str, channel_id: u32, seq: u64, payload: Vec<u8>) {
72 let mut guard = self.inner.lock().expect("journal lock");
73 guard
74 .entry(node_id.to_owned())
75 .or_default()
76 .push(JournalEntry {
77 seq,
78 channel_id,
79 payload,
80 });
81 }
82}
83
84impl NodeJournal for MemoryJournal {
85 fn replay_after(
86 &self,
87 node_id: &str,
88 channel_id: u32,
89 after: u64,
90 limit: usize,
91 ) -> Result<Vec<JournalEntry>, JournalError> {
92 let guard = self.inner.lock().expect("journal lock");
93 let Some(entries) = guard.get(node_id) else {
94 return Err(JournalError::NodeUnknown);
95 };
96 Ok(entries
97 .iter()
98 .filter(|e| e.channel_id == channel_id && e.seq > after)
99 .take(limit)
100 .cloned()
101 .collect())
102 }
103}
104
105#[derive(Clone)]
110pub struct FileJournal {
111 path: PathBuf,
112 state: Arc<Mutex<FileJournalState>>,
113}
114
115struct FileJournalState {
116 next_seq: BTreeMap<u32, u64>,
118}
119
120impl FileJournal {
121 pub fn open(path: impl Into<PathBuf>) -> Result<Self, JournalError> {
122 let path = path.into();
123 if let Some(parent) = path.parent() {
124 std::fs::create_dir_all(parent).map_err(|e| JournalError::Io(e.to_string()))?;
125 }
126 let mut next_seq = BTreeMap::new();
127 if path.exists() {
128 for entry in scan_file(&path)? {
129 let next = next_seq.entry(entry.channel_id).or_insert(1);
130 *next = (*next).max(entry.seq + 1);
131 }
132 } else {
133 File::create(&path).map_err(|e| JournalError::Io(e.to_string()))?;
134 }
135 Ok(Self {
136 path,
137 state: Arc::new(Mutex::new(FileJournalState { next_seq })),
138 })
139 }
140
141 pub fn path(&self) -> &Path {
142 &self.path
143 }
144
145 pub fn append(&self, channel_id: u32, payload: &[u8]) -> Result<u64, JournalError> {
147 if payload.len() > 1024 * 1024 {
148 return Err(JournalError::Other("payload too large".into()));
149 }
150 let mut guard = self.state.lock().expect("journal lock");
151 let seq = {
152 let entry = guard.next_seq.entry(channel_id).or_insert(1);
153 let seq = *entry;
154 *entry = seq + 1;
155 seq
156 };
157 let mut file = OpenOptions::new()
158 .append(true)
159 .open(&self.path)
160 .map_err(|e| JournalError::Io(e.to_string()))?;
161 write_record(&mut file, channel_id, seq, payload)?;
162 file.flush().map_err(|e| JournalError::Io(e.to_string()))?;
163 Ok(seq)
164 }
165
166 pub fn replay_after(
167 &self,
168 channel_id: u32,
169 after: u64,
170 limit: usize,
171 ) -> Result<Vec<JournalEntry>, JournalError> {
172 let mut out = Vec::new();
173 for entry in scan_file(&self.path)? {
174 if entry.channel_id == channel_id && entry.seq > after {
175 out.push(entry);
176 if out.len() >= limit {
177 break;
178 }
179 }
180 }
181 Ok(out)
182 }
183
184 pub fn reply_body(
186 &self,
187 channel_id: u32,
188 after: u64,
189 limit: usize,
190 ) -> Result<JournalReplyBody, JournalError> {
191 let entries = self.replay_after(channel_id, after, limit)?;
192 Ok(JournalReplyBody {
193 after,
194 entries: entries
195 .into_iter()
196 .map(|e| JournalWireEntry {
197 seq: e.seq,
198 payload: e.payload,
199 })
200 .collect(),
201 })
202 }
203}
204
205pub struct LocalFileJournal {
207 node_id: String,
208 journal: FileJournal,
209}
210
211impl LocalFileJournal {
212 pub fn new(node_id: impl Into<String>, journal: FileJournal) -> Self {
213 Self {
214 node_id: node_id.into(),
215 journal,
216 }
217 }
218
219 pub fn journal(&self) -> &FileJournal {
220 &self.journal
221 }
222}
223
224impl NodeJournal for LocalFileJournal {
225 fn replay_after(
226 &self,
227 node_id: &str,
228 channel_id: u32,
229 after: u64,
230 limit: usize,
231 ) -> Result<Vec<JournalEntry>, JournalError> {
232 if node_id != self.node_id {
233 return Err(JournalError::NodeUnknown);
234 }
235 self.journal.replay_after(channel_id, after, limit)
236 }
237}
238
239fn write_record(file: &mut File, channel_id: u32, seq: u64, payload: &[u8]) -> Result<(), JournalError> {
240 file.write_all(&channel_id.to_le_bytes())
241 .map_err(|e| JournalError::Io(e.to_string()))?;
242 file.write_all(&seq.to_le_bytes())
243 .map_err(|e| JournalError::Io(e.to_string()))?;
244 file.write_all(&(payload.len() as u32).to_le_bytes())
245 .map_err(|e| JournalError::Io(e.to_string()))?;
246 file.write_all(payload)
247 .map_err(|e| JournalError::Io(e.to_string()))?;
248 Ok(())
249}
250
251fn scan_file(path: &Path) -> Result<Vec<JournalEntry>, JournalError> {
252 let mut file = File::open(path).map_err(|e| JournalError::Io(e.to_string()))?;
253 let mut out = Vec::new();
254 loop {
255 let mut hdr = [0u8; 4 + 8 + 4];
256 match file.read_exact(&mut hdr) {
257 Ok(()) => {}
258 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
259 Err(e) => return Err(JournalError::Io(e.to_string())),
260 }
261 let channel_id = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
262 let seq = u64::from_le_bytes([
263 hdr[4], hdr[5], hdr[6], hdr[7], hdr[8], hdr[9], hdr[10], hdr[11],
264 ]);
265 let len = u32::from_le_bytes([hdr[12], hdr[13], hdr[14], hdr[15]]) as usize;
266 if len > 1024 * 1024 {
267 return Err(JournalError::Corrupt);
268 }
269 let mut payload = vec![0u8; len];
270 file.read_exact(&mut payload)
271 .map_err(|_| JournalError::Corrupt)?;
272 out.push(JournalEntry {
273 seq,
274 channel_id,
275 payload,
276 });
277 }
278 let pos = file.stream_position().map_err(|e| JournalError::Io(e.to_string()))?;
280 let len = file.seek(SeekFrom::End(0)).map_err(|e| JournalError::Io(e.to_string()))?;
281 if pos != len && pos + (4 + 8 + 4) as u64 <= len {
282 return Err(JournalError::Corrupt);
284 }
285 Ok(out)
286}
287
288pub fn recover_gap(
290 journal: &dyn NodeJournal,
291 node_id: &str,
292 channel_id: u32,
293 after: u64,
294 until_exclusive: Option<u64>,
295 limit: usize,
296) -> Result<Vec<BufferedEvent>, JournalError> {
297 let entries = journal.replay_after(node_id, channel_id, after, limit)?;
298 Ok(entries
299 .into_iter()
300 .filter(|e| until_exclusive.map(|u| e.seq < u).unwrap_or(true))
301 .map(|e| BufferedEvent {
302 seq: e.seq,
303 node_id: node_id.to_owned(),
304 channel_id: e.channel_id,
305 payload: e.payload,
306 enqueued_at: std::time::Instant::now(),
307 })
308 .collect())
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn memory_journal_replays_slice() {
317 let j = MemoryJournal::new();
318 for seq in 1..=5 {
319 j.append("n1", 1, seq, vec![seq as u8]);
320 }
321 let got = j.replay_after("n1", 1, 2, 10).unwrap();
322 assert_eq!(got.iter().map(|e| e.seq).collect::<Vec<_>>(), vec![3, 4, 5]);
323 }
324
325 #[test]
326 fn recover_gap_stops_before_buffer_oldest() {
327 let j = MemoryJournal::new();
328 for seq in 1..=10 {
329 j.append("n1", 1, seq, vec![seq as u8]);
330 }
331 let recovered = recover_gap(&j, "n1", 1, 1, Some(5), 100).unwrap();
332 assert_eq!(
333 recovered.iter().map(|e| e.seq).collect::<Vec<_>>(),
334 vec![2, 3, 4]
335 );
336 }
337
338 #[test]
339 fn file_journal_persists_across_reopen() {
340 let dir = std::env::temp_dir().join(format!("fj-{}", std::process::id()));
341 let _ = std::fs::remove_dir_all(&dir);
342 std::fs::create_dir_all(&dir).unwrap();
343 let path = dir.join("node.journal");
344 {
345 let j = FileJournal::open(&path).unwrap();
346 assert_eq!(j.append(1, b"a").unwrap(), 1);
347 assert_eq!(j.append(1, b"b").unwrap(), 2);
348 assert_eq!(j.append(2, b"x").unwrap(), 1);
349 }
350 let j = FileJournal::open(&path).unwrap();
351 let got = j.replay_after(1, 0, 10).unwrap();
352 assert_eq!(got.len(), 2);
353 assert_eq!(got[0].payload, b"a");
354 assert_eq!(got[1].payload, b"b");
355 assert_eq!(j.append(1, b"c").unwrap(), 3);
356 let body = j.reply_body(1, 1, 10).unwrap();
357 assert_eq!(body.entries.len(), 2);
358 assert_eq!(body.entries[0].seq, 2);
359 assert_eq!(body.entries[1].seq, 3);
360 let _ = std::fs::remove_dir_all(&dir);
361 }
362
363 #[test]
364 fn local_file_journal_trait() {
365 let dir = std::env::temp_dir().join(format!("lfj-{}", std::process::id()));
366 let _ = std::fs::remove_dir_all(&dir);
367 let path = dir.join("j");
368 let fj = FileJournal::open(&path).unwrap();
369 fj.append(1, b"p").unwrap();
370 let local = LocalFileJournal::new("node-a", fj);
371 let got = NodeJournal::replay_after(&local, "node-a", 1, 0, 10).unwrap();
372 assert_eq!(got[0].payload, b"p");
373 assert!(NodeJournal::replay_after(&local, "other", 1, 0, 10).is_err());
374 let _ = std::fs::remove_dir_all(&dir);
375 }
376}