Skip to main content

ic_logging/history/
mod.rs

1//! Opaque structured history; consumers own payload admission and timestamp semantics.
2
3use alloc::collections::VecDeque;
4use core::fmt;
5
6#[cfg(test)]
7mod tests;
8
9/// Immutable bounds on row count and caller-accounted payload bytes.
10///
11/// Bytes are a consumer-defined charge, not a measurement of all heap memory.
12/// Consumers must bound each payload's fields before appending and account for
13/// their retained content consistently. Inline metadata, spare capacity and
14/// allocator overhead are not automatically charged.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct HistoryLimits {
17    /// Maximum retained rows; zero disables admission.
18    pub entries: usize,
19    /// Maximum sum of retained row payload charges.
20    pub payload_bytes: usize,
21}
22
23/// Successful admission and the oldest history removed to make room.
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub struct HistoryAppend {
26    /// Number of evicted rows.
27    pub evicted: usize,
28    /// Sum of the evicted rows' payload charges.
29    pub evicted_payload_bytes: usize,
30}
31
32/// Why an opaque history refused an input before changing retained state.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum HistoryRejectionReason {
35    /// The configured row capacity is zero.
36    Disabled,
37    /// The input cannot fit even in an empty history.
38    PayloadTooLarge {
39        /// Caller-supplied input charge.
40        supplied: usize,
41        /// Configured total payload ceiling.
42        maximum: usize,
43    },
44}
45
46impl fmt::Display for HistoryRejectionReason {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Disabled => formatter.write_str("history admission is disabled"),
50            Self::PayloadTooLarge { supplied, maximum } => write!(
51                formatter,
52                "history payload charge {supplied} exceeds {maximum}"
53            ),
54        }
55    }
56}
57
58impl core::error::Error for HistoryRejectionReason {}
59
60/// Refused input, returned intact without requiring cloning.
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct HistoryRejection<T> {
63    /// Input whose ownership returns to the caller.
64    pub value: T,
65    /// Typed refusal reason.
66    pub reason: HistoryRejectionReason,
67}
68
69impl<T> fmt::Display for HistoryRejection<T> {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        self.reason.fmt(formatter)
72    }
73}
74
75impl<T: fmt::Debug> core::error::Error for HistoryRejection<T> {}
76
77#[derive(Clone, Debug)]
78struct Row<T> {
79    value: T,
80    payload_bytes: usize,
81}
82
83/// Count- and payload-bounded heap history of consumer-owned structured rows.
84///
85/// Oldest rows are evicted first. Iteration borrows immutable rows in append
86/// order and can be reversed for newest-first views. No `Clone` bound is needed.
87/// There is no clock, timestamp ordering, sequence identity, truncation, codec,
88/// checkpoint or global recorder. Consumers retain sanitization, field bounds,
89/// privacy, synchronization and lifecycle; this is not a durable byte format.
90/// Interior-mutability payloads must not grow their charged content while retained.
91#[derive(Clone, Debug)]
92pub struct BoundedHistory<T> {
93    limits: HistoryLimits,
94    rows: VecDeque<Row<T>>,
95    payload_bytes: usize,
96}
97
98impl<T> BoundedHistory<T> {
99    /// Construct an empty history without allocating.
100    #[must_use]
101    pub const fn new(limits: HistoryLimits) -> Self {
102        Self {
103            limits,
104            rows: VecDeque::new(),
105            payload_bytes: 0,
106        }
107    }
108
109    /// Append one pre-admitted payload, evicting oldest rows until both bounds fit.
110    ///
111    /// Work is bounded by the configured retained row count. Payload charges
112    /// are stored at admission and are not recomputed during eviction.
113    ///
114    /// # Errors
115    /// Returns the original input when disabled or individually oversized.
116    /// Refusal changes neither retained rows nor payload accounting.
117    pub fn append(
118        &mut self,
119        value: T,
120        payload_bytes: usize,
121    ) -> Result<HistoryAppend, HistoryRejection<T>> {
122        let reason = if self.limits.entries == 0 {
123            Some(HistoryRejectionReason::Disabled)
124        } else if payload_bytes > self.limits.payload_bytes {
125            Some(HistoryRejectionReason::PayloadTooLarge {
126                supplied: payload_bytes,
127                maximum: self.limits.payload_bytes,
128            })
129        } else {
130            None
131        };
132        if let Some(reason) = reason {
133            return Err(HistoryRejection { value, reason });
134        }
135
136        let mut receipt = HistoryAppend::default();
137        while self.rows.len() >= self.limits.entries
138            || self.payload_bytes > self.limits.payload_bytes - payload_bytes
139        {
140            if let Some(row) = self.rows.pop_front() {
141                self.payload_bytes -= row.payload_bytes;
142                receipt.evicted += 1;
143                receipt.evicted_payload_bytes += row.payload_bytes;
144            }
145        }
146        self.rows.push_back(Row {
147            value,
148            payload_bytes,
149        });
150        self.payload_bytes += payload_bytes;
151        Ok(receipt)
152    }
153
154    /// Borrow rows oldest first; reverse the iterator for newest-first projection.
155    pub fn entries(&self) -> impl DoubleEndedIterator<Item = &T> + ExactSizeIterator + '_ {
156        self.rows.iter().map(|row| &row.value)
157    }
158
159    /// Current sum of caller-accounted retained payload bytes.
160    #[must_use]
161    pub const fn retained_payload_bytes(&self) -> usize {
162        self.payload_bytes
163    }
164
165    /// Immutable consumer-selected bounds.
166    #[must_use]
167    pub const fn limits(&self) -> HistoryLimits {
168        self.limits
169    }
170
171    /// Drop all retained rows and charges, preserving limits.
172    /// This establishes no instance/reset identity.
173    pub fn clear(&mut self) {
174        self.rows.clear();
175        self.payload_bytes = 0;
176    }
177}