rtc_interceptor/gcc/state.rs
1//! The AIMD state machine: what a usage signal means for the rate.
2
3use super::overuse::Usage;
4
5/// What the rate controller should be doing.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum RateControlState {
8 /// Hold the current rate — used immediately after a decrease, so the effect of the backoff can
9 /// be observed before changing anything again.
10 #[default]
11 Hold,
12 /// Climb, looking for more capacity.
13 Increase,
14 /// Back off.
15 Decrease,
16}
17
18impl RateControlState {
19 /// The next state, given what the delay signal now says.
20 ///
21 /// Pure: no clock, no rate, no history beyond the current state. That is what makes it
22 /// exhaustively testable — the whole machine is nine transitions.
23 ///
24 /// The asymmetry is deliberate and is the heart of AIMD: **overuse always goes straight to
25 /// `Decrease`**, from any state, because congestion is urgent; but recovery goes through
26 /// `Hold` first, so the rate does not resume climbing before the backoff has taken effect.
27 pub fn next(self, usage: Usage) -> Self {
28 match (self, usage) {
29 // Congestion: back off at once, wherever we were.
30 (_, Usage::Over) => Self::Decrease,
31
32 // The queue is draining. Hold: the path is still recovering from whatever emptied it,
33 // and climbing now is what makes an estimate oscillate.
34 (_, Usage::Under) => Self::Hold,
35
36 // Quiet. Climb, unless we have just decreased — then hold once, to see the result.
37 (Self::Hold, Usage::Normal) => Self::Increase,
38 (Self::Increase, Usage::Normal) => Self::Increase,
39 (Self::Decrease, Usage::Normal) => Self::Hold,
40 }
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 /// Overuse is urgent: it goes to `Decrease` from anywhere, with no intermediate state.
49 #[test]
50 fn overuse_always_decreases() {
51 for state in [
52 RateControlState::Hold,
53 RateControlState::Increase,
54 RateControlState::Decrease,
55 ] {
56 assert_eq!(
57 RateControlState::Decrease,
58 state.next(Usage::Over),
59 "from {state:?}"
60 );
61 }
62 }
63
64 /// Recovery is not: after a decrease, one hold before climbing again. Without it the rate
65 /// resumes climbing before the backoff has reached the far end, and oscillates.
66 #[test]
67 fn recovery_holds_once_before_climbing() {
68 let after_backoff = RateControlState::Decrease.next(Usage::Normal);
69 assert_eq!(RateControlState::Hold, after_backoff);
70
71 assert_eq!(
72 RateControlState::Increase,
73 after_backoff.next(Usage::Normal),
74 "and then it may climb"
75 );
76 }
77
78 /// A quiet path keeps climbing, which is what finds capacity that has appeared.
79 #[test]
80 fn a_quiet_path_keeps_increasing() {
81 let mut state = RateControlState::Increase;
82 for _ in 0..10 {
83 state = state.next(Usage::Normal);
84 }
85 assert_eq!(RateControlState::Increase, state);
86 }
87
88 /// A draining queue holds rather than climbing: something else is using the path, or the
89 /// backlog is still clearing.
90 #[test]
91 fn a_draining_queue_holds() {
92 for state in [
93 RateControlState::Hold,
94 RateControlState::Increase,
95 RateControlState::Decrease,
96 ] {
97 assert_eq!(
98 RateControlState::Hold,
99 state.next(Usage::Under),
100 "from {state:?}"
101 );
102 }
103 }
104
105 /// The machine is total: nine transitions, all defined, none panicking.
106 #[test]
107 fn every_transition_is_defined() {
108 for state in [
109 RateControlState::Hold,
110 RateControlState::Increase,
111 RateControlState::Decrease,
112 ] {
113 for usage in [Usage::Normal, Usage::Over, Usage::Under] {
114 let _ = state.next(usage);
115 }
116 }
117 }
118}