tor_dirmgr/
retry.rs

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
6use std::num::{NonZeroU32, NonZeroU8};
7use std::time::Duration;
8
9use derive_builder::Builder;
10use serde::{Deserialize, Serialize};
11use tor_basic_utils::retry::RetryDelay;
12use tor_config::{impl_standard_builder, ConfigBuildError};
13
14/// Configuration for how many times to retry a download, with what
15/// frequency.
16#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq)]
17#[builder(build_fn(error = "ConfigBuildError"))]
18#[builder(derive(Debug, Serialize, Deserialize))]
19pub 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
48impl_standard_builder! { DownloadSchedule }
49
50impl DownloadScheduleBuilder {
51    /// Default value for retry_bootstrap in DownloadScheduleConfig.
52    pub(crate) fn build_retry_bootstrap(&self) -> Result<DownloadSchedule, ConfigBuildError> {
53        let mut bld = self.clone();
54        bld.attempts.get_or_insert(128);
55        bld.initial_delay.get_or_insert_with(|| Duration::new(1, 0));
56        bld.parallelism.get_or_insert(1);
57        bld.build()
58    }
59
60    /// Default value for microdesc_bootstrap in DownloadScheduleConfig.
61    pub(crate) fn build_retry_microdescs(&self) -> Result<DownloadSchedule, ConfigBuildError> {
62        let mut bld = self.clone();
63        bld.attempts.get_or_insert(3);
64        bld.initial_delay.get_or_insert_with(|| Duration::new(1, 0));
65        bld.parallelism.get_or_insert(4);
66        bld.build()
67    }
68}
69
70/// Helper for building a NonZero* field
71fn build_nonzero<NZ, I>(
72    spec: Option<I>,
73    default: I,
74    field: &'static str,
75) -> Result<NZ, ConfigBuildError>
76where
77    I: TryInto<NZ>,
78{
79    spec.unwrap_or(default).try_into().map_err(|_| {
80        let field = field.into();
81        let problem = "zero specified, but not permitted".to_string();
82        ConfigBuildError::Invalid { field, problem }
83    })
84}
85
86impl DownloadSchedule {
87    /// Return an iterator to use over all the supported attempts for
88    /// this configuration.
89    pub fn attempts(&self) -> impl Iterator<Item = u32> {
90        0..(self.attempts.into())
91    }
92
93    /// Return the number of times that we're supposed to retry, according
94    /// to this DownloadSchedule.
95    pub fn n_attempts(&self) -> u32 {
96        self.attempts.into()
97    }
98
99    /// Return the number of parallel attempts that we're supposed to launch,
100    /// according to this DownloadSchedule.
101    pub fn parallelism(&self) -> u8 {
102        self.parallelism.into()
103    }
104
105    /// Return a RetryDelay object for this configuration.
106    ///
107    /// If the initial delay is longer than 32
108    pub fn schedule(&self) -> RetryDelay {
109        RetryDelay::from_duration(self.initial_delay)
110    }
111}
112
113#[cfg(test)]
114mod test {
115    // @@ begin test lint list maintained by maint/add_warning @@
116    #![allow(clippy::bool_assert_comparison)]
117    #![allow(clippy::clone_on_copy)]
118    #![allow(clippy::dbg_macro)]
119    #![allow(clippy::mixed_attributes_style)]
120    #![allow(clippy::print_stderr)]
121    #![allow(clippy::print_stdout)]
122    #![allow(clippy::single_char_pattern)]
123    #![allow(clippy::unwrap_used)]
124    #![allow(clippy::unchecked_duration_subtraction)]
125    #![allow(clippy::useless_vec)]
126    #![allow(clippy::needless_pass_by_value)]
127    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
128    use super::*;
129    use tor_basic_utils::test_rng::testing_rng;
130
131    #[test]
132    fn config() {
133        // default configuration is 3 tries, 1000 msec initial delay
134        let cfg = DownloadSchedule::default();
135        let one_sec = Duration::from_secs(1);
136        let mut rng = testing_rng();
137
138        assert_eq!(cfg.n_attempts(), 3);
139        let v: Vec<_> = cfg.attempts().collect();
140        assert_eq!(&v[..], &[0, 1, 2]);
141
142        assert_eq!(cfg.initial_delay, one_sec);
143        let mut sched = cfg.schedule();
144        assert_eq!(sched.next_delay(&mut rng), one_sec);
145
146        // Try schedules with zeroes and show that they fail
147        DownloadSchedule::builder()
148            .attempts(0)
149            .build()
150            .expect_err("built with 0 retries");
151        DownloadSchedule::builder()
152            .parallelism(0)
153            .build()
154            .expect_err("built with 0 parallelism");
155    }
156}