1use 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#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq)]
17#[builder(build_fn(error = "ConfigBuildError"))]
18#[builder(derive(Debug, Serialize, Deserialize))]
19pub struct DownloadSchedule {
20 #[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 #[builder(default = "Duration::from_millis(1000)")]
33 #[builder_field_attr(serde(default, with = "humantime_serde::option"))]
34 initial_delay: Duration,
35
36 #[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 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 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
70fn 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 pub fn attempts(&self) -> impl Iterator<Item = u32> {
90 0..(self.attempts.into())
91 }
92
93 pub fn n_attempts(&self) -> u32 {
96 self.attempts.into()
97 }
98
99 pub fn parallelism(&self) -> u8 {
102 self.parallelism.into()
103 }
104
105 pub fn schedule(&self) -> RetryDelay {
109 RetryDelay::from_duration(self.initial_delay)
110 }
111}
112
113#[cfg(test)]
114mod test {
115 #![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 use super::*;
129 use tor_basic_utils::test_rng::testing_rng;
130
131 #[test]
132 fn config() {
133 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 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}