Skip to main content

git_policy_cli/
commit_landing_lock.rs

1//! What:
2//!  The per-worktree landing lock a landing attempt takes, and the recovery every acquisition runs first.
3//! Why:
4//!  A crashed landing must be resolved before another landing moves the ref or the index, so the lock is acquired
5//!  and then dead landings are recovered under it (`src/policy-engine/commit-landing-lock.ts`).
6//!  An invocation nested under the holder, proven by an inherited landing lease, proceeds under its ancestor's lock.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! await using lock = await acquireLandingLock({ gitPath, cwd, registryRoot });
11//! ```
12
13use super::capture_invocation::InvocationLayout;
14use super::diagnostic_log::debug;
15use super::owner_lock::{DEFAULT_POLL_DELAY, OwnerLock, SilentWait, acquire_owner_lock};
16use super::recovery_error::RecoveryError;
17use super::recovery_landing::RecoveryPlace;
18use super::recovery_scan::recover_dead_landings;
19use super::replay_reservation::{LandingReservation, ReservationError, acquire_reserved_landing_lock};
20use super::transaction_git::GitContext;
21use super::transaction_lease::{has_valid_landing_lease, landing_lease_token};
22use super::transaction_registry::LANDING_LOCK_NAME;
23use std::ffi::OsString;
24use std::path::{Path, PathBuf};
25
26/// Module tag for diagnostics.
27const TAG: &str = "acquireLandingLock";
28
29/// What:
30///  The landing lock directory of one worktree's registry.
31/// Why:
32///   The reservation and the lease checks name the same directory.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// join(registryRoot, LANDING_LOCK_NAME)
37/// ```
38pub fn landing_lock_directory(layout: &InvocationLayout) -> PathBuf {
39    return layout.registry_root.join(LANDING_LOCK_NAME);
40}
41
42/// What:
43///  Whether this invocation lands under an ancestor's landing lock.
44/// Why:
45///   The ancestor's lock is the one the reservation holder waits for, so a nested landing neither waits nor reserves.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// await hasValidLandingLease({ environment: process.env, lockDirectory })
50/// ```
51pub fn landing_is_nested(layout: &InvocationLayout, environment: &[(OsString, OsString)]) -> bool {
52    return has_valid_landing_lease(environment, landing_lock_directory(layout).as_path());
53}
54
55/// What:
56///  Take the landing lock and recover dead landings under it, or take the ancestor's view of it.
57/// Why:
58///   The lock is released again when recovery fails before the caller receives it.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// const lock = await acquireLandingLock({ gitPath, cwd, registryRoot });
63/// ```
64pub fn acquire_landing_lock(
65    context: &GitContext,
66    cwd: &Path,
67    layout: &InvocationLayout,
68    environment: &[(OsString, OsString)],
69) -> Result<OwnerLock, RecoveryError> {
70    let directory: PathBuf = landing_lock_directory(layout);
71    let lock: OwnerLock = if landing_is_nested(layout, environment) {
72        let token: String = landing_lease_token(environment).unwrap_or_default();
73        debug(TAG, format!("leaving {} to the ancestor that holds it", directory.display()).as_str());
74        OwnerLock::inherited(directory.as_path(), token.as_str())
75    } else {
76        let mut silent: SilentWait = SilentWait;
77        acquire_owner_lock(directory.as_path(), DEFAULT_POLL_DELAY, &mut silent)
78            .map_err(|error| { return RecoveryError(error.to_string()); })?
79    };
80    let place: RecoveryPlace = RecoveryPlace {
81        git: context.clone(),
82        cwd: cwd.to_path_buf(),
83        common_dir: layout.common_dir.clone(),
84    };
85    // A failure drops `lock`, which releases it before the caller ever holds it.
86    recover_dead_landings(&place, layout.registry_root.as_path())?;
87    return Ok(lock);
88}
89
90/// What:
91///  Take the landing lock once no other transaction's reservation stands in the way.
92/// Why:
93///   The reservation is checked again under the lock, so a reservation granted while this landing waited is honored.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// await using lock = await acquireReservedLandingLock({ gitPath, cwd, registryRoot, reservation });
98/// ```
99pub fn acquire_reserved_lock(
100    context: &GitContext,
101    cwd: &Path,
102    layout: &InvocationLayout,
103    environment: &[(OsString, OsString)],
104    reservation: &LandingReservation,
105) -> Result<OwnerLock, ReservationError> {
106    return acquire_reserved_landing_lock(reservation, || {
107        return acquire_landing_lock(context, cwd, layout, environment).map_err(ReservationError::Registry);
108    });
109}
110
111/// Controls for this module stay out of the release executable.
112#[cfg(test)]
113#[path = "commit_landing_lock_tests.rs"]
114mod tests;