1use crate::{dirstatus::DirStatus, skew::SkewObservation};
8use rand::seq::IteratorRandom;
9use tor_dircommon::fallback::{FallbackDir, FallbackList};
10use tor_linkspec::HasRelayIds;
11use web_time_compat::{Duration, Instant};
12
13use crate::{PickGuardError, ids::FallbackId};
14use tor_basic_utils::iter::{FilterCount, IteratorExt as _};
15
16#[derive(Debug, Clone)]
18pub(crate) struct FallbackState {
19 fallbacks: Vec<Entry>,
23}
24
25#[derive(Debug, Clone)]
31pub(super) struct Entry {
32 fallback: FallbackDir,
34
35 status: DirStatus,
38 clock_skew: Option<SkewObservation>,
41}
42
43const FALLBACK_RETRY_FLOOR: Duration = Duration::from_secs(150);
47
48impl From<FallbackDir> for Entry {
49 fn from(fallback: FallbackDir) -> Self {
50 let status = DirStatus::new(FALLBACK_RETRY_FLOOR);
51 Entry {
52 fallback,
53 status,
54 clock_skew: None,
55 }
56 }
57}
58
59impl HasRelayIds for Entry {
60 fn identity(
61 &self,
62 key_type: tor_linkspec::RelayIdType,
63 ) -> Option<tor_linkspec::RelayIdRef<'_>> {
64 self.fallback.identity(key_type)
65 }
66}
67
68impl From<&FallbackList> for FallbackState {
69 fn from(list: &FallbackList) -> Self {
70 let mut fallbacks: Vec<Entry> = list.iter().map(|fb| fb.clone().into()).collect();
71 fallbacks.sort_by(|x, y| x.cmp_by_relay_ids(y));
72 fallbacks.dedup_by(|x, y| x.same_relay_ids(y));
73 FallbackState { fallbacks }
74 }
75}
76
77impl FallbackState {
78 pub(crate) fn choose<R: rand::Rng>(
80 &self,
81 rng: &mut R,
82 now: Instant,
83 filter: &crate::GuardFilter,
84 ) -> Result<&FallbackDir, PickGuardError> {
85 if self.fallbacks.is_empty() {
86 return Err(PickGuardError::NoCandidatesAvailable);
87 }
88
89 let mut running = FilterCount::default();
90 let mut filtered = FilterCount::default();
91
92 self.fallbacks
93 .iter()
94 .filter_cnt(&mut running, |ent| ent.status.usable_at(now))
95 .filter_cnt(&mut filtered, |ent| filter.permits(&ent.fallback))
96 .choose(rng)
97 .map(|ent| &ent.fallback)
98 .ok_or_else(|| PickGuardError::AllFallbacksDown {
99 retry_at: self.next_retry(),
100 running,
101 filtered,
102 })
103 }
104
105 fn next_retry(&self) -> Option<Instant> {
109 self.fallbacks
110 .iter()
111 .filter_map(|ent| ent.status.next_retriable())
112 .min()
113 }
114
115 fn get(&self, id: &FallbackId) -> Option<&Entry> {
117 match self.fallbacks.binary_search_by(|e| e.cmp_by_relay_ids(id)) {
118 Ok(idx) => Some(&self.fallbacks[idx]),
119 Err(_) => None,
120 }
121 }
122
123 fn get_mut(&mut self, id: &FallbackId) -> Option<&mut Entry> {
125 match self.fallbacks.binary_search_by(|e| e.cmp_by_relay_ids(id)) {
126 Ok(idx) => Some(&mut self.fallbacks[idx]),
127 Err(_) => None,
128 }
129 }
130
131 pub(crate) fn contains(&self, id: &FallbackId) -> bool {
133 self.get(id).is_some()
134 }
135
136 pub(crate) fn note_success(&mut self, id: &FallbackId) {
142 if let Some(entry) = self.get_mut(id) {
143 entry.status.note_success();
144 }
145 }
146
147 pub(crate) fn note_failure(&mut self, id: &FallbackId, now: Instant) {
150 if let Some(entry) = self.get_mut(id) {
151 entry.status.note_failure(now);
152 }
153 }
154
155 pub(crate) fn take_status_from(&mut self, other: FallbackState) {
157 use itertools::EitherOrBoth::Both;
158
159 itertools::merge_join_by(self.fallbacks.iter_mut(), other.fallbacks, |a, b| {
160 a.fallback.cmp_by_relay_ids(&b.fallback)
161 })
162 .for_each(|entry| {
163 if let Both(entry, other) = entry {
164 debug_assert!(entry.fallback.same_relay_ids(&other.fallback));
165 entry.status = other.status;
166 }
167 });
168 }
169
170 pub(crate) fn note_skew(&mut self, id: &FallbackId, observation: SkewObservation) {
172 if let Some(entry) = self.get_mut(id) {
173 entry.clock_skew = Some(observation);
174 }
175 }
176
177 pub(crate) fn skew_observations(&self) -> impl Iterator<Item = &SkewObservation> {
179 self.fallbacks
180 .iter()
181 .filter_map(|fb| fb.clock_skew.as_ref())
182 }
183}
184
185#[cfg(test)]
186mod test {
187 #![allow(clippy::bool_assert_comparison)]
189 #![allow(clippy::clone_on_copy)]
190 #![allow(clippy::dbg_macro)]
191 #![allow(clippy::mixed_attributes_style)]
192 #![allow(clippy::print_stderr)]
193 #![allow(clippy::print_stdout)]
194 #![allow(clippy::single_char_pattern)]
195 #![allow(clippy::unwrap_used)]
196 #![allow(clippy::unchecked_time_subtraction)]
197 #![allow(clippy::useless_vec)]
198 #![allow(clippy::needless_pass_by_value)]
199 use super::*;
202 use rand::Rng;
203 use tor_basic_utils::test_rng::testing_rng;
204 use web_time_compat::InstantExt;
205
206 fn rand_fb<R: Rng>(rng: &mut R) -> FallbackDir {
211 let ed: [u8; 32] = rng.random();
212 let rsa: [u8; 20] = rng.random();
213 let ip: u32 = rng.random();
214 let mut bld = FallbackDir::builder();
215 bld.ed_identity(ed.into())
216 .rsa_identity(rsa.into())
217 .orports()
218 .push(std::net::SocketAddrV4::new(ip.into(), 9090).into());
219 bld.build().unwrap()
220 }
221
222 #[test]
223 fn construct_fallback_set() {
224 use rand::seq::SliceRandom;
225 use std::cmp::Ordering as O;
226
227 let mut rng = testing_rng();
229 let fbs = vec![
230 rand_fb(&mut rng),
231 rand_fb(&mut rng),
232 rand_fb(&mut rng),
233 rand_fb(&mut rng),
234 ];
235 let fb_other = rand_fb(&mut rng);
236 let id_other = FallbackId::from_relay_ids(&fb_other);
237
238 let list: FallbackList = fbs.clone().into();
240 assert!(!list.is_empty());
241 assert_eq!(list.len(), 4);
242 let mut set: FallbackState = (&list).into();
243
244 assert_eq!(set.fallbacks.len(), 4);
246 assert_eq!(
247 set.fallbacks[0].cmp_by_relay_ids(&set.fallbacks[1]),
248 O::Less
249 );
250 assert_eq!(
251 set.fallbacks[1].cmp_by_relay_ids(&set.fallbacks[2]),
252 O::Less
253 );
254 assert_eq!(
255 set.fallbacks[2].cmp_by_relay_ids(&set.fallbacks[3]),
256 O::Less
257 );
258
259 for fb in fbs.iter() {
261 let id = FallbackId::from_relay_ids(fb);
262 assert_eq!(set.get_mut(&id).unwrap().cmp_by_relay_ids(&id), O::Equal);
263 }
264 assert!(set.get_mut(&id_other).is_none());
265
266 let mut redundant_fbs = fbs.clone();
268 redundant_fbs.extend(fbs.clone());
269 redundant_fbs.extend(fbs[0..2].iter().map(Clone::clone));
270 redundant_fbs[..].shuffle(&mut testing_rng());
271 let list2 = redundant_fbs.into();
272 assert_ne!(&list, &list2);
273 let set2: FallbackState = (&list2).into();
274
275 assert_eq!(set.fallbacks.len(), set2.fallbacks.len());
277 assert!(
278 set.fallbacks
279 .iter()
280 .zip(set2.fallbacks.iter())
281 .all(|(ent1, ent2)| ent1.same_relay_ids(ent2))
282 );
283 }
284
285 #[test]
286 fn set_choose() {
287 dbg!("X");
288
289 let mut rng = testing_rng();
290 let fbs = vec![
291 rand_fb(&mut rng),
292 rand_fb(&mut rng),
293 rand_fb(&mut rng),
294 rand_fb(&mut rng),
295 ];
296 let list: FallbackList = fbs.into();
297 let mut set: FallbackState = (&list).into();
298 let filter = crate::GuardFilter::unfiltered();
299
300 let mut counts = [0_usize; 4];
301 let now = Instant::get();
302 dbg!("A");
303 fn lookup_idx(set: &FallbackState, id: &impl HasRelayIds) -> Option<usize> {
304 set.fallbacks
305 .binary_search_by(|ent| ent.fallback.cmp_by_relay_ids(id))
306 .ok()
307 }
308 for _ in 0..100 {
310 let fb = set.choose(&mut rng, now, &filter).unwrap();
311 let idx = lookup_idx(&set, fb).unwrap();
312 counts[idx] += 1;
313 }
314 dbg!("B");
315 assert!(counts.iter().all(|v| *v > 0));
316
317 let ids: Vec<_> = set
319 .fallbacks
320 .iter()
321 .map(|ent| FallbackId::from_relay_ids(&ent.fallback))
322 .collect();
323 set.note_failure(&ids[2], now);
324 counts = [0; 4];
325 for _ in 0..100 {
326 let fb = set.choose(&mut rng, now, &filter).unwrap();
327 let idx = lookup_idx(&set, fb).unwrap();
328 counts[idx] += 1;
329 }
330 assert_eq!(counts.iter().filter(|v| **v > 0).count(), 3);
331 assert_eq!(counts[2], 0);
332
333 for id in ids.iter() {
335 set.note_failure(id, now);
336 }
337 assert!(matches!(
338 set.choose(&mut rng, now, &filter),
339 Err(PickGuardError::AllFallbacksDown { .. })
340 ));
341
342 let empty_set = FallbackState::from(&FallbackList::from(vec![]));
344 assert!(matches!(
345 empty_set.choose(&mut rng, now, &filter),
346 Err(PickGuardError::NoCandidatesAvailable)
347 ));
348
349 }
351
352 #[test]
353 fn test_status() {
354 let mut rng = testing_rng();
355 let fbs = vec![
356 rand_fb(&mut rng),
357 rand_fb(&mut rng),
358 rand_fb(&mut rng),
359 rand_fb(&mut rng),
360 ];
361 let list: FallbackList = fbs.clone().into();
362 let mut set: FallbackState = (&list).into();
363 let ids: Vec<_> = set
364 .fallbacks
365 .iter()
366 .map(|ent| FallbackId::from_relay_ids(&ent.fallback))
367 .collect();
368
369 let now = Instant::get();
370
371 assert!(set.next_retry().is_none());
373
374 set.note_failure(&ids[3], now);
376 assert!(set.fallbacks[3].status.next_retriable().unwrap() > now);
377 assert!(!set.fallbacks[3].status.usable_at(now));
378 assert_eq!(set.next_retry(), set.fallbacks[3].status.next_retriable());
379
380 set.note_failure(&ids[0], now);
382 assert!(set.fallbacks[0].status.next_retriable().unwrap() > now);
383 assert!(!set.fallbacks[0].status.usable_at(now));
384 assert_eq!(
385 set.next_retry().unwrap(),
386 std::cmp::min(
387 set.fallbacks[0].status.next_retriable().unwrap(),
388 set.fallbacks[3].status.next_retriable().unwrap()
389 )
390 );
391
392 set.note_success(&ids[0]);
394 assert!(set.fallbacks[0].status.next_retriable().is_none());
395 assert!(set.fallbacks[0].status.usable_at(now));
396
397 let mut fbs2: Vec<_> = fbs
399 .into_iter()
400 .filter(|fb| FallbackId::from_relay_ids(fb) != ids[2])
402 .collect();
403 let fbs_new = vec![rand_fb(&mut rng), rand_fb(&mut rng), rand_fb(&mut rng)];
405 fbs2.extend(fbs_new.clone());
406
407 let mut set2 = FallbackState::from(&FallbackList::from(fbs2.clone()));
408 set2.take_status_from(set); assert_eq!(set2.fallbacks.len(), 6); assert!(set2.get_mut(&ids[0]).unwrap().status.usable_at(now));
413 assert!(set2.get_mut(&ids[1]).unwrap().status.usable_at(now));
414 assert!(set2.get_mut(&ids[2]).is_none());
415 assert!(!set2.get_mut(&ids[3]).unwrap().status.usable_at(now));
416
417 for new_fb in fbs_new {
419 assert!(
420 set2.get_mut(&FallbackId::from_relay_ids(&new_fb))
421 .unwrap()
422 .status
423 .usable_at(now)
424 );
425 }
426 }
427}