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::live_lock::acquire_live_owner_lock;
16use super::owner_lock::OwnerLock;
17use super::recovery_error::RecoveryError;
18use super::recovery_landing::RecoveryPlace;
19use super::recovery_scan::recover_dead_landings;
20use super::replay_reservation::{LandingReservation, ReservationError, acquire_reserved_landing_lock};
21use super::transaction_git::GitContext;
22use super::transaction_lease::{has_valid_landing_lease, landing_lease_token};
23use super::transaction_registry::LANDING_LOCK_NAME;
24use std::ffi::OsString;
25use std::path::{Path, PathBuf};
26
27/// Module tag for diagnostics.
28const TAG: &str = "acquireLandingLock";
29
30/// What:
31///  The landing lock directory of one worktree's registry.
32/// Why:
33///   The reservation and the lease checks name the same directory.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// join(registryRoot, LANDING_LOCK_NAME)
38/// ```
39pub fn landing_lock_directory(layout: &InvocationLayout) -> PathBuf {
40    return layout.registry_root.join(LANDING_LOCK_NAME);
41}
42
43/// What:
44///  Whether this invocation lands under an ancestor's landing lock.
45/// Why:
46///   The ancestor's lock is the one the reservation holder waits for, so a nested landing neither waits nor reserves.
47///
48/// In TS you'd write (pseudocode):
49/// ```ts
50/// await hasValidLandingLease({ environment: process.env, lockDirectory })
51/// ```
52pub fn landing_is_nested(layout: &InvocationLayout, environment: &[(OsString, OsString)]) -> bool {
53    return has_valid_landing_lease(environment, landing_lock_directory(layout).as_path());
54}
55
56/// What:
57///  Take the landing lock and recover dead landings under it, or take the ancestor's view of it.
58/// Why:
59///   The lock is released again when recovery fails before the caller receives it.
60///
61/// In TS you'd write (pseudocode):
62/// ```ts
63/// const lock = await acquireLandingLock({ gitPath, cwd, registryRoot });
64/// ```
65pub fn acquire_landing_lock(
66    context: &GitContext,
67    cwd: &Path,
68    layout: &InvocationLayout,
69    environment: &[(OsString, OsString)],
70) -> Result<OwnerLock, RecoveryError> {
71    let directory: PathBuf = landing_lock_directory(layout);
72    let lock: OwnerLock = if landing_is_nested(layout, environment) {
73        let token: String = landing_lease_token(environment).unwrap_or_default();
74        debug(TAG, format!("leaving {} to the ancestor that holds it", directory.display()).as_str());
75        OwnerLock::inherited(directory.as_path(), token.as_str())
76    } else {
77        acquire_live_owner_lock(directory.as_path(), "the landing lock")
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;