Skip to main content

sozu_lib/protocol/mux/
debug.rs

1//! Debug history ring buffer and event enum.
2//!
3//! Used by the mux layer to record a bounded trail of per-session events for
4//! post-mortem inspection when `debug_assertions` are enabled. In release
5//! builds `push` and `set_interesting` are no-ops, so the ring is allocated
6//! lazily on the first `push` and a release session never allocates it.
7
8use std::collections::VecDeque;
9
10use kawa::ParsingPhase;
11use mio::Token;
12use sozu_command::{ready::Ready, state::ClusterId};
13
14use super::{BackendConnectionError, MuxResult, Readiness, StreamState};
15
16/// Maximum number of debug events retained in the ring buffer.
17/// Oldest entries are dropped when this limit is reached.
18#[cfg(debug_assertions)]
19pub(super) const DEBUG_HISTORY_CAPACITY: usize = 512;
20
21#[derive(Default)]
22pub struct DebugHistory {
23    /// Starts empty and unallocated: only a debug-build `push` ever records,
24    /// and it reserves the whole ring on first use.
25    pub events: VecDeque<DebugEvent>,
26    pub is_interesting: bool,
27}
28impl DebugHistory {
29    pub fn new() -> Self {
30        Self::default()
31    }
32    pub fn push(&mut self, _event: DebugEvent) {
33        #[cfg(debug_assertions)]
34        {
35            if self.events.capacity() == 0 {
36                self.events.reserve_exact(DEBUG_HISTORY_CAPACITY);
37            }
38            if self.events.len() >= DEBUG_HISTORY_CAPACITY {
39                self.events.pop_front();
40            }
41            self.events.push_back(_event);
42        }
43    }
44    pub fn set_interesting(&mut self, _interesting: bool) {
45        #[cfg(debug_assertions)]
46        {
47            self.is_interesting = _interesting;
48        }
49    }
50    pub fn is_interesting(&self) -> bool {
51        #[cfg(debug_assertions)]
52        {
53            self.is_interesting
54        }
55        #[cfg(not(debug_assertions))]
56        {
57            false
58        }
59    }
60}
61
62#[derive(Debug)]
63pub enum DebugEvent {
64    EV(Token, Ready),
65    ReadyTimestamp(usize),
66    LoopStart,
67    LoopIteration(i32),
68    SR(Token, MuxResult, Readiness),
69    SW(Token, MuxResult, Readiness),
70    CW(Token, MuxResult, Readiness),
71    CR(Token, MuxResult, Readiness),
72    CC(usize, StreamState),
73    CCS(Token, ClusterId),
74    CCF(usize, BackendConnectionError),
75    CH(Token, Readiness),
76    S(u32, usize, ParsingPhase, usize, usize),
77    Str(String),
78    StreamEvent(usize, usize),
79    SocketIO(usize, usize, usize),
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::test_allocations::allocations;
86
87    /// Every session builds one `DebugHistory` through `Context::new`; release
88    /// builds never record into it, so building one must not allocate the
89    /// ring ([#1585](https://github.com/sozu-proxy/sozu/issues/1585)).
90    #[test]
91    fn creating_a_debug_history_allocates_nothing() {
92        let before = allocations();
93        let history = DebugHistory::new();
94        let after = allocations();
95        assert_eq!(after - before, 0, "DebugHistory::new must not allocate");
96        assert_eq!(history.events.capacity(), 0);
97    }
98
99    /// Debug builds keep the full ring: one allocation of the whole capacity
100    /// on the first push, then oldest-first eviction at the bound.
101    #[cfg(debug_assertions)]
102    #[test]
103    fn debug_history_keeps_the_newest_events_up_to_its_capacity() {
104        let mut history = DebugHistory::new();
105        let before = allocations();
106        for i in 0..=DEBUG_HISTORY_CAPACITY as i32 {
107            history.push(DebugEvent::LoopIteration(i));
108        }
109        let after = allocations();
110        assert_eq!(
111            after - before,
112            1,
113            "the ring is allocated once, at full capacity"
114        );
115        assert!(history.events.capacity() >= DEBUG_HISTORY_CAPACITY);
116        assert_eq!(history.events.len(), DEBUG_HISTORY_CAPACITY);
117        assert!(matches!(
118            history.events.front(),
119            Some(DebugEvent::LoopIteration(1))
120        ));
121        assert!(matches!(
122            history.events.back(),
123            Some(DebugEvent::LoopIteration(i)) if *i == DEBUG_HISTORY_CAPACITY as i32
124        ));
125        history.set_interesting(true);
126        assert!(history.is_interesting());
127        let dump = format!("{:?}", history.events);
128        assert!(dump.starts_with("[LoopIteration(1), "), "{dump}");
129    }
130
131    /// Release builds record nothing, so pushing never allocates either.
132    #[cfg(not(debug_assertions))]
133    #[test]
134    fn release_history_records_nothing() {
135        let mut history = DebugHistory::new();
136        let before = allocations();
137        history.push(DebugEvent::LoopIteration(0));
138        history.set_interesting(true);
139        let after = allocations();
140        assert_eq!(after - before, 0);
141        assert!(history.events.is_empty());
142        assert!(!history.is_interesting());
143    }
144}