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`].
56use std::num::{NonZeroU32, NonZeroU8};
7use std::time::Duration;
89use derive_builder::Builder;
10use serde::{Deserialize, Serialize};
11use tor_basic_utils::retry::RetryDelay;
12use tor_config::{impl_standard_builder, ConfigBuildError};
1314/// 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(
24type = "Option<u32>",
25 build = r#"build_nonzero(self.attempts, 3, "attempts")?"#
26)
27 )]
28attempts: NonZeroU32,
2930/// 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"))]
34initial_delay: Duration,
3536/// 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(
41type = "Option<u8>",
42 build = r#"build_nonzero(self.parallelism, 1, "parallelism")?"#
43)
44 )]
45parallelism: NonZeroU8,
46}
4748impl_standard_builder! { DownloadSchedule }
4950impl DownloadScheduleBuilder {
51/// Default value for retry_bootstrap in DownloadScheduleConfig.
52pub(crate) fn build_retry_bootstrap(&self) -> Result<DownloadSchedule, ConfigBuildError> {
53let 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 }
5960/// Default value for microdesc_bootstrap in DownloadScheduleConfig.
61pub(crate) fn build_retry_microdescs(&self) -> Result<DownloadSchedule, ConfigBuildError> {
62let mut bld = self.clone();
63 bld.attempts.get_or_insert(3);
64 bld.initial_delay
65 .get_or_insert_with(|| (Duration::new(1, 0)));
66 bld.parallelism.get_or_insert(4);
67 bld.build()
68 }
69}
7071/// Helper for building a NonZero* field
72fn build_nonzero<NZ, I>(
73 spec: Option<I>,
74 default: I,
75 field: &'static str,
76) -> Result<NZ, ConfigBuildError>
77where
78I: TryInto<NZ>,
79{
80 spec.unwrap_or(default).try_into().map_err(|_| {
81let field = field.into();
82let problem = "zero specified, but not permitted".to_string();
83 ConfigBuildError::Invalid { field, problem }
84 })
85}
8687impl DownloadSchedule {
88/// Return an iterator to use over all the supported attempts for
89 /// this configuration.
90pub fn attempts(&self) -> impl Iterator<Item = u32> {
910..(self.attempts.into())
92 }
9394/// Return the number of times that we're supposed to retry, according
95 /// to this DownloadSchedule.
96pub fn n_attempts(&self) -> u32 {
97self.attempts.into()
98 }
99100/// Return the number of parallel attempts that we're supposed to launch,
101 /// according to this DownloadSchedule.
102pub fn parallelism(&self) -> u8 {
103self.parallelism.into()
104 }
105106/// Return a RetryDelay object for this configuration.
107 ///
108 /// If the initial delay is longer than 32
109pub fn schedule(&self) -> RetryDelay {
110 RetryDelay::from_duration(self.initial_delay)
111 }
112}
113114#[cfg(test)]
115mod 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 @@ -->
129use super::*;
130use tor_basic_utils::test_rng::testing_rng;
131132#[test]
133fn config() {
134// default configuration is 3 tries, 1000 msec initial delay
135let cfg = DownloadSchedule::default();
136let one_sec = Duration::from_secs(1);
137let mut rng = testing_rng();
138139assert_eq!(cfg.n_attempts(), 3);
140let v: Vec<_> = cfg.attempts().collect();
141assert_eq!(&v[..], &[0, 1, 2]);
142143assert_eq!(cfg.initial_delay, one_sec);
144let mut sched = cfg.schedule();
145assert_eq!(sched.next_delay(&mut rng), one_sec);
146147// Try schedules with zeroes and show that they fail
148DownloadSchedule::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}