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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Configure timers for a timer for retrying a single failed fetch or object.
//!
//! For a more information on the algorithm, see
//! [`RetryDelay`].

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

use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use tor_basic_utils::retry::RetryDelay;
use tor_config::{impl_standard_builder, ConfigBuildError};

/// Configuration for how many times to retry a download, with what
/// frequency.
#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
pub struct DownloadSchedule {
    /// How many attempts to make before giving up?
    #[builder(
        setter(strip_option),
        field(
            type = "Option<u32>",
            build = r#"build_nonzero(self.attempts, 3, "attempts")?"#
        )
    )]
    attempts: NonZeroU32,

    /// The amount of time to delay after the first failure, and a
    /// lower-bound for future delays.
    #[builder(default = "Duration::from_millis(1000)")]
    #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
    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?
    #[builder(
        setter(strip_option),
        field(
            type = "Option<u8>",
            build = r#"build_nonzero(self.parallelism, 1, "parallelism")?"#
        )
    )]
    parallelism: NonZeroU8,
}

impl_standard_builder! { DownloadSchedule }

impl DownloadScheduleBuilder {
    /// Default value for retry_bootstrap in DownloadScheduleConfig.
    pub(crate) fn build_retry_bootstrap(&self) -> Result<DownloadSchedule, ConfigBuildError> {
        let mut bld = self.clone();
        bld.attempts.get_or_insert(128);
        bld.initial_delay.get_or_insert_with(|| Duration::new(1, 0));
        bld.parallelism.get_or_insert(1);
        bld.build()
    }

    /// Default value for microdesc_bootstrap in DownloadScheduleConfig.
    pub(crate) fn build_retry_microdescs(&self) -> Result<DownloadSchedule, ConfigBuildError> {
        let mut bld = self.clone();
        bld.attempts.get_or_insert(3);
        bld.initial_delay
            .get_or_insert_with(|| (Duration::new(1, 0)));
        bld.parallelism.get_or_insert(4);
        bld.build()
    }
}

/// Helper for building a NonZero* field
fn build_nonzero<NZ, I>(
    spec: Option<I>,
    default: I,
    field: &'static str,
) -> Result<NZ, ConfigBuildError>
where
    I: TryInto<NZ>,
{
    spec.unwrap_or(default).try_into().map_err(|_| {
        let field = field.into();
        let problem = "zero specified, but not permitted".to_string();
        ConfigBuildError::Invalid { field, problem }
    })
}

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

    /// Return the number of times that we're supposed to retry, according
    /// to this DownloadSchedule.
    pub fn n_attempts(&self) -> u32 {
        self.attempts.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 {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use tor_basic_utils::test_rng::testing_rng;

    #[test]
    fn config() {
        // default configuration is 3 tries, 1000 msec initial delay
        let cfg = DownloadSchedule::default();
        let one_sec = Duration::from_secs(1);
        let mut rng = testing_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 schedules with zeroes and show that they fail
        DownloadSchedule::builder()
            .attempts(0)
            .build()
            .expect_err("built with 0 retries");
        DownloadSchedule::builder()
            .parallelism(0)
            .build()
            .expect_err("built with 0 parallelism");
    }
}