1
//! Configure timers for a timer for retrying a single failed fetch or object.
2
//!
3
//! For a more information on the algorithm, see
4
//! [`RetryDelay`].
5

            
6
use std::num::{NonZeroU32, NonZeroU8};
7
use std::time::Duration;
8

            
9
use derive_builder::Builder;
10
use serde::{Deserialize, Serialize};
11
use tor_basic_utils::retry::RetryDelay;
12
use tor_config::{impl_standard_builder, ConfigBuildError};
13

            
14
/// Configuration for how many times to retry a download, with what
15
/// frequency.
16
20480
#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq)]
17
#[builder(build_fn(error = "ConfigBuildError"))]
18
#[builder(derive(Debug, Serialize, Deserialize))]
19
pub struct DownloadSchedule {
20
    /// How many attempts to make before giving up?
21
    #[builder(
22
        setter(strip_option),
23
        field(
24
            type = "Option<u32>",
25
            build = r#"build_nonzero(self.attempts, 3, "attempts")?"#
26
        )
27
    )]
28
    attempts: NonZeroU32,
29

            
30
    /// The amount of time to delay after the first failure, and a
31
    /// lower-bound for future delays.
32
    #[builder(default = "Duration::from_millis(1000)")]
33
    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
34
    initial_delay: Duration,
35

            
36
    /// When we want to download a bunch of these at a time, how many
37
    /// attempts should we try to launch at once?
38
    #[builder(
39
        setter(strip_option),
40
        field(
41
            type = "Option<u8>",
42
            build = r#"build_nonzero(self.parallelism, 1, "parallelism")?"#
43
        )
44
    )]
45
    parallelism: NonZeroU8,
46
}
47

            
48
impl_standard_builder! { DownloadSchedule }
49

            
50
impl DownloadScheduleBuilder {
51
    /// Default value for retry_bootstrap in DownloadScheduleConfig.
52
2467
    pub(crate) fn build_retry_bootstrap(&self) -> Result<DownloadSchedule, ConfigBuildError> {
53
2467
        let mut bld = self.clone();
54
2467
        bld.attempts.get_or_insert(128);
55
2559
        bld.initial_delay.get_or_insert_with(|| Duration::new(1, 0));
56
2467
        bld.parallelism.get_or_insert(1);
57
2467
        bld.build()
58
2467
    }
59

            
60
    /// Default value for microdesc_bootstrap in DownloadScheduleConfig.
61
2467
    pub(crate) fn build_retry_microdescs(&self) -> Result<DownloadSchedule, ConfigBuildError> {
62
2467
        let mut bld = self.clone();
63
2467
        bld.attempts.get_or_insert(3);
64
2467
        bld.initial_delay
65
2558
            .get_or_insert_with(|| (Duration::new(1, 0)));
66
2467
        bld.parallelism.get_or_insert(4);
67
2467
        bld.build()
68
2467
    }
69
}
70

            
71
/// Helper for building a NonZero* field
72
19770
fn build_nonzero<NZ, I>(
73
19770
    spec: Option<I>,
74
19770
    default: I,
75
19770
    field: &'static str,
76
19770
) -> Result<NZ, ConfigBuildError>
77
19770
where
78
19770
    I: TryInto<NZ>,
79
19770
{
80
19770
    spec.unwrap_or(default).try_into().map_err(|_| {
81
4
        let field = field.into();
82
4
        let problem = "zero specified, but not permitted".to_string();
83
4
        ConfigBuildError::Invalid { field, problem }
84
19770
    })
85
19770
}
86

            
87
impl DownloadSchedule {
88
    /// Return an iterator to use over all the supported attempts for
89
    /// this configuration.
90
4
    pub fn attempts(&self) -> impl Iterator<Item = u32> {
91
4
        0..(self.attempts.into())
92
4
    }
93

            
94
    /// Return the number of times that we're supposed to retry, according
95
    /// to this DownloadSchedule.
96
14
    pub fn n_attempts(&self) -> u32 {
97
14
        self.attempts.into()
98
14
    }
99

            
100
    /// Return the number of parallel attempts that we're supposed to launch,
101
    /// according to this DownloadSchedule.
102
12
    pub fn parallelism(&self) -> u8 {
103
12
        self.parallelism.into()
104
12
    }
105

            
106
    /// Return a RetryDelay object for this configuration.
107
    ///
108
    /// If the initial delay is longer than 32
109
4
    pub fn schedule(&self) -> RetryDelay {
110
4
        RetryDelay::from_duration(self.initial_delay)
111
4
    }
112
}
113

            
114
#[cfg(test)]
115
mod test {
116
    // @@ begin test lint list maintained by maint/add_warning @@
117
    #![allow(clippy::bool_assert_comparison)]
118
    #![allow(clippy::clone_on_copy)]
119
    #![allow(clippy::dbg_macro)]
120
    #![allow(clippy::mixed_attributes_style)]
121
    #![allow(clippy::print_stderr)]
122
    #![allow(clippy::print_stdout)]
123
    #![allow(clippy::single_char_pattern)]
124
    #![allow(clippy::unwrap_used)]
125
    #![allow(clippy::unchecked_duration_subtraction)]
126
    #![allow(clippy::useless_vec)]
127
    #![allow(clippy::needless_pass_by_value)]
128
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
129
    use super::*;
130
    use tor_basic_utils::test_rng::testing_rng;
131

            
132
    #[test]
133
    fn config() {
134
        // default configuration is 3 tries, 1000 msec initial delay
135
        let cfg = DownloadSchedule::default();
136
        let one_sec = Duration::from_secs(1);
137
        let mut rng = testing_rng();
138

            
139
        assert_eq!(cfg.n_attempts(), 3);
140
        let v: Vec<_> = cfg.attempts().collect();
141
        assert_eq!(&v[..], &[0, 1, 2]);
142

            
143
        assert_eq!(cfg.initial_delay, one_sec);
144
        let mut sched = cfg.schedule();
145
        assert_eq!(sched.next_delay(&mut rng), one_sec);
146

            
147
        // Try schedules with zeroes and show that they fail
148
        DownloadSchedule::builder()
149
            .attempts(0)
150
            .build()
151
            .expect_err("built with 0 retries");
152
        DownloadSchedule::builder()
153
            .parallelism(0)
154
            .build()
155
            .expect_err("built with 0 parallelism");
156
    }
157
}