1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::VecDeque;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9
10use crate::error::VictauriError;
11use crate::event::{AppEvent, IpcCall};
12
13const DEFAULT_MAX_CHECKPOINTS: usize = 1000;
14const DEFAULT_MAX_EVENTS: usize = 50_000;
15
16#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
18#[non_exhaustive]
19pub struct StateCheckpoint {
20 pub id: String,
22 pub label: Option<String>,
24 pub timestamp: DateTime<Utc>,
26 pub state: serde_json::Value,
28 pub event_index: usize,
30}
31
32#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct RecordedSession {
36 pub id: String,
38 pub started_at: DateTime<Utc>,
40 pub events: Vec<RecordedEvent>,
42 pub checkpoints: Vec<StateCheckpoint>,
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
48#[non_exhaustive]
49pub struct RecordedEvent {
50 pub index: usize,
52 pub timestamp: DateTime<Utc>,
54 pub event: AppEvent,
56}
57
58impl RecordedSession {
59 #[must_use]
62 pub fn new(
63 id: impl Into<String>,
64 started_at: DateTime<Utc>,
65 events: Vec<RecordedEvent>,
66 checkpoints: Vec<StateCheckpoint>,
67 ) -> Self {
68 Self {
69 id: id.into(),
70 started_at,
71 events,
72 checkpoints,
73 }
74 }
75}
76
77impl RecordedEvent {
78 #[must_use]
91 pub fn new(index: usize, timestamp: DateTime<Utc>, event: AppEvent) -> Self {
92 Self {
93 index,
94 timestamp,
95 event,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
103pub struct EventRecorder {
104 recording: Arc<Mutex<Option<ActiveRecording>>>,
105 last_session: Arc<Mutex<Option<RecordedSession>>>,
106 max_events: usize,
107 generations: Arc<AtomicU64>,
111}
112
113#[derive(Debug, Clone)]
114struct ActiveRecording {
115 session_id: String,
116 generation: u64,
117 started_at: DateTime<Utc>,
118 events: VecDeque<RecordedEvent>,
119 checkpoints: VecDeque<StateCheckpoint>,
120 event_counter: usize,
121 max_events: usize,
122 max_checkpoints: usize,
123}
124
125impl EventRecorder {
126 #[must_use]
136 pub fn new(max_events: usize) -> Self {
137 Self {
138 recording: Arc::new(Mutex::new(None)),
139 last_session: Arc::new(Mutex::new(None)),
140 max_events,
141 generations: Arc::new(AtomicU64::new(0)),
142 }
143 }
144
145 fn next_generation(&self) -> u64 {
146 self.generations.fetch_add(1, Ordering::Relaxed) + 1
147 }
148
149 pub fn start(&self, session_id: String) -> crate::error::Result<()> {
165 self.start_session(session_id).map(|_| ())
166 }
167
168 pub fn start_session(&self, session_id: String) -> crate::error::Result<u64> {
175 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
176 if rec.is_some() {
177 return Err(VictauriError::RecordingAlreadyActive);
178 }
179 let generation = self.next_generation();
180 *rec = Some(ActiveRecording {
181 session_id,
182 generation,
183 started_at: Utc::now(),
184 events: VecDeque::new(),
185 checkpoints: VecDeque::new(),
186 event_counter: 0,
187 max_events: self.max_events,
188 max_checkpoints: DEFAULT_MAX_CHECKPOINTS,
189 });
190 Ok(generation)
191 }
192
193 #[must_use]
207 pub fn stop(&self) -> Option<RecordedSession> {
208 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
209 self.finish(&mut rec)
210 }
211
212 fn finish(&self, rec: &mut Option<ActiveRecording>) -> Option<RecordedSession> {
215 rec.take().map(|r| {
216 let session = RecordedSession {
217 id: r.session_id,
218 started_at: r.started_at,
219 events: r.events.into_iter().collect(),
220 checkpoints: r.checkpoints.into_iter().collect(),
221 };
222 *crate::acquire_lock(&self.last_session, "EventRecorder::last_session") =
223 Some(session.clone());
224 session
225 })
226 }
227
228 #[must_use]
230 pub fn is_recording(&self) -> bool {
231 crate::acquire_lock(&self.recording, "EventRecorder").is_some()
232 }
233
234 #[must_use]
236 pub fn generation(&self) -> Option<u64> {
237 crate::acquire_lock(&self.recording, "EventRecorder")
238 .as_ref()
239 .map(|r| r.generation)
240 }
241
242 pub fn record_event(&self, event: AppEvent) {
244 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
245 if let Some(ref mut active) = *rec {
246 Self::append(active, event);
247 }
248 }
249
250 #[must_use]
255 pub fn record_event_if(&self, generation: u64, event: AppEvent) -> bool {
256 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
257 match rec.as_mut() {
258 Some(active) if active.generation == generation => {
259 Self::append(active, event);
260 true
261 }
262 _ => false,
263 }
264 }
265
266 fn append(active: &mut ActiveRecording, event: AppEvent) {
267 let timestamp = event.timestamp();
268 let index = active.event_counter;
269 active.event_counter = active.event_counter.saturating_add(1);
273
274 if active.events.len() >= active.max_events {
275 active.events.pop_front();
276 }
277
278 active.events.push_back(RecordedEvent {
279 index,
280 timestamp,
281 event,
282 });
283 }
284
285 pub fn checkpoint(
291 &self,
292 id: String,
293 label: Option<String>,
294 state: serde_json::Value,
295 ) -> crate::error::Result<()> {
296 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
297 if let Some(ref mut active) = *rec {
298 let event_index = active.event_counter;
299 if active.checkpoints.len() >= active.max_checkpoints {
300 active.checkpoints.pop_front();
301 }
302 active.checkpoints.push_back(StateCheckpoint {
303 id,
304 label,
305 timestamp: Utc::now(),
306 state,
307 event_index,
308 });
309 Ok(())
310 } else {
311 Err(VictauriError::NoActiveRecording)
312 }
313 }
314
315 #[must_use]
317 pub fn active_session_id(&self) -> Option<String> {
318 crate::acquire_lock(&self.recording, "EventRecorder")
319 .as_ref()
320 .map(|r| r.session_id.clone())
321 }
322
323 #[must_use]
332 pub fn stop_if_session(&self, session_id: &str) -> Option<RecordedSession> {
333 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
334 if rec.as_ref().is_some_and(|r| r.session_id == session_id) {
335 self.finish(&mut rec)
336 } else {
337 None
338 }
339 }
340
341 #[must_use]
343 pub fn stop_if_generation(&self, generation: u64) -> Option<RecordedSession> {
344 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
345 if rec.as_ref().is_some_and(|r| r.generation == generation) {
346 self.finish(&mut rec)
347 } else {
348 None
349 }
350 }
351
352 #[must_use]
354 pub fn started_at(&self) -> Option<DateTime<Utc>> {
355 crate::acquire_lock(&self.recording, "EventRecorder")
356 .as_ref()
357 .map(|r| r.started_at)
358 }
359
360 #[must_use]
364 pub fn latest_event_timestamp(&self) -> Option<DateTime<Utc>> {
365 crate::acquire_lock(&self.recording, "EventRecorder")
366 .as_ref()
367 .map(|r| {
368 r.events
369 .iter()
370 .map(|e| e.timestamp)
371 .max()
372 .unwrap_or(r.started_at)
373 })
374 }
375
376 #[must_use]
378 pub fn event_count(&self) -> usize {
379 crate::acquire_lock(&self.recording, "EventRecorder")
380 .as_ref()
381 .map_or(0, |r| r.events.len())
382 }
383
384 #[must_use]
386 pub fn checkpoint_count(&self) -> usize {
387 crate::acquire_lock(&self.recording, "EventRecorder")
388 .as_ref()
389 .map_or(0, |r| r.checkpoints.len())
390 }
391
392 #[must_use]
395 pub fn events_since(&self, index: usize) -> Vec<RecordedEvent> {
396 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
397 if let Some(active) = rec.as_ref() {
398 return active
399 .events
400 .iter()
401 .filter(|e| e.index >= index)
402 .cloned()
403 .collect();
404 }
405 drop(rec);
406 let last = crate::acquire_lock(&self.last_session, "EventRecorder::last_session");
407 last.as_ref().map_or_else(Vec::new, |session| {
408 session
409 .events
410 .iter()
411 .filter(|e| e.index >= index)
412 .cloned()
413 .collect()
414 })
415 }
416
417 #[must_use]
420 pub fn events_between(&self, from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<RecordedEvent> {
421 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
422 if let Some(active) = rec.as_ref() {
423 return active
424 .events
425 .iter()
426 .filter(|e| e.timestamp >= from && e.timestamp <= to)
427 .cloned()
428 .collect();
429 }
430 drop(rec);
431 let last = crate::acquire_lock(&self.last_session, "EventRecorder::last_session");
432 last.as_ref().map_or_else(Vec::new, |session| {
433 session
434 .events
435 .iter()
436 .filter(|e| e.timestamp >= from && e.timestamp <= to)
437 .cloned()
438 .collect()
439 })
440 }
441
442 #[must_use]
445 pub fn get_checkpoints(&self) -> Vec<StateCheckpoint> {
446 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
447 if let Some(active) = rec.as_ref() {
448 return active.checkpoints.iter().cloned().collect();
449 }
450 drop(rec);
451 let last = crate::acquire_lock(&self.last_session, "EventRecorder::last_session");
452 last.as_ref()
453 .map_or_else(Vec::new, |session| session.checkpoints.to_vec())
454 }
455
456 pub fn events_between_checkpoints(
464 &self,
465 from_checkpoint_id: &str,
466 to_checkpoint_id: &str,
467 ) -> crate::error::Result<Vec<RecordedEvent>> {
468 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
469 let source_checkpoints;
470 let source_events;
471 if let Some(active) = rec.as_ref() {
472 source_checkpoints = active.checkpoints.iter().cloned().collect::<Vec<_>>();
473 source_events = active.events.iter().cloned().collect::<Vec<_>>();
474 } else {
475 drop(rec);
476 let last = crate::acquire_lock(&self.last_session, "EventRecorder::last_session");
477 let session = last.as_ref().ok_or(VictauriError::NoActiveRecording)?;
478 source_checkpoints = session.checkpoints.clone();
479 source_events = session.events.clone();
480 }
481
482 let from_idx = source_checkpoints
483 .iter()
484 .find(|c| c.id == from_checkpoint_id)
485 .ok_or_else(|| VictauriError::CheckpointNotFound {
486 id: from_checkpoint_id.to_string(),
487 })?
488 .event_index;
489 let to_idx = source_checkpoints
490 .iter()
491 .find(|c| c.id == to_checkpoint_id)
492 .ok_or_else(|| VictauriError::CheckpointNotFound {
493 id: to_checkpoint_id.to_string(),
494 })?
495 .event_index;
496
497 let (start, end) = if from_idx <= to_idx {
498 (from_idx, to_idx)
499 } else {
500 (to_idx, from_idx)
501 };
502
503 Ok(source_events
504 .iter()
505 .filter(|e| e.index >= start && e.index < end)
506 .cloned()
507 .collect())
508 }
509
510 #[must_use]
513 pub fn export(&self) -> Option<RecordedSession> {
514 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
515 if let Some(r) = rec.as_ref() {
516 return Some(RecordedSession {
517 id: r.session_id.clone(),
518 started_at: r.started_at,
519 events: r.events.iter().cloned().collect(),
520 checkpoints: r.checkpoints.iter().cloned().collect(),
521 });
522 }
523 drop(rec);
524 crate::acquire_lock(&self.last_session, "EventRecorder::last_session").clone()
525 }
526
527 pub fn import(&self, session: RecordedSession) {
535 let active = self.imported(session);
536 *crate::acquire_lock(&self.recording, "EventRecorder") = Some(active);
537 }
538
539 pub fn import_if_idle(&self, session: RecordedSession) -> crate::error::Result<u64> {
547 let active = self.imported(session);
548 let generation = active.generation;
549 let mut rec = crate::acquire_lock(&self.recording, "EventRecorder");
550 if rec.is_some() {
551 return Err(VictauriError::RecordingAlreadyActive);
552 }
553 *rec = Some(active);
554 Ok(generation)
555 }
556
557 fn imported(&self, session: RecordedSession) -> ActiveRecording {
558 let max_events = self.max_events;
559 let max_checkpoints = DEFAULT_MAX_CHECKPOINTS;
560
561 let mut events: std::collections::VecDeque<RecordedEvent> =
562 session.events.into_iter().collect();
563 while events.len() > max_events {
564 events.pop_front();
565 }
566 let mut checkpoints: std::collections::VecDeque<StateCheckpoint> =
567 session.checkpoints.into_iter().collect();
568 while checkpoints.len() > max_checkpoints {
569 checkpoints.pop_front();
570 }
571
572 let event_counter = events.back().map_or(0, |e| e.index.saturating_add(1));
573
574 ActiveRecording {
575 session_id: session.id,
576 generation: self.next_generation(),
577 started_at: session.started_at,
578 events,
579 checkpoints,
580 event_counter,
581 max_events,
582 max_checkpoints,
583 }
584 }
585
586 #[must_use]
588 pub fn ipc_replay_sequence(&self) -> Vec<IpcCall> {
589 let rec = crate::acquire_lock(&self.recording, "EventRecorder");
590 if let Some(active) = rec.as_ref() {
591 return active
592 .events
593 .iter()
594 .filter_map(|re| match &re.event {
595 AppEvent::Ipc(call) => Some(call.clone()),
596 _ => None,
597 })
598 .collect();
599 }
600 drop(rec);
601 let last = crate::acquire_lock(&self.last_session, "EventRecorder::last_session");
602 last.as_ref().map_or_else(Vec::new, |session| {
603 session
604 .events
605 .iter()
606 .filter_map(|re| match &re.event {
607 AppEvent::Ipc(call) => Some(call.clone()),
608 _ => None,
609 })
610 .collect()
611 })
612 }
613}
614
615impl Default for EventRecorder {
616 fn default() -> Self {
617 Self::new(DEFAULT_MAX_EVENTS)
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 fn console(msg: &str) -> AppEvent {
626 AppEvent::console("log".to_string(), msg.to_string(), Utc::now())
627 }
628
629 #[test]
633 fn stop_if_session_never_stops_a_different_session() {
634 let rec = EventRecorder::new(100);
635 let racer = rec.clone();
636 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
637 let stop2 = Arc::clone(&stop);
638 let t = std::thread::spawn(move || {
639 while !stop2.load(std::sync::atomic::Ordering::Relaxed) {
640 let _ = racer.stop();
641 let _ = racer.start("other".to_string());
642 }
643 });
644 let mut wrong = 0;
645 for _ in 0..200_000 {
646 let _ = rec.start("mine".to_string());
647 if let Some(s) = rec.stop_if_session("mine")
648 && s.id != "mine"
649 {
650 wrong += 1;
651 }
652 }
653 stop.store(true, std::sync::atomic::Ordering::Relaxed);
654 t.join().unwrap();
655 assert_eq!(wrong, 0, "stop_if_session stopped someone else's recording");
656 }
657
658 #[test]
660 fn stale_generation_cannot_stop_a_later_recording_with_the_same_id() {
661 let rec = EventRecorder::new(100);
662 let old = rec.start_session("s".to_string()).unwrap();
663 let _ = rec.stop();
664 let new = rec.start_session("s".to_string()).unwrap();
665 assert_ne!(old, new);
666 assert!(rec.stop_if_generation(old).is_none());
667 assert!(rec.is_recording());
668 assert_eq!(rec.stop_if_generation(new).unwrap().id, "s");
669 }
670
671 #[test]
673 fn record_event_if_rejects_a_stale_generation() {
674 let rec = EventRecorder::new(100);
675 let a = rec.start_session("A".to_string()).unwrap();
676 assert!(rec.record_event_if(a, console("during A")));
677 let _ = rec.stop();
678 let b = rec.start_session("B".to_string()).unwrap();
679 assert!(!rec.record_event_if(a, console("late A read")));
680 assert!(rec.record_event_if(b, console("during B")));
681 assert_eq!(rec.event_count(), 1);
682 assert_eq!(rec.generation(), Some(b));
683 }
684
685 #[test]
687 fn import_if_idle_refuses_while_recording() {
688 let rec = EventRecorder::new(100);
689 rec.start("live".to_string()).unwrap();
690 let session = RecordedSession::new("imported", Utc::now(), vec![], vec![]);
691 assert!(rec.import_if_idle(session.clone()).is_err());
692 assert_eq!(rec.active_session_id().as_deref(), Some("live"));
693 let _ = rec.stop();
694 let g = rec.import_if_idle(session).unwrap();
695 assert_eq!(rec.generation(), Some(g));
696 assert_eq!(rec.active_session_id().as_deref(), Some("imported"));
697 }
698}