Skip to main content

git_policy_cli/
index_lock_wait.rs

1//! What:
2//!  Waiting for a foreign `index.lock` under the classification rules:
3//!  a proven-alive owner
4//!       is waited for up to this process's wait limit (`CLI_GIT_LOCK_WAIT_SECONDS`, 30 minutes by default),
5//!  a dead or evidence-free owner is retried on Git's backoff
6//!       schedule until the unproven budget runs out.
7//! Why:
8//!  An index writer must not run beside a landing,
9//!  and a lock nobody can prove alive must
10//!      not block forever.
11//!  Nothing here deletes a lock;
12//!  the caller decides what the failure means
13//!      (`src/index-lock/index-lock-wait.ts`).
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! await waitForIndexLock({ realIndexPath, timeoutMs, consequence, attempt });
18//! ```
19
20/// The backstop every wait on a live process is bounded by.
21use super::live_lock::{LIVE_WAIT_VARIABLE, live_wait_limit};
22/// Foreign-lock evidence and its verdicts.
23use super::index_lock_evidence::{
24    Evidence, Verdict, classify_index_lock, describe_index_lock_evidence, proven_holder_line,
25    proven_holder_progress_line,
26};
27/// When the notices of a wait are due.
28use super::wait_progress::{Cadence, Ticker};
29/// Time spans.
30use std::time::Duration;
31
32/// Git's `BACKOFF_MAX_MULTIPLIER`:
33///  the backoff stops growing here.
34pub const BACKOFF_MAX_MULTIPLIER: u64 = 1_000;
35
36/// Git's jitter floor in thousandths of the backoff.
37const JITTER_FLOOR_PERMILLE: u64 = 750;
38
39/// Git's jitter span in thousandths of the backoff.
40const JITTER_SPAN_PERMILLE: u64 = 500;
41
42/// Thousandths per whole.
43const PERMILLE: u64 = 1_000;
44
45/// Longest single poll while a PID file proves the owner alive,
46///  so its release is noticed promptly.
47pub const PROVEN_BY_PID_FILE_POLL_CAP_MS: u64 = 100;
48
49/// Longest single poll while only an open descriptor proves the owner alive;
50///  each poll repeats the
51/// open-holder scan,
52///  so it polls less often.
53pub const PROVEN_BY_DESCRIPTOR_POLL_CAP_MS: u64 = 500;
54
55/// What:
56///  Git's backoff position:
57///  `step` is Git's `n`,
58///  `multiplier` the wait base in milliseconds.
59/// Why:
60///   `BackoffState` in the incumbent.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// type BackoffState = { step: number; multiplier: number };
65/// ```
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub struct BackoffState {
68    /// Git's `n`.
69    pub step: u64,
70    /// Git's `multiplier`,
71    ///  the current backoff in milliseconds.
72    pub multiplier: u64,
73}
74
75/// Backoff before the first wait.
76pub const INITIAL_BACKOFF: BackoffState = BackoffState {
77    step: 1,
78    multiplier: 1,
79};
80
81/// What:
82///  One jittered wait from Git's formula `(750 + rand() % 500) * backoff / 1000`,
83///  rounded down.
84/// Why:
85///   Jitter spreads retrying processes apart;
86///  `random` is a value in `[0, 1)`.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// Math.floor(((750 + Math.floor(random * 500)) * state.multiplier) / 1000);
91/// ```
92pub fn jittered_wait(state: BackoffState, random: f64) -> u64 {
93    let span: u64 = (random * (JITTER_SPAN_PERMILLE as f64)).floor() as u64;
94    return (JITTER_FLOOR_PERMILLE + span) * state.multiplier / PERMILLE;
95}
96
97/// What:
98///  The next backoff:
99///  `multiplier += 2n + 1`,
100///  capped at `BACKOFF_MAX_MULTIPLIER`.
101/// Why:
102///   Adding `2n + 1` makes the multiplier the square of the step,
103///  which is Git's schedule.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// nextBackoff({ step: 1, multiplier: 1 }); // { step: 2, multiplier: 4 }
108/// ```
109pub fn next_backoff(state: BackoffState) -> BackoffState {
110    let multiplier: u64 = state.multiplier + 2 * state.step + 1;
111    if multiplier > BACKOFF_MAX_MULTIPLIER {
112        return BackoffState {
113            step: state.step,
114            multiplier: BACKOFF_MAX_MULTIPLIER,
115        };
116    }
117    return BackoffState {
118        step: state.step + 1,
119        multiplier,
120    };
121}
122
123/// What:
124///  What one attempt found,
125///  or why the wait ended without a result.
126/// Why:
127///   The caller reports the owner and the consequence;
128///  the attempt itself is the lock check.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// type IndexLockWaitError = { kind: 'unproven-owner'; evidence; verdict; timeoutMs; consequence } | { kind: 'read'; reason };
133/// ```
134#[derive(Clone, Debug, PartialEq)]
135pub enum WaitFailure {
136    /// A dead or unproven owner outlasted the unproven budget.
137    UnprovenOwner {
138        /// Last attempt's evidence.
139        evidence: Box<Evidence>,
140        /// Last verdict,
141        ///  which is never proven alive.
142        verdict: Verdict,
143        /// Budget that ran out,
144        ///  in milliseconds.
145        timeout_ms: u64,
146        /// What cli-git therefore did not do.
147        consequence: String,
148    },
149    /// A proven-alive owner kept the lock until the wait limit ran out.
150    LiveHolderTimedOut {
151        /// The lock that stayed in place.
152        lock_path: std::path::PathBuf,
153        /// The owner's process ID.
154        pid: i64,
155        /// The owner's command name when readable.
156        command: Option<String>,
157        /// Milliseconds spent waiting for a proven-alive owner.
158        waited_ms: u64,
159        /// The limit that ran out,
160        ///  in milliseconds.
161        limit_ms: u64,
162        /// What cli-git therefore did not do.
163        consequence: String,
164    },
165    /// The lock or its evidence could not be read.
166    Read(String),
167}
168
169/// What:
170///  The side effects of a wait,
171///  injected so tests can drive a fake clock and fake holders.
172/// Why:
173///   The host implementation reads `/proc`,
174///  sleeps and writes standard error;
175///       tests script each step.
176///
177/// In TS you'd write (pseudocode):
178/// ```ts
179/// type IndexLockWaitEffects = { gather; sleep; random; report; now };
180/// ```
181pub trait WaitEffects {
182    /// Whether the lock currently exists (the attempt).
183    fn lock_present(&mut self) -> Result<bool, String>;
184    /// This attempt's evidence,
185    ///  or nothing when the lock was released meanwhile.
186    fn gather(&mut self) -> Result<Option<Evidence>, String>;
187    /// Sleeps for `ms` milliseconds.
188    fn sleep(&mut self, ms: u64);
189    /// A uniform value in `[0, 1)`.
190    fn random(&mut self) -> f64;
191    /// The current time in milliseconds.
192    fn now_ms(&mut self) -> f64;
193    /// Writes one line of diagnostics to standard error.
194    fn report(&mut self, line: &str);
195    /// When this wait repeats its notice:
196    ///  the host reads the environment,
197    ///  and a fake keeps the standard cadence unless a test sets another.
198    fn cadence(&mut self) -> Cadence {
199        return Cadence::standard();
200    }
201}
202
203/// What:
204///  The per-wait loop state:
205///  backoff,
206///  time spent unproven,
207///  and whether the holder line was written.
208/// Why:
209///   `state` in `waitForIndexLock`.
210///
211/// In TS you'd write (pseudocode):
212/// ```ts
213/// const state = { backoff: INITIAL_BACKOFF, unprovenMs: 0, reported: false };
214/// ```
215#[derive(Clone, Copy, Debug)]
216struct LoopState {
217    /// Backoff position.
218    backoff: BackoffState,
219    /// Milliseconds spent without a proven owner.
220    unproven_ms: f64,
221    /// Milliseconds spent waiting for a proven-alive owner.
222    live_ms: f64,
223    /// Whether the holder line was written.
224    reported: bool,
225    /// When the next repeat of the holder line is due, counted in `live_ms`.
226    live_ticker: Ticker,
227    /// When the next notice of a wait behind an unproven owner is due, counted in `unproven_ms`.
228    unproven_ticker: Ticker,
229}
230
231/// What:
232///  Wait until the lock is gone,
233///  or fail once a dead or unproven owner outlasts `timeout_ms`
234///  or a live owner outlasts this process's wait limit.
235/// Why:
236///   A live holder gets one diagnostic and the process's wait limit;
237///  other holders get the budget,
238///       which includes the evidence gathering because a `/proc` scan is not free.
239///
240/// In TS you'd write (pseudocode):
241/// ```ts
242/// await waitForIndexLock({ realIndexPath, timeoutMs, consequence, attempt, effects });
243/// ```
244pub fn wait_for_index_lock(
245    effects: &mut dyn WaitEffects,
246    timeout_ms: u64,
247    consequence: &str,
248) -> Result<(), WaitFailure> {
249    let limit_ms: u64 = u64::try_from(live_wait_limit().as_millis()).unwrap_or(u64::MAX);
250    return wait_for_index_lock_within(effects, timeout_ms, limit_ms, consequence);
251}
252
253/// What:
254///  The wait with an explicit limit on the time spent behind a proven-alive owner.
255/// Why:
256///   The default wait and the tests share one loop; only the limit differs.
257///
258/// In TS you'd write (pseudocode):
259/// ```ts
260/// await waitForIndexLock({ realIndexPath, timeoutMs, liveLimitMs, consequence, attempt, effects });
261/// ```
262pub fn wait_for_index_lock_within(
263    effects: &mut dyn WaitEffects,
264    timeout_ms: u64,
265    live_limit_ms: u64,
266    consequence: &str,
267) -> Result<(), WaitFailure> {
268    let cadence: Cadence = effects.cadence();
269    // `mut` lets each iteration advance the loop state.
270    let mut state: LoopState = LoopState {
271        backoff: INITIAL_BACKOFF,
272        unproven_ms: 0.0,
273        live_ms: 0.0,
274        reported: false,
275        live_ticker: Ticker::announced_at_start(cadence),
276        unproven_ticker: Ticker::new(cadence),
277    };
278    loop {
279        let iteration_started: f64 = effects.now_ms();
280        let present: bool = match effects.lock_present() {
281            Ok(found) => found,
282            Err(reason) => return Err(WaitFailure::Read(reason)),
283        };
284        if !present {
285            return Ok(());
286        }
287        let evidence: Evidence = match effects.gather() {
288            Ok(Some(found)) => found,
289            // The lock went away between the attempt and the evidence read.
290            Ok(None) => continue,
291            Err(reason) => return Err(WaitFailure::Read(reason)),
292        };
293        let verdict: Verdict = classify_index_lock(&evidence);
294        let random: f64 = effects.random();
295        let backoff_ms: u64 = jittered_wait(state.backoff, random);
296        state.backoff = next_backoff(state.backoff);
297        if let Verdict::ProvenAlive {
298            pid,
299            by_descriptor,
300            command,
301        } = &verdict
302        {
303            if !state.reported {
304                let line: String = proven_holder_line(
305                    *pid,
306                    *by_descriptor,
307                    command.as_deref(),
308                    evidence.lock_path.as_path(),
309                    live_limit_ms / 1_000,
310                );
311                effects.report(line.as_str());
312                state.reported = true;
313            }
314            let cap: u64 = if *by_descriptor {
315                PROVEN_BY_DESCRIPTOR_POLL_CAP_MS
316            } else {
317                PROVEN_BY_PID_FILE_POLL_CAP_MS
318            };
319            effects.sleep(backoff_ms.min(cap));
320            state.live_ms += effects.now_ms() - iteration_started;
321            // A notice that is due goes out before the limit is checked, so a refused wait was announced.
322            if state.live_ticker.due(Duration::from_millis(state.live_ms as u64)) {
323                let repeat: String = proven_holder_progress_line(
324                    *pid,
325                    *by_descriptor,
326                    command.as_deref(),
327                    evidence.lock_path.as_path(),
328                    state.live_ms as u64 / 1_000,
329                    live_limit_ms / 1_000,
330                );
331                effects.report(repeat.as_str());
332            }
333            if state.live_ms >= live_limit_ms as f64 {
334                return Err(WaitFailure::LiveHolderTimedOut {
335                    lock_path: evidence.lock_path.clone(),
336                    pid: *pid,
337                    command: command.clone(),
338                    waited_ms: state.live_ms as u64,
339                    limit_ms: live_limit_ms,
340                    consequence: String::from(consequence),
341                });
342            }
343            continue;
344        }
345        state.unproven_ms += effects.now_ms() - iteration_started;
346        // A notice that is due goes out before the budget is checked, so a refused wait was announced.
347        if state.unproven_ticker.due(Duration::from_millis(state.unproven_ms as u64)) {
348            let notice: String = unproven_progress_line(
349                evidence.lock_path.as_path(),
350                &verdict,
351                state.unproven_ms as u64,
352                timeout_ms,
353            );
354            effects.report(notice.as_str());
355        }
356        if state.unproven_ms >= timeout_ms as f64 {
357            return Err(WaitFailure::UnprovenOwner {
358                evidence: Box::new(evidence),
359                verdict,
360                timeout_ms,
361                consequence: String::from(consequence),
362            });
363        }
364        let sleep_started: f64 = effects.now_ms();
365        effects.sleep(backoff_ms);
366        state.unproven_ms += effects.now_ms() - sleep_started;
367    }
368}
369
370/// What:
371///  The diagnostic text of an unproven-owner failure.
372/// Why:
373///   The person needs the lock path,
374///  the owner state,
375///  the budget,
376///  what was not done,
377///  and the evidence.
378///
379/// In TS you'd write (pseudocode):
380/// ```ts
381/// new IndexLockUnprovenOwnerError({ evidence, verdict, timeoutMs, consequence }).message
382/// ```
383pub fn unproven_owner_message(
384    evidence: &Evidence,
385    verdict: &Verdict,
386    timeout_ms: u64,
387    consequence: &str,
388) -> String {
389    let owner: String = match verdict {
390        Verdict::Dead(pid) => format!("whose owner PID {pid} no longer runs"),
391        _ => String::from("whose owner cli-git could not prove alive"),
392    };
393    return format!(
394        "Another process left {}, {owner}; after {timeout_ms} ms (indexLock.unprovenOwnerTimeoutMs) the lock was still there. cli-git left the lock in place and {consequence}. Evidence: {}. Remove the lock only after confirming no Git process is using this repository.",
395        evidence.lock_path.display(),
396        describe_index_lock_evidence(evidence),
397    );
398}
399
400/// What:
401///  The notice a wait behind a lock with no proven live owner repeats.
402/// Why:
403///   The wait retries on Git's backoff schedule and is silent otherwise,
404///  so the person learns what is held, why no process is named, how long the wait has lasted and where it gives up.
405///
406/// In TS you'd write (pseudocode):
407/// ```ts
408/// `cli-git: still waiting for ${lockPath} after ${waitedMs} ms ...`
409/// ```
410pub fn unproven_progress_line(
411    lock_path: &std::path::Path,
412    verdict: &Verdict,
413    waited_ms: u64,
414    timeout_ms: u64,
415) -> String {
416    let owner: String = match verdict {
417        Verdict::Dead(pid) => format!("whose owner PID {pid} no longer runs"),
418        _ => String::from("whose owner cli-git cannot prove alive"),
419    };
420    return format!(
421        "cli-git: still waiting for {}, {owner}, after {} seconds; retrying until {timeout_ms} ms \
422         (indexLock.unprovenOwnerTimeoutMs) have passed, then stopping and leaving the lock in place.\n",
423        lock_path.display(),
424        waited_ms / 1_000
425    );
426}
427
428/// What:
429///  The diagnostic text of a live-owner timeout.
430/// Why:
431///   The person needs the lock path, the owner to look at, the limit and its setting,
432///  and what was not done.
433///
434/// In TS you'd write (pseudocode):
435/// ```ts
436/// new IndexLockLiveOwnerTimeoutError({ lockPath, pid, command, waitedMs, limitMs, consequence }).message
437/// ```
438pub fn live_holder_timeout_message(
439    lock_path: &std::path::Path,
440    pid: i64,
441    command: Option<&str>,
442    waited_ms: u64,
443    consequence: &str,
444) -> String {
445    let owner: String = match command {
446        Some(name) => format!("PID {pid} ({name})"),
447        None => format!("PID {pid}"),
448    };
449    return format!(
450        "Gave up after waiting {} seconds for {owner}, which still holds {}; {LIVE_WAIT_VARIABLE} sets how long cli-git waits. cli-git left the lock in place and {consequence}. Look at that process, or raise the limit.",
451        waited_ms / 1_000,
452        lock_path.display(),
453    );
454}
455
456/// Wait controls stay out of the release executable.
457#[cfg(test)]
458#[path = "index_lock_wait_tests.rs"]
459mod tests;