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};
#[derive(Debug, Builder, Copy, Clone, Eq, PartialEq)]
#[builder(build_fn(error = "ConfigBuildError"))]
#[builder(derive(Debug, Serialize, Deserialize))]
pub struct DownloadSchedule {
#[builder(
setter(strip_option),
field(
type = "Option<u32>",
build = r#"build_nonzero(self.attempts, 3, "attempts")?"#
)
)]
attempts: NonZeroU32,
#[builder(default = "Duration::from_millis(1000)")]
#[builder_field_attr(serde(default, with = "humantime_serde::option"))]
initial_delay: Duration,
#[builder(
setter(strip_option),
field(
type = "Option<u8>",
build = r#"build_nonzero(self.parallelism, 1, "parallelism")?"#
)
)]
parallelism: NonZeroU8,
}
impl_standard_builder! { DownloadSchedule }
impl DownloadScheduleBuilder {
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()
}
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()
}
}
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 {
pub fn attempts(&self) -> impl Iterator<Item = u32> {
0..(self.attempts.into())
}
pub fn n_attempts(&self) -> u32 {
self.attempts.into()
}
pub fn parallelism(&self) -> u8 {
self.parallelism.into()
}
pub fn schedule(&self) -> RetryDelay {
RetryDelay::from_duration(self.initial_delay)
}
}
#[cfg(test)]
mod test {
#![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)]
use super::*;
use tor_basic_utils::test_rng::testing_rng;
#[test]
fn config() {
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);
DownloadSchedule::builder()
.attempts(0)
.build()
.expect_err("built with 0 retries");
DownloadSchedule::builder()
.parallelism(0)
.build()
.expect_err("built with 0 parallelism");
}
}