Skip to main content

git_policy_cli/lock/
index_lock_wait.rs

1//! What:
2//!  Git's own lock-retry schedule: the backoff position, its next step, and the jittered wait it prescribes.
3//! Why:
4//!  A caller that retries a lock the way Git retries its own waits on the same
5//!  schedule, so it neither hammers the lock nor starves behind it. Waiting for a foreign `index.lock` is gone: a command
6//!  that cannot take Git's `index.lock` ends as native Git ends it, and the commit service's landing retries it a bounded
7//!  number of times itself (`src/index-lock/index-lock-wait.ts` is the incumbent of the schedule).
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const wait = jitteredWait(state, Math.random()); state = nextBackoff(state);
12//! ```
13
14/// Git's `BACKOFF_MAX_MULTIPLIER`:
15///  the backoff stops growing here.
16pub const BACKOFF_MAX_MULTIPLIER: u64 = 1_000;
17
18/// Git's jitter floor in thousandths of the backoff.
19const JITTER_FLOOR_PERMILLE: u64 = 750;
20
21/// Git's jitter span in thousandths of the backoff.
22const JITTER_SPAN_PERMILLE: u64 = 500;
23
24/// Thousandths per whole.
25const PERMILLE: u64 = 1_000;
26
27/// What:
28///  Git's backoff position:
29///  `step` is Git's `n`,
30///  `multiplier` the wait base in milliseconds.
31/// Why:
32///   `BackoffState` in the incumbent.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// type BackoffState = { step: number; multiplier: number };
37/// ```
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct BackoffState {
40    /// Git's `n`.
41    pub step: u64,
42    /// Git's `multiplier`,
43    ///  the current backoff in milliseconds.
44    pub multiplier: u64,
45}
46
47/// Backoff before the first wait.
48pub const INITIAL_BACKOFF: BackoffState = BackoffState {
49    step: 1,
50    multiplier: 1,
51};
52
53/// What:
54///  One jittered wait from Git's formula `(750 + rand() % 500) * backoff / 1000`,
55///  rounded down.
56/// Why:
57///   Jitter spreads retrying processes apart;
58///  `random` is a value in `[0, 1)`.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// Math.floor(((750 + Math.floor(random * 500)) * state.multiplier) / 1000);
63/// ```
64pub fn jittered_wait(state: BackoffState, random: f64) -> u64 {
65    let span: u64 = (random * (JITTER_SPAN_PERMILLE as f64)).floor() as u64;
66    return (JITTER_FLOOR_PERMILLE + span) * state.multiplier / PERMILLE;
67}
68
69/// What:
70///  The next backoff:
71///  `multiplier += 2n + 1`,
72///  capped at `BACKOFF_MAX_MULTIPLIER`.
73/// Why:
74///   Adding `2n + 1` makes the multiplier the square of the step,
75///  which is Git's schedule.
76///
77/// In TS you'd write (pseudocode):
78/// ```ts
79/// nextBackoff({ step: 1, multiplier: 1 }); // { step: 2, multiplier: 4 }
80/// ```
81pub fn next_backoff(state: BackoffState) -> BackoffState {
82    let multiplier: u64 = state.multiplier + 2 * state.step + 1;
83    if multiplier > BACKOFF_MAX_MULTIPLIER {
84        return BackoffState {
85            step: state.step,
86            multiplier: BACKOFF_MAX_MULTIPLIER,
87        };
88    }
89    return BackoffState {
90        step: state.step + 1,
91        multiplier,
92    };
93}
94
95/// Backoff controls stay out of the release executable.
96#[cfg(test)]
97#[path = "index_lock_wait_tests.rs"]
98mod tests;