-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
…ponential)
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
use crate::{ | ||
api::Api, | ||
network::{DnsResolver, EmailTransport}, | ||
scheduler::{SchedulerJobMetadata, SchedulerJobRetryState, SchedulerJobRetryStrategy}, | ||
}; | ||
use std::ops::Add; | ||
use time::OffsetDateTime; | ||
use tokio_cron_scheduler::JobId; | ||
|
||
pub struct SchedulerApiExt<'a, DR: DnsResolver, ET: EmailTransport> { | ||
api: &'a Api<DR, ET>, | ||
} | ||
|
||
impl<'a, DR: DnsResolver, ET: EmailTransport> SchedulerApiExt<'a, DR, ET> { | ||
/// Creates Scheduler API. | ||
pub fn new(api: &'a Api<DR, ET>) -> Self { | ||
Self { api } | ||
} | ||
|
||
/// Tries to schedule a retry for a specified job. If retry is not possible, returns `None`. | ||
pub async fn schedule_retry( | ||
&self, | ||
job_id: JobId, | ||
retry_strategy: &SchedulerJobRetryStrategy, | ||
) -> anyhow::Result<Option<SchedulerJobRetryState>> { | ||
let db = &self.api.db; | ||
let SchedulerJobMetadata { job_type, retry } = | ||
db.get_scheduler_job_meta(job_id).await?.ok_or_else(|| { | ||
anyhow::anyhow!( | ||
"Could not find a job state for a scheduler job ('{}').", | ||
job_id | ||
) | ||
})?; | ||
|
||
let retry_attempts = retry | ||
.map(|retry_state| retry_state.attempts) | ||
.unwrap_or_default(); | ||
// Check if retry is possible. | ||
let retry_state = if retry_attempts >= retry_strategy.max_attempts() { | ||
log::warn!( | ||
"Retry limit reached ('{}') for a scheduler job ('{job_id}').", | ||
retry_attempts | ||
); | ||
None | ||
} else { | ||
let retry_interval = retry_strategy.interval(retry_attempts); | ||
log::debug!( | ||
"Scheduling a retry for job ('{job_id}') in {}.", | ||
humantime::format_duration(retry_interval), | ||
); | ||
|
||
Some(SchedulerJobRetryState { | ||
attempts: retry_attempts + 1, | ||
next_at: OffsetDateTime::now_utc().add(retry_interval), | ||
}) | ||
}; | ||
|
||
db.update_scheduler_job_meta( | ||
job_id, | ||
SchedulerJobMetadata { | ||
job_type, | ||
retry: retry_state, | ||
}, | ||
) | ||
.await?; | ||
|
||
Ok(retry_state) | ||
} | ||
} | ||
|
||
impl<DR: DnsResolver, ET: EmailTransport> Api<DR, ET> { | ||
/// Returns an API to work with scheduler jobs. | ||
pub fn scheduler(&self) -> SchedulerApiExt<DR, ET> { | ||
SchedulerApiExt::new(self) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::{ | ||
scheduler::{SchedulerJob, SchedulerJobMetadata, SchedulerJobRetryStrategy}, | ||
tests::mock_api, | ||
}; | ||
use std::{ops::Add, time::Duration}; | ||
use time::OffsetDateTime; | ||
use tokio_cron_scheduler::{CronJob, JobStored, JobStoredData, JobType}; | ||
use uuid::uuid; | ||
|
||
#[tokio::test] | ||
async fn properly_schedules_retry() -> anyhow::Result<()> { | ||
let api = mock_api().await?; | ||
let scheduler = api.scheduler(); | ||
|
||
let job_id = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); | ||
let job = JobStoredData { | ||
id: Some(job_id.into()), | ||
last_updated: Some(946720800u64), | ||
last_tick: Some(946720700u64), | ||
next_tick: 946720900u64, | ||
count: 3, | ||
job_type: JobType::Cron as i32, | ||
extra: SchedulerJobMetadata::new(SchedulerJob::NotificationsSend).try_into()?, | ||
ran: true, | ||
stopped: false, | ||
job: Some(JobStored::CronJob(CronJob { | ||
schedule: "0 0 0 1 1 * *".to_string(), | ||
})), | ||
}; | ||
|
||
api.db.upsert_scheduler_job(&job).await?; | ||
|
||
let now = OffsetDateTime::now_utc(); | ||
let retry_state = scheduler | ||
.schedule_retry( | ||
job_id, | ||
&SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(120), | ||
max_attempts: 2, | ||
}, | ||
) | ||
.await? | ||
.unwrap(); | ||
assert_eq!(retry_state.attempts, 1); | ||
assert!(retry_state.next_at >= now.add(Duration::from_secs(120))); | ||
|
||
let retry_state = scheduler | ||
.schedule_retry( | ||
job_id, | ||
&SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(120), | ||
max_attempts: 2, | ||
}, | ||
) | ||
.await? | ||
.unwrap(); | ||
assert_eq!(retry_state.attempts, 2); | ||
assert!(retry_state.next_at >= now.add(Duration::from_secs(120))); | ||
|
||
let retry_state = scheduler | ||
.schedule_retry( | ||
job_id, | ||
&SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(120), | ||
max_attempts: 2, | ||
}, | ||
) | ||
.await?; | ||
assert!(retry_state.is_none()); | ||
|
||
Ok(()) | ||
} | ||
} |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
use crate::scheduler::{SchedulerJob, SchedulerJobMetadata}; | ||
use tokio_cron_scheduler::{Job, JobStoredData}; | ||
|
||
pub trait JobExt { | ||
/// Populates job's `extra` field with the job metadata that includes type. | ||
fn set_job_type(&mut self, job_type: SchedulerJob) -> anyhow::Result<()>; | ||
} | ||
|
||
impl JobExt for Job { | ||
/// Populates job's `extra` field with the job metadata that includes type. | ||
fn set_job_type(&mut self, job_type: SchedulerJob) -> anyhow::Result<()> { | ||
let job_data = self.job_data()?; | ||
self.set_job_data(JobStoredData { | ||
extra: SchedulerJobMetadata::new(job_type).try_into()?, | ||
..job_data | ||
})?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::JobExt; | ||
use crate::scheduler::{SchedulerJob, SchedulerJobMetadata}; | ||
use std::time::Duration; | ||
use tokio_cron_scheduler::{Job, JobStoredData}; | ||
|
||
#[tokio::test] | ||
async fn can_set_job_type() -> anyhow::Result<()> { | ||
let mut job = Job::new_one_shot(Duration::from_secs(10), |_, _| {})?; | ||
let original_job_data = job.job_data()?; | ||
assert!(original_job_data.extra.is_empty()); | ||
|
||
job.set_job_type(SchedulerJob::WebPageTrackersSchedule)?; | ||
|
||
let job_data = job.job_data()?; | ||
assert_eq!( | ||
SchedulerJobMetadata::try_from(job_data.extra.as_slice())?, | ||
SchedulerJobMetadata::new(SchedulerJob::WebPageTrackersSchedule) | ||
); | ||
|
||
job.set_job_type(SchedulerJob::NotificationsSend)?; | ||
|
||
let job_data = job.job_data()?; | ||
assert_eq!( | ||
SchedulerJobMetadata::try_from(job_data.extra.as_slice())?, | ||
SchedulerJobMetadata::new(SchedulerJob::NotificationsSend) | ||
); | ||
|
||
// Other fields should not be affected. | ||
assert_eq!( | ||
job_data, | ||
JobStoredData { | ||
extra: Vec::try_from(SchedulerJobMetadata::new(SchedulerJob::NotificationsSend))?, | ||
..original_job_data | ||
} | ||
); | ||
|
||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
use cron::Schedule; | ||
use std::time::Duration; | ||
|
||
pub trait ScheduleExt { | ||
/// Returns the minimum interval between occurrences. | ||
fn min_interval(&self) -> anyhow::Result<Duration>; | ||
} | ||
|
||
impl ScheduleExt for Schedule { | ||
/// Returns the minimum interval between occurrences. To calculate it, we take the first 10 | ||
/// upcoming occurrences and calculate the interval between each of them. Then we take the | ||
/// smallest interval. | ||
fn min_interval(&self) -> anyhow::Result<Duration> { | ||
let mut minimum_interval = Duration::MAX; | ||
let next_occurrences = self.upcoming(chrono::Utc).take(100).collect::<Vec<_>>(); | ||
for (index, occurrence) in next_occurrences.iter().enumerate().skip(1) { | ||
let interval = (*occurrence - next_occurrences[index - 1]).to_std()?; | ||
if interval < minimum_interval { | ||
minimum_interval = interval; | ||
} | ||
} | ||
|
||
Ok(minimum_interval) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::ScheduleExt; | ||
use cron::Schedule; | ||
use std::{str::FromStr, time::Duration}; | ||
|
||
#[test] | ||
fn can_calculate_min_interval() -> anyhow::Result<()> { | ||
let schedule = Schedule::from_str("0 * * * * * *")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(60)); | ||
|
||
let schedule = Schedule::from_str("0 0 * * * * *")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(3600)); | ||
let schedule = Schedule::from_str("@hourly")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(3600)); | ||
|
||
let schedule = Schedule::from_str("0 0 0 * * * *")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(24 * 3600)); | ||
let schedule = Schedule::from_str("@daily")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(24 * 3600)); | ||
|
||
let schedule = Schedule::from_str("0 0 0 * * 1 *")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(7 * 24 * 3600)); | ||
let schedule = Schedule::from_str("@weekly")?; | ||
assert_eq!(schedule.min_interval()?, Duration::from_secs(7 * 24 * 3600)); | ||
|
||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
use crate::scheduler::SchedulerJobRetryStrategy; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
/// Represents a job configuration that can be scheduled. | ||
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct SchedulerJobConfig { | ||
/// Defines a schedule for the job. | ||
pub schedule: String, | ||
/// Defines a retry strategy for the job. | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
pub retry_strategy: Option<SchedulerJobRetryStrategy>, | ||
/// Indicates whether the job result should result into a notification. If retry strategy is | ||
/// defined, the error notification will be sent only if the job fails after all the retries. | ||
pub notifications: bool, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
use crate::scheduler::{SchedulerJob, SchedulerJobRetryState}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
/// Secutils.dev specific metadata of the scheduler job. | ||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, PartialEq, Eq)] | ||
pub struct SchedulerJobMetadata { | ||
/// The type of the job. | ||
pub job_type: SchedulerJob, | ||
/// The state of the job if it is being retried. | ||
pub retry: Option<SchedulerJobRetryState>, | ||
} | ||
|
||
impl SchedulerJobMetadata { | ||
/// Create a new job state without retry state. | ||
pub fn new(job_type: SchedulerJob) -> Self { | ||
Self { | ||
job_type, | ||
retry: None, | ||
} | ||
} | ||
} | ||
|
||
impl TryFrom<&[u8]> for SchedulerJobMetadata { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { | ||
Ok(postcard::from_bytes(value)?) | ||
} | ||
} | ||
|
||
impl TryFrom<SchedulerJobMetadata> for Vec<u8> { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(value: SchedulerJobMetadata) -> Result<Self, Self::Error> { | ||
Ok(postcard::to_stdvec(&value)?) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::SchedulerJob; | ||
use crate::scheduler::{SchedulerJobMetadata, SchedulerJobRetryState}; | ||
use insta::assert_debug_snapshot; | ||
use time::OffsetDateTime; | ||
|
||
#[test] | ||
fn properly_creates_metadata() -> anyhow::Result<()> { | ||
assert_eq!( | ||
SchedulerJobMetadata::new(SchedulerJob::WebPageTrackersSchedule), | ||
SchedulerJobMetadata { | ||
job_type: SchedulerJob::WebPageTrackersSchedule, | ||
retry: None | ||
} | ||
); | ||
|
||
assert_eq!( | ||
SchedulerJobMetadata::new(SchedulerJob::NotificationsSend), | ||
SchedulerJobMetadata { | ||
job_type: SchedulerJob::NotificationsSend, | ||
retry: None | ||
} | ||
); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn serialize() -> anyhow::Result<()> { | ||
assert_eq!( | ||
Vec::try_from(SchedulerJobMetadata::new( | ||
SchedulerJob::WebPageTrackersSchedule | ||
))?, | ||
vec![1, 0] | ||
); | ||
assert_eq!( | ||
Vec::try_from(SchedulerJobMetadata { | ||
job_type: SchedulerJob::WebPageTrackersSchedule, | ||
retry: Some(SchedulerJobRetryState { | ||
attempts: 10, | ||
next_at: OffsetDateTime::from_unix_timestamp(946720800)?, | ||
}) | ||
})?, | ||
vec![1, 1, 10, 160, 31, 1, 10, 0, 0, 0, 0, 0, 0] | ||
); | ||
|
||
assert_eq!( | ||
Vec::try_from(SchedulerJobMetadata::new(SchedulerJob::NotificationsSend))?, | ||
vec![3, 0] | ||
); | ||
assert_eq!( | ||
Vec::try_from(SchedulerJobMetadata { | ||
job_type: SchedulerJob::NotificationsSend, | ||
retry: Some(SchedulerJobRetryState { | ||
attempts: 10, | ||
next_at: OffsetDateTime::from_unix_timestamp(946720800)?, | ||
}) | ||
})?, | ||
vec![3, 1, 10, 160, 31, 1, 10, 0, 0, 0, 0, 0, 0] | ||
); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn deserialize() -> anyhow::Result<()> { | ||
assert_eq!( | ||
SchedulerJobMetadata::try_from([1, 0].as_ref())?, | ||
SchedulerJobMetadata::new(SchedulerJob::WebPageTrackersSchedule) | ||
); | ||
|
||
assert_eq!( | ||
SchedulerJobMetadata::try_from([1, 1, 10, 160, 31, 1, 10, 0, 0, 0, 0, 0, 0].as_ref())?, | ||
SchedulerJobMetadata { | ||
job_type: SchedulerJob::WebPageTrackersSchedule, | ||
retry: Some(SchedulerJobRetryState { | ||
attempts: 10, | ||
next_at: OffsetDateTime::from_unix_timestamp(946720800)?, | ||
}) | ||
} | ||
); | ||
|
||
assert_eq!( | ||
SchedulerJobMetadata::try_from([3, 0].as_ref())?, | ||
SchedulerJobMetadata::new(SchedulerJob::NotificationsSend) | ||
); | ||
|
||
assert_eq!( | ||
SchedulerJobMetadata::try_from([3, 1, 10, 160, 31, 1, 10, 0, 0, 0, 0, 0, 0].as_ref())?, | ||
SchedulerJobMetadata { | ||
job_type: SchedulerJob::NotificationsSend, | ||
retry: Some(SchedulerJobRetryState { | ||
attempts: 10, | ||
next_at: OffsetDateTime::from_unix_timestamp(946720800)?, | ||
}) | ||
} | ||
); | ||
|
||
assert_debug_snapshot!(SchedulerJobMetadata::try_from([4].as_ref()), @r###" | ||
Err( | ||
SerdeDeCustom, | ||
) | ||
"###); | ||
|
||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use time::OffsetDateTime; | ||
|
||
/// Describes the state of a job that is being retried. | ||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, PartialEq, Eq)] | ||
pub struct SchedulerJobRetryState { | ||
/// How many times the job has been retried. | ||
pub attempts: u32, | ||
/// The time at which the job will be retried. | ||
pub next_at: OffsetDateTime, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use serde_with::{serde_as, DurationMilliSeconds}; | ||
use std::time::Duration; | ||
|
||
#[serde_as] | ||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, PartialEq, Eq)] | ||
#[serde(rename_all = "camelCase")] | ||
#[serde(tag = "type")] | ||
pub enum SchedulerJobRetryStrategy { | ||
/// The job will be retried with a constant interval (1s -> 1s -> 1s). | ||
#[serde(rename_all = "camelCase")] | ||
Constant { | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
interval: Duration, | ||
max_attempts: u32, | ||
}, | ||
/// The job will be retried with an exponential interval (1s -> 2s -> 4s -> 8s). | ||
#[serde(rename_all = "camelCase")] | ||
Exponential { | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
initial_interval: Duration, | ||
multiplier: u32, | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
max_interval: Duration, | ||
max_attempts: u32, | ||
}, | ||
/// The job will be retried with a linear interval (1s -> 2s -> 3s). | ||
#[serde(rename_all = "camelCase")] | ||
Linear { | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
initial_interval: Duration, | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
increment: Duration, | ||
#[serde_as(as = "DurationMilliSeconds<u64>")] | ||
max_interval: Duration, | ||
max_attempts: u32, | ||
}, | ||
} | ||
|
||
impl SchedulerJobRetryStrategy { | ||
/// Calculates the interval for the next retry attempt. | ||
pub fn interval(&self, attempt: u32) -> Duration { | ||
match self { | ||
Self::Constant { interval, .. } => *interval, | ||
Self::Exponential { | ||
initial_interval, | ||
multiplier, | ||
max_interval, | ||
.. | ||
} => multiplier | ||
.checked_pow(attempt) | ||
.and_then(|multiplier| initial_interval.checked_mul(multiplier)) | ||
.map(|interval| interval.min(*max_interval)) | ||
.unwrap_or_else(|| *max_interval), | ||
Self::Linear { | ||
initial_interval, | ||
increment, | ||
max_interval, | ||
.. | ||
} => increment | ||
.checked_mul(attempt) | ||
.and_then(|increment| initial_interval.checked_add(increment)) | ||
.map(|interval| interval.min(*max_interval)) | ||
.unwrap_or_else(|| *max_interval), | ||
} | ||
} | ||
|
||
/// Returns the maximum number of attempts. | ||
pub fn max_attempts(&self) -> u32 { | ||
match self { | ||
Self::Constant { max_attempts, .. } => *max_attempts, | ||
Self::Exponential { max_attempts, .. } => *max_attempts, | ||
Self::Linear { max_attempts, .. } => *max_attempts, | ||
} | ||
} | ||
|
||
/// Returns the minimum retry interval. | ||
pub fn min_interval(&self) -> &Duration { | ||
match self { | ||
Self::Constant { interval, .. } => interval, | ||
Self::Exponential { | ||
initial_interval, .. | ||
} | ||
| Self::Linear { | ||
initial_interval, .. | ||
} => initial_interval, | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::SchedulerJobRetryStrategy; | ||
use std::time::Duration; | ||
|
||
#[test] | ||
fn properly_detects_max_number_of_attempts() { | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(1), | ||
max_attempts: 10, | ||
} | ||
.max_attempts(), | ||
10 | ||
); | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Exponential { | ||
initial_interval: Duration::from_secs(1), | ||
multiplier: 2, | ||
max_interval: Duration::from_secs(10), | ||
max_attempts: 15, | ||
} | ||
.max_attempts(), | ||
15 | ||
); | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Linear { | ||
initial_interval: Duration::from_secs(1), | ||
increment: Duration::from_secs(1), | ||
max_interval: Duration::from_secs(10), | ||
max_attempts: 20, | ||
} | ||
.max_attempts(), | ||
20 | ||
); | ||
} | ||
|
||
#[test] | ||
fn properly_detects_min_interval() { | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(1), | ||
max_attempts: 10, | ||
} | ||
.min_interval(), | ||
&Duration::from_secs(1) | ||
); | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Exponential { | ||
initial_interval: Duration::from_secs(2), | ||
multiplier: 2, | ||
max_interval: Duration::from_secs(10), | ||
max_attempts: 15, | ||
} | ||
.min_interval(), | ||
&Duration::from_secs(2) | ||
); | ||
assert_eq!( | ||
SchedulerJobRetryStrategy::Linear { | ||
initial_interval: Duration::from_secs(3), | ||
increment: Duration::from_secs(1), | ||
max_interval: Duration::from_secs(10), | ||
max_attempts: 20, | ||
} | ||
.min_interval(), | ||
&Duration::from_secs(3) | ||
); | ||
} | ||
|
||
#[test] | ||
fn properly_calculates_constant_interval() { | ||
let retry_strategy = SchedulerJobRetryStrategy::Constant { | ||
interval: Duration::from_secs(1), | ||
max_attempts: 10, | ||
}; | ||
assert_eq!(retry_strategy.interval(0), Duration::from_secs(1)); | ||
assert_eq!(retry_strategy.interval(1), Duration::from_secs(1)); | ||
assert_eq!(retry_strategy.interval(2), Duration::from_secs(1)); | ||
assert_eq!(retry_strategy.interval(u32::MAX), Duration::from_secs(1)); | ||
} | ||
|
||
#[test] | ||
fn properly_calculates_linear_interval() { | ||
let retry_strategy = SchedulerJobRetryStrategy::Linear { | ||
initial_interval: Duration::from_secs(1), | ||
increment: Duration::from_secs(1), | ||
max_interval: Duration::from_secs(5), | ||
max_attempts: 10, | ||
}; | ||
assert_eq!(retry_strategy.interval(0), Duration::from_secs(1)); | ||
assert_eq!(retry_strategy.interval(1), Duration::from_secs(2)); | ||
assert_eq!(retry_strategy.interval(2), Duration::from_secs(3)); | ||
assert_eq!(retry_strategy.interval(3), Duration::from_secs(4)); | ||
assert_eq!(retry_strategy.interval(4), Duration::from_secs(5)); | ||
assert_eq!(retry_strategy.interval(5), Duration::from_secs(5)); | ||
assert_eq!(retry_strategy.interval(6), Duration::from_secs(5)); | ||
assert_eq!(retry_strategy.interval(100), Duration::from_secs(5)); | ||
assert_eq!(retry_strategy.interval(u32::MAX), Duration::from_secs(5)); | ||
} | ||
|
||
#[test] | ||
fn properly_calculates_exponential_interval() { | ||
let retry_strategy = SchedulerJobRetryStrategy::Exponential { | ||
initial_interval: Duration::from_secs(1), | ||
multiplier: 2, | ||
max_interval: Duration::from_secs(100), | ||
max_attempts: 10, | ||
}; | ||
assert_eq!(retry_strategy.interval(0), Duration::from_secs(1)); | ||
assert_eq!(retry_strategy.interval(1), Duration::from_secs(2)); | ||
assert_eq!(retry_strategy.interval(2), Duration::from_secs(4)); | ||
assert_eq!(retry_strategy.interval(3), Duration::from_secs(8)); | ||
assert_eq!(retry_strategy.interval(4), Duration::from_secs(16)); | ||
assert_eq!(retry_strategy.interval(5), Duration::from_secs(32)); | ||
assert_eq!(retry_strategy.interval(6), Duration::from_secs(64)); | ||
assert_eq!(retry_strategy.interval(7), Duration::from_secs(100)); | ||
assert_eq!(retry_strategy.interval(100), Duration::from_secs(100)); | ||
assert_eq!(retry_strategy.interval(u32::MAX), Duration::from_secs(100)); | ||
} | ||
} |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.