Skip to main content

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}