headless_lms_models/library/credit_registration/
backoff.rs1use chrono::TimeDelta;
5use headless_lms_utils::backoff::{exponential_backoff_secs, window_expired};
6
7use crate::prelude::*;
8use crate::suotar_api_calls::SuotarEndpoint;
9
10pub const SUBMIT_BASE_BACKOFF: TimeDelta = TimeDelta::minutes(1);
11pub const SUBMIT_MAX_BACKOFF: TimeDelta = TimeDelta::hours(6);
12pub const SUBMIT_MAX_RETRY_AGE: TimeDelta = TimeDelta::days(7);
14const VERIFY_WINDOW_START: TimeDelta = TimeDelta::minutes(270);
17const VERIFY_DENSE_WINDOW_END: TimeDelta = TimeDelta::minutes(630);
18const VERIFY_WINDOW_END: TimeDelta = TimeDelta::hours(29);
19pub const VERIFY_WINDOW_INTERVAL: TimeDelta = TimeDelta::minutes(30);
20const VERIFY_LATE_WINDOW_INTERVAL: TimeDelta = TimeDelta::hours(1);
21pub const VERIFY_BASE_BACKOFF: TimeDelta = TimeDelta::hours(2);
22pub const VERIFY_MAX_BACKOFF: TimeDelta = TimeDelta::hours(6);
23pub const VERIFY_MAX_AGE: TimeDelta = TimeDelta::days(14);
26pub const VERIFY_GIVE_UP_POLL: TimeDelta = TimeDelta::days(1);
27pub const JITTER_MAX: TimeDelta = TimeDelta::seconds(30);
28
29pub const UNCERTAIN_RECHECK: TimeDelta = TimeDelta::minutes(15);
32pub const UNCERTAIN_MAX_RECHECK: TimeDelta = TimeDelta::hours(6);
33pub const UNCERTAIN_ADMIN_AFTER: TimeDelta = TimeDelta::days(1);
36pub const SUOTAR_PENDING_WINDOW: TimeDelta = TimeDelta::hours(24);
39
40pub const PARTIAL_REGISTRATION_ADMIN_AFTER: TimeDelta = TimeDelta::days(3);
42pub const NOT_REGISTERED_REIMPORT_ADMIN_THRESHOLD: i32 = 3;
45
46pub const SUBMITTING_RECOVERY_GRACE: TimeDelta = TimeDelta::seconds(
49 SuotarEndpoint::ImportAttainments
50 .request_timeout()
51 .as_secs() as i64
52 + 15 * 60,
53);
54pub const RESOLVING_RECOVERY_GRACE: TimeDelta = TimeDelta::seconds(
58 SuotarEndpoint::ResolveEnrolments
59 .request_timeout()
60 .as_secs() as i64
61 + 10 * 60,
62);
63
64fn doubling(base: TimeDelta, max: TimeDelta, count: i32) -> TimeDelta {
65 TimeDelta::seconds(exponential_backoff_secs(
66 base.num_seconds(),
67 max.num_seconds(),
68 count,
69 ))
70}
71
72pub fn submit_backoff(retry_count: i32) -> TimeDelta {
74 doubling(SUBMIT_BASE_BACKOFF, SUBMIT_MAX_BACKOFF, retry_count)
75}
76
77pub fn verify_delay(submitted_at: Option<DateTime<Utc>>, now: DateTime<Utc>) -> TimeDelta {
81 let Some(submitted_at) = submitted_at else {
82 return VERIFY_WINDOW_INTERVAL;
83 };
84 let age = now - submitted_at;
85 if age < VERIFY_WINDOW_START {
86 VERIFY_WINDOW_START - age
87 } else if age < VERIFY_DENSE_WINDOW_END {
88 VERIFY_WINDOW_INTERVAL
89 } else if age < VERIFY_WINDOW_END {
90 VERIFY_LATE_WINDOW_INTERVAL
91 } else {
92 (age - VERIFY_WINDOW_END).clamp(VERIFY_BASE_BACKOFF, VERIFY_MAX_BACKOFF)
94 }
95}
96
97pub fn uncertain_recheck_delay(lookup_count: i32) -> TimeDelta {
99 doubling(UNCERTAIN_RECHECK, UNCERTAIN_MAX_RECHECK, lookup_count)
100}
101
102pub fn next_attempt_at(now: DateTime<Utc>, delay: TimeDelta) -> DateTime<Utc> {
104 headless_lms_utils::backoff::next_attempt_at(now, delay.num_seconds(), JITTER_MAX.num_seconds())
105}
106
107pub fn submit_window_expired(first_failed_at: Option<DateTime<Utc>>, now: DateTime<Utc>) -> bool {
108 window_expired(first_failed_at, now, SUBMIT_MAX_RETRY_AGE.num_seconds())
109}
110
111pub fn verify_window_expired(submitted_at: Option<DateTime<Utc>>, now: DateTime<Utc>) -> bool {
112 window_expired(submitted_at, now, VERIFY_MAX_AGE.num_seconds())
113}
114
115pub fn uncertain_needs_admin(submitted_at: Option<DateTime<Utc>>, now: DateTime<Utc>) -> bool {
118 window_expired(submitted_at, now, UNCERTAIN_ADMIN_AFTER.num_seconds())
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn backoff_doubles_and_then_stops_growing() {
127 assert_eq!(submit_backoff(0), SUBMIT_BASE_BACKOFF);
128 assert_eq!(submit_backoff(1), SUBMIT_BASE_BACKOFF * 2);
129 assert_eq!(submit_backoff(3), SUBMIT_BASE_BACKOFF * 8);
130 assert_eq!(submit_backoff(30), SUBMIT_MAX_BACKOFF);
131 assert_eq!(submit_backoff(i32::MAX), SUBMIT_MAX_BACKOFF);
132 }
133
134 #[test]
135 fn verify_polls_follow_the_landing_window_from_the_submission() {
136 let now = Utc::now();
137 let delay = |age: TimeDelta| verify_delay(Some(now - age), now);
138 assert_eq!(delay(TimeDelta::zero()), VERIFY_WINDOW_START);
139 assert_eq!(delay(TimeDelta::hours(3)), TimeDelta::minutes(90));
140 assert_eq!(delay(TimeDelta::minutes(270)), VERIFY_WINDOW_INTERVAL);
141 assert_eq!(delay(TimeDelta::hours(10)), VERIFY_WINDOW_INTERVAL);
142 assert_eq!(delay(TimeDelta::hours(11)), VERIFY_LATE_WINDOW_INTERVAL);
143 assert_eq!(delay(TimeDelta::hours(28)), VERIFY_LATE_WINDOW_INTERVAL);
144 assert_eq!(delay(TimeDelta::hours(29)), VERIFY_BASE_BACKOFF);
145 assert_eq!(delay(TimeDelta::hours(33)), VERIFY_BASE_BACKOFF * 2);
146 assert_eq!(delay(TimeDelta::days(5)), VERIFY_MAX_BACKOFF);
147 assert_eq!(verify_delay(None, now), VERIFY_WINDOW_INTERVAL);
148 }
149
150 #[test]
151 fn the_retry_window_runs_from_the_first_failure() {
152 let now = Utc::now();
153 assert!(!submit_window_expired(None, now));
154 assert!(!submit_window_expired(Some(now - TimeDelta::days(6)), now));
155 assert!(submit_window_expired(Some(now - TimeDelta::days(8)), now));
156 }
157
158 #[test]
159 fn the_verify_window_runs_from_the_submission() {
160 let now = Utc::now();
161 assert!(!verify_window_expired(None, now));
162 assert!(!verify_window_expired(Some(now - TimeDelta::days(13)), now));
163 assert!(verify_window_expired(Some(now - TimeDelta::days(15)), now));
164 }
165
166 #[test]
167 fn jitter_never_shortens_a_backoff() {
168 let now = Utc::now();
169 for _ in 0..50 {
170 let scheduled = next_attempt_at(now, TimeDelta::minutes(1));
171 assert!(scheduled - now >= TimeDelta::minutes(1));
172 assert!(scheduled - now <= TimeDelta::minutes(1) + JITTER_MAX);
173 }
174 }
175}