Skip to main content

iris_chat_protocol/protocol_engine/
group_retry_pacing.rs

1use super::*;
2
3/// Resource policy for automatic history recovery, separate from ratchet state
4/// and rollback checkpoints. Time spent on a failed transaction is still work.
5#[derive(Default)]
6pub(super) struct GroupRetryPacing {
7    pub(super) enabled: bool,
8    pub(super) not_before: Option<std::time::Instant>,
9    pub(super) fresh_not_before: Option<std::time::Instant>,
10    #[cfg(test)]
11    pub(super) clock: Option<std::time::Instant>,
12    #[cfg(test)]
13    pub(super) key_trials: usize,
14    #[cfg(test)]
15    pub(super) work_calls: usize,
16    #[cfg(test)]
17    pub(super) fresh_key_trials: usize,
18    #[cfg(test)]
19    pub(super) fresh_work_calls: usize,
20}
21
22impl GroupRetryPacing {
23    pub(super) fn now(&self) -> std::time::Instant {
24        #[cfg(test)]
25        if let Some(now) = self.clock {
26            return now;
27        }
28        std::time::Instant::now()
29    }
30
31    pub(super) fn delay(&self) -> std::time::Duration {
32        if !self.enabled {
33            return std::time::Duration::ZERO;
34        }
35        self.not_before
36            .map_or(std::time::Duration::ZERO, |deadline| {
37                deadline.saturating_duration_since(self.now())
38            })
39    }
40
41    pub(super) fn record_work(&mut self, elapsed: std::time::Duration) {
42        if self.enabled {
43            #[cfg(test)]
44            if let Some(now) = self.clock {
45                // Explicit virtual clocks advance through the measured work as
46                // well as the cooldown; latency evidence must count both.
47                self.clock = now.checked_add(elapsed);
48            }
49            // At most 2% sustained blind-search duty, leaving room for the rest
50            // of the application. This is wall work, conservatively including
51            // preemption; it is not a claim about total application CPU usage.
52            self.not_before = self
53                .not_before
54                .unwrap_or_else(|| self.now())
55                .max(self.now())
56                .checked_add(elapsed.saturating_mul(49));
57        }
58    }
59
60    pub(super) fn fresh_delay(&self) -> std::time::Duration {
61        self.fresh_not_before
62            .map_or(std::time::Duration::ZERO, |deadline| {
63                deadline.saturating_duration_since(self.now())
64            })
65    }
66
67    pub(super) fn record_fresh_work(&mut self, elapsed: std::time::Duration) {
68        // A small current-position probe may run while history is cooling down.
69        // Its measured cost also extends the GLOBAL debt: sustained fresh work
70        // replaces history work instead of creating another ongoing CPU budget.
71        self.record_work(elapsed);
72        self.fresh_not_before = self.now().checked_add(elapsed.saturating_mul(49));
73    }
74}
75
76impl ProtocolEngine {
77    /// Pace automatic blind history recovery by its measured work cost.
78    /// Manual protocol drivers retain their existing per-call work bounds unless
79    /// they opt in. Hosts must use `group_sender_key_retry_delay` when scheduling
80    /// continuations; calls from other events obey the same internal work gate.
81    pub fn enable_background_history_recovery_pacing(&self) {
82        self.group_retry_pacing.borrow_mut().enabled = true;
83    }
84
85    /// Earliest useful blind-search continuation. Prepared receive plans can be
86    /// applied immediately; membership and ratchet validation remain mandatory.
87    pub fn group_sender_key_retry_delay(&self) -> std::time::Duration {
88        let retry = self.group_sender_key_retry.borrow();
89        if retry.prepared.is_some() {
90            return std::time::Duration::ZERO;
91        }
92        let pacing = self.group_retry_pacing.borrow();
93        if retry.fresh.is_empty() {
94            pacing.delay()
95        } else {
96            pacing.delay().min(pacing.fresh_delay())
97        }
98    }
99}