git_policy_cli/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 (the worktree copy's settlement lock) 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;