1#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ReplicaSlot {
18 pub id: String,
21 pub last_seen_ns: u64,
24 pub acked_offset: u64,
27}
28
29#[derive(Debug, Default)]
35pub struct SlotTable {
36 slots: Vec<ReplicaSlot>,
37}
38
39impl SlotTable {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn len(&self) -> usize {
47 self.slots.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.slots.is_empty()
53 }
54
55 pub fn get(&self, id: &str) -> Option<&ReplicaSlot> {
57 self.slots.iter().find(|s| s.id == id)
58 }
59
60 pub fn touch_or_insert_unacked(&mut self, id: &str, now_ns: u64) {
69 if let Some(s) = self.slots.iter_mut().find(|s| s.id == id) {
70 s.last_seen_ns = now_ns;
71 return;
72 }
73 self.slots.push(ReplicaSlot {
74 id: id.to_string(),
75 last_seen_ns: now_ns,
76 acked_offset: 0,
77 });
78 }
79
80 pub fn iter(&self) -> impl Iterator<Item = &ReplicaSlot> {
82 self.slots.iter()
83 }
84
85 pub fn insert_or_touch(&mut self, id: &str, acked_offset: u64, now_ns: u64) {
92 if let Some(s) = self.slots.iter_mut().find(|s| s.id == id) {
93 s.last_seen_ns = now_ns;
94 if acked_offset > s.acked_offset {
95 s.acked_offset = acked_offset;
96 }
97 return;
98 }
99 self.slots.push(ReplicaSlot {
100 id: id.to_string(),
101 last_seen_ns: now_ns,
102 acked_offset,
103 });
104 }
105
106 pub fn remove(&mut self, id: &str) -> bool {
109 if let Some(pos) = self.slots.iter().position(|s| s.id == id) {
110 self.slots.swap_remove(pos);
111 true
112 } else {
113 false
114 }
115 }
116
117 pub fn expire(&mut self, now_ns: u64, window_ns: u64) -> Vec<String> {
121 let mut dropped = Vec::new();
122 let mut i = self.slots.len();
125 while i > 0 {
126 i -= 1;
127 let cutoff = self.slots[i].last_seen_ns.saturating_add(window_ns);
128 if cutoff <= now_ns {
129 let s = self.slots.swap_remove(i);
130 dropped.push(s.id);
131 }
132 }
133 dropped
134 }
135
136 pub fn min_acked_offset(&self) -> Option<u64> {
140 self.slots.iter().map(|s| s.acked_offset).min()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn fresh_table_is_empty() {
150 let t = SlotTable::new();
151 assert!(t.is_empty());
152 assert_eq!(t.len(), 0);
153 assert_eq!(t.min_acked_offset(), None);
154 }
155
156 #[test]
157 fn insert_then_get_returns_the_slot() {
158 let mut t = SlotTable::new();
159 t.insert_or_touch("a", 5, 100);
160 let s = t.get("a").unwrap();
161 assert_eq!(s.id, "a");
162 assert_eq!(s.acked_offset, 5);
163 assert_eq!(s.last_seen_ns, 100);
164 assert_eq!(t.len(), 1);
165 }
166
167 #[test]
168 fn touch_advances_last_seen_and_acked_offset() {
169 let mut t = SlotTable::new();
170 t.insert_or_touch("a", 5, 100);
171 t.insert_or_touch("a", 9, 200);
172 let s = t.get("a").unwrap();
173 assert_eq!(s.acked_offset, 9);
174 assert_eq!(s.last_seen_ns, 200);
175 assert_eq!(t.len(), 1);
177 }
178
179 #[test]
180 fn touch_with_lower_acked_offset_keeps_the_higher_one() {
181 let mut t = SlotTable::new();
182 t.insert_or_touch("a", 10, 100);
183 t.insert_or_touch("a", 7, 200);
185 let s = t.get("a").unwrap();
186 assert_eq!(s.acked_offset, 10);
187 assert_eq!(s.last_seen_ns, 200, "last_seen still advances");
188 }
189
190 #[test]
191 fn remove_existing_returns_true_and_drops_slot() {
192 let mut t = SlotTable::new();
193 t.insert_or_touch("a", 1, 100);
194 assert!(t.remove("a"));
195 assert!(t.is_empty());
196 assert_eq!(t.get("a"), None);
197 }
198
199 #[test]
200 fn remove_missing_returns_false() {
201 let mut t = SlotTable::new();
202 assert!(!t.remove("missing"));
203 }
204
205 #[test]
206 fn expire_drops_slots_past_window() {
207 let mut t = SlotTable::new();
208 t.insert_or_touch("old", 1, 100);
209 t.insert_or_touch("fresh", 1, 500);
210 let dropped = t.expire(350, 200);
213 assert_eq!(dropped, vec!["old".to_string()]);
214 assert_eq!(t.len(), 1);
215 assert!(t.get("fresh").is_some());
216 }
217
218 #[test]
219 fn expire_when_nothing_expires_returns_empty() {
220 let mut t = SlotTable::new();
221 t.insert_or_touch("a", 1, 1000);
222 let dropped = t.expire(1100, 500); assert!(dropped.is_empty());
224 assert_eq!(t.len(), 1);
225 }
226
227 #[test]
228 fn expire_with_overflow_window_saturates_does_not_panic() {
229 let mut t = SlotTable::new();
235 t.insert_or_touch("a", 1, u64::MAX - 10);
236 let dropped = t.expire(u64::MAX - 1, u64::MAX);
237 assert!(dropped.is_empty());
238 assert_eq!(t.len(), 1);
239 }
240
241 #[test]
242 fn min_acked_offset_returns_the_floor() {
243 let mut t = SlotTable::new();
244 t.insert_or_touch("a", 7, 100);
245 t.insert_or_touch("b", 3, 100);
246 t.insert_or_touch("c", 12, 100);
247 assert_eq!(t.min_acked_offset(), Some(3));
248 }
249
250 #[test]
251 fn touch_or_insert_unacked_never_advances_acked() {
252 let mut t = SlotTable::new();
253 t.insert_or_touch("a", 7, 100);
254 t.touch_or_insert_unacked("a", 200);
256 let s = t.get("a").unwrap();
257 assert_eq!(s.last_seen_ns, 200);
258 assert_eq!(s.acked_offset, 7, "close must not write sent-as-acked");
259 t.touch_or_insert_unacked("b", 300);
261 assert_eq!(t.get("b").unwrap().acked_offset, 0);
262 }
263
264 #[test]
265 fn iter_visits_every_slot() {
266 let mut t = SlotTable::new();
267 t.insert_or_touch("a", 1, 100);
268 t.insert_or_touch("b", 2, 100);
269 let mut ids: Vec<_> = t.iter().map(|s| s.id.clone()).collect();
270 ids.sort();
271 assert_eq!(ids, vec!["a".to_string(), "b".to_string()]);
272 }
273
274 #[test]
275 fn expire_all_when_window_zero() {
276 let mut t = SlotTable::new();
277 t.insert_or_touch("a", 1, 100);
278 t.insert_or_touch("b", 1, 100);
279 let dropped = t.expire(100, 0);
280 assert_eq!(dropped.len(), 2);
281 assert!(t.is_empty());
282 }
283}