Skip to main content

conmonrs/streaming_server/
bounded_map.rs

1//! The tokens of the streaming sessions which wait for their clients, bounded
2//! in age and number.
3
4use std::{collections::VecDeque, fmt::Debug, time::Duration};
5// The tokio clock, so that tests can pause and advance it.
6use tokio::time::Instant;
7use tracing::debug;
8
9/// A map bounded by element age and maximum amount of items. Elements only
10/// get inserted and taken. A full map takes no more elements until one of
11/// them got taken or expired, a valid element never gets dropped for a new
12/// one. Clients take their element right after it got inserted, the map
13/// holds few of them and a linear search is quick enough.
14pub(super) struct BoundedMap<K, V> {
15    /// The elements with the time of their insert, the oldest first.
16    entries: VecDeque<(K, Instant, V)>,
17    capacity: usize,
18    max_duration: Duration,
19}
20
21impl<K, V> Debug for BoundedMap<K, V> {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("BoundedMap")
24            .field("len", &self.entries.len())
25            .field("max_duration", &self.max_duration)
26            .finish()
27    }
28}
29
30impl<K, V> BoundedMap<K, V>
31where
32    K: Eq + Debug,
33{
34    /// Insert an element into the map, after removing the timed-out ones.
35    /// Returns the new one back if the map is full. The keys are random
36    /// tokens, which do not repeat, so the map does not look for an element
37    /// with the same key.
38    pub(super) fn insert(&mut self, k: K, v: V) -> Result<(), V> {
39        let now = Instant::now();
40        self.remove_expired(now);
41
42        if self.entries.len() >= self.capacity {
43            return Err(v);
44        }
45        self.entries.push_back((k, now, v));
46        Ok(())
47    }
48
49    /// Remove an element from the map and return it if the element has not
50    /// expired. The expired ones get removed as well, so that they do not
51    /// linger until the next insert, they may hold sockets.
52    pub(super) fn remove(&mut self, k: &K) -> Option<V> {
53        let now = Instant::now();
54        // Taken before the other expired ones go, which tells about a client
55        // which came too late.
56        let entry = self.position(k).and_then(|pos| self.entries.remove(pos));
57        self.remove_expired(now);
58
59        let (_, inserted, value) = entry?;
60        if now - inserted > self.max_duration {
61            debug!("Max duration expired for key: {k:?}");
62            None
63        } else {
64            Some(value)
65        }
66    }
67
68    /// Whether the map holds an element for the key which has not expired,
69    /// without taking it.
70    pub(super) fn contains_key(&self, k: &K) -> bool {
71        self.get(k).is_some()
72    }
73
74    /// The element for the key if it has not expired, without taking it.
75    pub(super) fn get(&self, k: &K) -> Option<&V> {
76        let now = Instant::now();
77        self.entries
78            .iter()
79            .find(|(key, inserted, _)| key == k && now - *inserted <= self.max_duration)
80            .map(|(_, _, value)| value)
81    }
82
83    /// Remove the expired elements, which may hold resources until then.
84    pub(super) fn sweep(&mut self) {
85        self.remove_expired(Instant::now());
86    }
87
88    fn position(&self, k: &K) -> Option<usize> {
89        self.entries.iter().position(|(key, _, _)| key == k)
90    }
91
92    /// Remove expired items from the oldest end. If the oldest one is not
93    /// expired, no entries are expired, so this can stop early.
94    fn remove_expired(&mut self, now: Instant) {
95        let mut expired_count = 0;
96        while let Some((_, inserted, _)) = self.entries.front() {
97            if now - *inserted > self.max_duration {
98                self.entries.pop_front();
99                expired_count += 1;
100            } else {
101                break;
102            }
103        }
104
105        if expired_count > 0 {
106            debug!("Removed {expired_count} timed out elements");
107        }
108    }
109
110    /// Whether the map holds no elements, also no expired ones.
111    pub(super) fn is_empty(&self) -> bool {
112        self.entries.is_empty()
113    }
114
115    /// Get the current number of items in the map
116    #[cfg(test)]
117    pub(super) fn len(&self) -> usize {
118        self.entries.len()
119    }
120}
121
122/// The most elements of a map, like the most streaming requests in flight
123/// of the kubelet and CRI-O. An element only holds what its client needs to
124/// start, a low limit would let clients which do not come fail the others
125/// until the elements expire.
126pub(crate) const DEFAULT_CAPACITY: usize = 1000;
127
128/// How long an element waits for its client by default. A client connects
129/// right after it got the URL, the kubelet keeps its streaming requests for
130/// a minute as well.
131const MAX_AGE: Duration = Duration::from_secs(60);
132
133impl<K, V> Default for BoundedMap<K, V> {
134    fn default() -> Self {
135        Self {
136            entries: VecDeque::new(),
137            capacity: DEFAULT_CAPACITY,
138            max_duration: MAX_AGE,
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use tokio::time::advance;
147
148    #[tokio::test(start_paused = true)]
149    async fn contains_key_leaves_the_element() {
150        let mut sut = BoundedMap::default();
151        assert_eq!(sut.insert(1, "one"), Ok(()));
152        assert!(sut.contains_key(&1));
153        assert!(!sut.contains_key(&2));
154        assert_eq!(sut.remove(&1), Some("one"));
155
156        assert_eq!(sut.insert(3, "three"), Ok(()));
157        advance(sut.max_duration + Duration::from_secs(1)).await;
158        assert!(!sut.contains_key(&3));
159    }
160
161    #[tokio::test(start_paused = true)]
162    async fn bounded_map_test() {
163        let mut sut = BoundedMap {
164            entries: VecDeque::new(),
165            capacity: 2,
166            max_duration: Duration::new(60 * 60, 0),
167        };
168
169        assert_eq!(sut.len(), 0);
170
171        // Insert first item should be fine
172        assert_eq!(sut.insert(0, 0), Ok(()));
173        assert_eq!(sut.len(), 1);
174
175        // Insert second item should be fine, removal should work as well
176        assert_eq!(sut.insert(1, 0), Ok(()));
177        assert_eq!(sut.len(), 2);
178        assert!(sut.remove(&1).is_some());
179        assert_eq!(sut.len(), 1);
180        assert_eq!(sut.insert(1, 0), Ok(()));
181
182        // A full map takes no third item, the valid ones stay.
183        assert_eq!(sut.insert(2, 2), Err(2));
184        assert_eq!(sut.len(), 2);
185        assert!(sut.remove(&2).is_none());
186
187        assert!(sut.remove(&0).is_some());
188        assert!(sut.remove(&1).is_some());
189
190        // Change the max age of the elements, all should be timed out
191        sut.max_duration = Duration::from_millis(100);
192        assert_eq!(sut.insert(0, 0), Ok(()));
193        assert_eq!(sut.insert(1, 0), Ok(()));
194        advance(Duration::from_millis(200)).await;
195
196        // A full map of expired items takes a new one.
197        assert_eq!(sut.insert(2, 0), Ok(()));
198        assert_eq!(sut.len(), 1);
199        assert!(sut.remove(&0).is_none());
200
201        // The last element should be also timed out if we wait
202        advance(Duration::from_millis(200)).await;
203        assert!(sut.remove(&2).is_none());
204
205        // Removing any key removes all expired elements.
206        assert_eq!(sut.insert(0, 0), Ok(()));
207        assert_eq!(sut.insert(1, 0), Ok(()));
208        advance(Duration::from_millis(200)).await;
209        assert!(sut.remove(&2).is_none());
210        assert_eq!(sut.len(), 0);
211
212        // So does a sweep.
213        assert_eq!(sut.insert(0, 0), Ok(()));
214        advance(Duration::from_millis(200)).await;
215        assert!(!sut.is_empty());
216        sut.sweep();
217        assert!(sut.is_empty());
218    }
219
220    #[tokio::test(start_paused = true)]
221    async fn expired_elements_are_not_handed_out() {
222        let mut sut = BoundedMap::default();
223        assert_eq!(sut.insert(1, "one"), Ok(()));
224        assert_eq!(sut.insert(2, "two"), Ok(()));
225        advance(sut.max_duration + Duration::from_secs(1)).await;
226        assert_eq!(sut.insert(3, "three"), Ok(()));
227
228        // The expired element is taken, but not handed out, the other expired
229        // one goes as well.
230        assert_eq!(sut.remove(&1), None);
231        assert_eq!(sut.len(), 1);
232        assert_eq!(sut.remove(&3), Some("three"));
233    }
234}