git_policy_cli/service_retry.rs
1//! What:
2//! The repositories the service must look at again, and when: those with a claim that another instance holds and that may run
3//! out its lease while this service sits idle.
4//! Why:
5//! A restarted service that finds the claims of the instance it replaces cannot take them before their lease is over, and
6//! nothing wakes it when the lease is: the next commit might be hours away, and the work would sit claimed by a holder that
7//! died. The work loop asks this schedule how long to wait for a wake before it looks again.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const wait = schedule.nextWait(now); const due = schedule.takeDue(now); schedule.replace(ran, retries, now);
12//! ```
13
14use std::path::PathBuf;
15use std::time::{Duration, Instant};
16
17/// Added to a wait so that the lease is over when the service looks, not exactly at its edge.
18pub const RETRY_SLACK: Duration = Duration::from_secs(1);
19
20/// What:
21/// The repositories to look at again, each with the time to do it.
22/// Why:
23/// One entry per repository, replaced by the newest answer of a round that looked at it.
24///
25/// In TS you'd write (pseudocode):
26/// ```ts
27/// type RetrySchedule = Map<string, number>;
28/// ```
29#[derive(Debug, Default)]
30pub struct RetrySchedule {
31 /// The repositories and when each is due.
32 due: Vec<(PathBuf, Instant)>,
33}
34
35/// Methods over the schedule.
36impl RetrySchedule {
37 /// Whether nothing is waiting to be looked at again.
38 pub fn is_empty(&self) -> bool {
39 return self.due.is_empty();
40 }
41
42 /// How long until the first entry is due (nothing at all when the schedule is empty, zero for one already due).
43 pub fn next_wait(&self, now: Instant) -> Option<Duration> {
44 return self.due.iter().map(|(_, when)| { return when.saturating_duration_since(now); }).min();
45 }
46
47 /// Removes and returns the repositories that are due at `now`.
48 pub fn take_due(&mut self, now: Instant) -> Vec<PathBuf> {
49 let (due, waiting): (Vec<(PathBuf, Instant)>, Vec<(PathBuf, Instant)>) = std::mem::take(&mut self.due).into_iter().partition(|(_, when)| { return *when <= now; });
50 self.due = waiting;
51 return due.into_iter().map(|(directory, _)| { return directory; }).collect();
52 }
53
54 /// Forgets what was scheduled for the repositories a round just ran, and schedules what that round asked for.
55 pub fn replace(&mut self, ran: &[PathBuf], retries: Vec<(PathBuf, Duration)>, now: Instant) {
56 self.due.retain(|(directory, _)| { return !ran.contains(directory); });
57 for (directory, wait) in retries {
58 self.due.push((directory, now + wait + RETRY_SLACK));
59 }
60 }
61}
62
63/// Schedule controls stay out of the release executable.
64#[cfg(test)]
65#[path = "service_retry_tests.rs"]
66mod tests;