1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
//! Configure timers for a timer for retrying a single failed fetch or object.
//!
//! For a more information on the algorithm, see
//! [`RetryDelay`](tor_basic_utils::retry::RetryDelay).
use std::convert::TryInto;
use std::num::{NonZeroU32, NonZeroU8};
use std::time::Duration;
use serde::Deserialize;
use tor_basic_utils::retry::RetryDelay;
/// Configuration for how many times to retry a download, with what
/// frequency.
#[derive(Debug, Copy, Clone, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DownloadSchedule {
/// How many times to retry before giving up?
num_retries: NonZeroU32,
/// The amount of time to delay after the first failure, and a
/// lower-bound for future delays.
#[serde(with = "humantime_serde")]
initial_delay: Duration,
/// When we want to download a bunch of these at a time, how many
/// attempts should we try to launch at once?
#[serde(default = "default_parallelism")]
parallelism: NonZeroU8,
}
impl Default for DownloadSchedule {
fn default() -> Self {
DownloadSchedule::new(3, Duration::from_millis(1000), 1)
}
}
/// Return the default parallelism for DownloadSchedule.
fn default_parallelism() -> NonZeroU8 {
#![allow(clippy::unwrap_used)]
1.try_into().unwrap()
}
impl DownloadSchedule {
/// Create a new DownloadSchedule to control our logic for retrying
/// a given download.
///
/// The resulting configuration will always make at least one
/// attempt, and at most `attempts`. After a failure, it will
/// wait at least `initial_delay` before trying again.
#[allow(clippy::missing_panics_doc)] // can't really panic.
pub fn new(attempts: u32, initial_delay: Duration, parallelism: u8) -> Self {
// If unwrapping `1.try_into()` is not safe there are bigger problems
#![allow(clippy::unwrap_used)]
let num_retries = attempts
.try_into()
.unwrap_or_else(|_| 1.try_into().unwrap());
let parallelism = parallelism
.try_into()
.unwrap_or_else(|_| 1.try_into().unwrap());
DownloadSchedule {
num_retries,
initial_delay,
parallelism,
}
}
/// Return an iterator to use over all the supported attempts for
/// this configuration.
pub fn attempts(&self) -> impl Iterator<Item = u32> {
0..(self.num_retries.into())
}
/// Return the number of times that we're supposed to retry, according
/// to this DownloadSchedule.
pub fn n_attempts(&self) -> u32 {
self.num_retries.into()
}
/// Return the number of parallel attempts that we're supposed to launch,
/// according to this DownloadSchedule.
pub fn parallelism(&self) -> u8 {
self.parallelism.into()
}
/// Return a RetryDelay object for this configuration.
///
/// If the initial delay is longer than 32
pub fn schedule(&self) -> RetryDelay {
RetryDelay::from_duration(self.initial_delay)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn config() {
// default configuration is 3 tries, 1000 msec initial delay
let cfg = DownloadSchedule::default();
let one_sec = Duration::from_secs(1);
let zero_sec = Duration::from_secs(0);
let mut rng = rand::thread_rng();
assert_eq!(cfg.n_attempts(), 3);
let v: Vec<_> = cfg.attempts().collect();
assert_eq!(&v[..], &[0, 1, 2]);
assert_eq!(cfg.initial_delay, one_sec);
let mut sched = cfg.schedule();
assert_eq!(sched.next_delay(&mut rng), one_sec);
// Try a zero-attempt schedule, and have it get remapped to 1,1
let cfg = DownloadSchedule::new(0, zero_sec, 0);
assert_eq!(cfg.n_attempts(), 1);
assert_eq!(cfg.parallelism(), 1);
let v: Vec<_> = cfg.attempts().collect();
assert_eq!(&v[..], &[0]);
assert_eq!(cfg.initial_delay, zero_sec);
let mut sched = cfg.schedule();
assert_eq!(sched.next_delay(&mut rng), one_sec);
}
}
|