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 let lock: String = evidence.lock_path.display().to_string();
394 return format!(
395 "Another process left {lock}, {owner}; after {timeout_ms} ms (indexLock.unprovenOwnerTimeoutMs) the lock was still there. cli-git left the lock in place and {consequence}. Evidence: {}. cli-git removes such a lock by itself only when the record of a cli-git transaction it recovered shows that a cli-git process that has since exited created it; nothing here shows that, so every commit stops here until the lock is gone. To remove it yourself, first confirm that no git or cli-git command is running in this repository (for example with `lsof {lock}` and `ps`), because a running Git that holds the lock would lose its index update, then run `rm {lock}`. Removal is safe when no process holds the lock and the PID file beside it, if there is one, names a process that no longer runs. Your edit stays in the working tree, and the next commit lands once the lock is gone.",
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;