Skip to main content

git_policy_cli/landing/
service_index_lock.rs

1//! What:
2//!  The service's hold on Git's own `index.lock` across a landing: taken first, by exclusive creation, waited for without
3//!  polling when another program holds it, and released by moving the new index over the real one.
4//! Why:
5//!  Git's own `commit` takes the index lock before it moves the branch and gives it up after the index is written. A landing
6//!  that moved the branch first and took the lock afterwards left the index showing content a landed commit had replaced for
7//!  as long as a foreign `git` held the lock, and a commit prepared from that index could land a revert. The wait for a
8//!  foreign holder is event-driven: an inotify watch on the Git directory wakes the service when the lock itself is removed or
9//!  moved away, and the only timer is the repetition of the holder's notice. On
10//!  platforms without inotify a held lock fails at once with the plain message. The lock carries a marker with the batch
11//!  number and the holding process from the moment it exists until it is removed after the new index is in place, so a
12//!  restart can tell its own stale lock from a foreign one wherever the service was killed, and never takes over a lock
13//!  whose holder still runs (`doc/planning/cli-git-resident-service.md`, "Landing every command line").
14//! The marker,
15//! the inotify watch,
16//! the placement of the marked file,
17//! and the attempts and the wait
18//! are in `service_index_marker.rs`,
19//! `service_index_watch.rs`,
20//! `service_index_placement.rs`
21//! and `service_index_attempts.rs`,
22//! and are re-exported here under the names callers use.
23//!
24//! In TS you'd write (pseudocode):
25//! ```ts
26//! const lock = await acquireIndexLock(indexPath, deadline, announce); ... await lock.releaseWith(newIndex);
27//! ```
28
29use crate::service_index_attempts::{Placing, wait_and_place};
30use crate::wait_ceilings::LOCK_CEILING;
31use crate::service_protocol::FailureCode;
32use std::path::{Path, PathBuf};
33use std::time::Duration;
34
35use crate::service_index_prestage::{Prestaged, take};
36pub use crate::service_index_found::{WaitNotes, describe_lock};
37pub use crate::service_index_hold::IndexLock;
38pub use crate::service_index_marker::{LOCK_MARKER, Marked, marker_text, parse_marker};
39pub use crate::service_index_ownership::{Ownership, inspect_ownership, unsettled_locks};
40#[cfg(target_os = "linux")]
41pub use crate::service_index_watch::{events_end_watch, events_name};
42pub use crate::service_index_placement::{Platform, SYSTEM, placement_failure_text};
43pub use crate::service_index_staging::{STAGED_INFIX, staged_path_for};
44pub use crate::service_index_watch::Watch;
45use crate::service_index_placement::{log_placement_failure, place_marked, reclaim};
46pub use crate::service_index_reclaim::{Reclaimed, foreign_words, remove_stale, stale_lock_words, unjudged_words};
47use crate::service_index_staging::{refresh_age, remove_dead_staging, stage_marker_synced};
48
49/// How often the holder's notice is repeated while the wait lasts.
50///  Exempt from a citation under `LQM` (class: notice pacing): it times a line the person reads or a log threshold, and
51///  nothing waits on it beyond the wait that already exists. Classed once in `doc/planning/cli-git-shipped-limits-
52///  audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
53pub const NOTICE_EVERY: Duration = Duration::from_secs(10);
54
55/// The environment variable that sets how often the holder's line is repeated, in whole seconds.
56pub const NOTICE_VARIABLE: &str = "CLI_GIT_WAIT_NOTICE_SECONDS";
57
58/// How often the holder's line is repeated: the variable when it holds a positive number of seconds, ten seconds otherwise.
59pub(crate) fn notice_every() -> Duration {
60    return notice_interval(std::env::var(NOTICE_VARIABLE).ok().as_deref());
61}
62
63/// The interval a setting of `CLI_GIT_WAIT_NOTICE_SECONDS` stands for:
64/// a positive whole number of seconds,
65/// else ten seconds.
66/// Why:
67///   Zero would repeat the holder's line without a pause.
68pub(crate) fn notice_interval(setting: Option<&str>) -> Duration {
69    return setting.and_then(|text| { return text.trim().parse::<u64>().ok(); }).filter(|seconds| { return *seconds > 0; }).map_or(NOTICE_EVERY, Duration::from_secs);
70}
71
72/// What the wait tells the clients of the batch.
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub enum Announce {
75    /// A line for the person's terminal: who holds the lock and how long the wait lasts.
76    Waiting(String),
77    /// Who held the lock when the wait ran out, in words that fit "... is using this repository's index".
78    Holder(String),
79}
80
81/// Why the lock was not taken.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct LockFailure {
84    /// The code the clients are answered with.
85    pub code: FailureCode,
86}
87
88/// What:
89///  One attempt to take the lock, with no wait.
90/// Why:
91///   Finishing a batch a killed service left open must not hold up the next batch for the whole limit; a foreign holder, or
92///  a service holder that still runs, leaves the batch open for the next round.
93///
94/// In TS you'd write (pseudocode):
95/// ```ts
96/// tryAcquireIndexLock(index, seq)
97/// ```
98pub fn try_acquire(index: &Path, seq: u64) -> Option<IndexLock> {
99    return try_acquire_on(&SYSTEM, index, seq);
100}
101
102/// `try_acquire` with the calls of `platform`.
103pub fn try_acquire_on(platform: &Platform, index: &Path, seq: u64) -> Option<IndexLock> {
104    let lock: PathBuf = lock_path_of(index);
105    let marker: String = match marker_text(seq, platform.identity) {
106        Ok(text) => text,
107        Err(error) => {
108            crate::service_error!("cannot take the index lock {}: this process has no birth identity to name in the lock ({error})", lock.display());
109            return None;
110        }
111    };
112    let (staged, held) = match stage_marker_synced(platform.sync, &lock, seq, marker.as_str()) {
113        Ok(made) => made,
114        Err(error) => {
115            log_placement_failure(&lock, &error);
116            return None;
117        }
118    };
119    refresh_age(&held);
120    let placed: std::io::Result<()> = match place_marked(platform, &staged, &lock) {
121        // A stale lock removed (or replaced by then) is worth one more placement; a lock another reclaimer is removing, a live
122        //  one, and one that cannot be removed leave the batch open for the next round.
123        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => match reclaim(platform, &lock) {
124            Reclaimed::Removed | Reclaimed::Changed => place_marked(platform, &staged, &lock),
125            Reclaimed::Busy | Reclaimed::Kept | Reclaimed::Stuck | Reclaimed::Unguarded(_) | Reclaimed::Foreign { .. } | Reclaimed::Unjudged { .. } => Err(error),
126        },
127        other => other,
128    };
129    let _removed = std::fs::remove_file(&staged);
130    if let Err(error) = &placed {
131        log_placement_failure(&lock, error);
132        return None;
133    }
134    remove_dead_staging(&lock);
135    return Some(IndexLock::placed(lock, index, marker, held, (platform.inspect, platform.release)));
136}
137
138/// The path of the lock of an index.
139pub fn lock_path_of(index: &Path) -> PathBuf {
140    let mut name = index.as_os_str().to_os_string();
141    name.push(".lock");
142    return PathBuf::from(name);
143}
144
145/// What:
146///  Takes `index.lock` by exclusive creation, waiting for a foreign holder until it lets go.
147/// Why:
148///   Git's own protocol: creation fails while any process holds the lock. The wait blocks on an inotify watch set before the
149///  first attempt, so a release between the attempt and the wait is not missed, and it wakes only for the lock itself. The
150///  marker is written once, before the first attempt, so the attempts themselves create and remove nothing in the watched
151///  directory. A holder is waited for however long it holds, and a dead owner (a PID file naming a process that is gone) ends the wait at once with
152///  nothing changed; the clients learn who held the lock.
153///
154/// In TS you'd write (pseudocode):
155/// ```ts
156/// await acquireIndexLock(index, marker, announce)
157/// ```
158///
159/// # Errors
160///
161/// `LockFailure` with `IndexInUse` when the lock's owner is dead, and with the storage
162///  code when the lock cannot be created.
163pub fn acquire(index: &Path, seq: u64, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
164    return acquire_noting_holder(index, seq, announce, &mut None);
165}
166
167/// What:
168///  `acquire`,
169/// leaving the words for the holder the evidence named in `holder` while the lock was waited for.
170/// Why:
171///   The landing's log line names who held the index when a commit waited for it,
172/// also when the wait ended in a success,
173///  which `acquire` alone reports to no one (the clients are told only when the wait fails).
174///
175/// In TS you'd write (pseudocode):
176/// ```ts
177/// await acquireIndexLock(index, marker, announce, (who) => { holder = who })
178/// ```
179///
180/// # Errors
181///
182/// As `acquire`.
183pub fn acquire_noting_holder(index: &Path, seq: u64, announce: &dyn Fn(Announce), holder: &mut Option<String>) -> Result<IndexLock, LockFailure> {
184    let mut notes: WaitNotes = WaitNotes::default();
185    let taken: Result<IndexLock, LockFailure> = acquire_placing(&SYSTEM, index, seq, LOCK_CEILING, announce, &mut notes, None);
186    holder.clone_from(&notes.holder);
187    return taken;
188}
189
190/// What:
191///  `acquire`,
192/// leaving in `notes` what the wait was for:
193/// the holder's words,
194/// what the first and the last look at the held lock found,
195/// and how many looks there were.
196/// Why:
197///   The landing's log says who held the index when a commit waited for it,
198/// whatever the wait ended in.
199///
200/// In TS you'd write (pseudocode):
201/// ```ts
202/// await acquireIndexLock(index, marker, announce, notes)
203/// ```
204///
205/// # Errors
206///
207/// As `acquire`.
208pub fn acquire_noting_wait(index: &Path, seq: u64, announce: &dyn Fn(Announce), notes: &mut WaitNotes) -> Result<IndexLock, LockFailure> {
209    return acquire_noting_wait_within(index, seq, LOCK_CEILING, announce, notes);
210}
211
212/// `acquire_noting_wait` that gives up on a foreign holder after `ceiling`.
213///
214/// # Errors
215///
216/// As `acquire`, and `IndexInUse` when the lock was still held at the ceiling.
217pub fn acquire_noting_wait_within(index: &Path, seq: u64, ceiling: Duration, announce: &dyn Fn(Announce), notes: &mut WaitNotes) -> Result<IndexLock, LockFailure> {
218    // The whole of the preparation counts as staging time: taking the marker made ahead, waiting for a flush that fails or is
219    //  for another batch, and the cold staging that follows; `acquire_placing` adds its own cold staging to this.
220    let preparing: std::time::Instant = std::time::Instant::now();
221    let prepared: Option<Prestaged> = take(index, seq);
222    notes.staging = preparing.elapsed();
223    return acquire_placing(&SYSTEM, index, seq, ceiling, announce, notes, prepared);
224}
225
226/// `acquire` with the calls of `platform`.
227///
228/// # Errors
229///
230/// As `acquire`;
231/// the log of the service says why a lock that could not be created or removed stays,
232/// and why a process with no birth identity takes none.
233pub fn acquire_on(platform: &Platform, index: &Path, seq: u64, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
234    return acquire_placing(platform, index, seq, LOCK_CEILING, announce, &mut WaitNotes::default(), None);
235}
236
237/// `acquire_on` that gives up on a foreign holder after `ceiling`, leaving in `notes` what the wait was for.
238///
239/// # Errors
240///
241/// As `acquire_on`, and `IndexInUse` when the lock was still held at the ceiling.
242pub fn acquire_within(platform: &Platform, index: &Path, seq: u64, ceiling: Duration, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
243    return acquire_placing(platform, index, seq, ceiling, announce, &mut WaitNotes::default(), None);
244}
245
246/// `acquire_noting_holder` with the calls of `platform`.
247fn acquire_placing(
248    platform: &Platform,
249    index: &Path,
250    seq: u64,
251    ceiling: Duration,
252    announce: &dyn Fn(Announce),
253    notes: &mut WaitNotes,
254    prestaged: Option<Prestaged>,
255) -> Result<IndexLock, LockFailure> {
256    let lock: PathBuf = lock_path_of(index);
257    let directory: &Path = lock.parent().unwrap_or(Path::new("."));
258    let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
259    let mut watch: Option<Watch> = (platform.watch)(directory);
260    let (marker, staged, held) = match prestaged {
261        Some(made) => {
262            // The file was flushed while the service was idle: what was waited for is what the preparation measured.
263            notes.stage_kind = "pre";
264            (made.marker, made.staged, made.held)
265        }
266        None => {
267            let marker: String = match marker_text(seq, platform.identity) {
268                Ok(text) => text,
269                Err(error) => {
270                    crate::service_error!("cannot take the index lock {}: this process has no birth identity to name in the lock ({error})", lock.display());
271                    return Err(LockFailure { code: FailureCode::Unavailable });
272                }
273            };
274            let staging_began: std::time::Instant = std::time::Instant::now();
275            let staged_result = stage_marker_synced(platform.sync, &lock, seq, marker.as_str());
276            notes.staging += staging_began.elapsed();
277            notes.stage_kind = "cold";
278            let (staged, held) = staged_result.map_err(|error| {
279                log_placement_failure(&lock, &error);
280                return LockFailure { code: crate::service_land_follow::storage_failure(&error) };
281            })?;
282            (marker, staged, held)
283        }
284    };
285    let placing_began: std::time::Instant = std::time::Instant::now();
286    let outcome: Result<(), LockFailure> = wait_and_place(
287        platform,
288        &Placing { staged: &staged, held: &held, lock: &lock, lock_name: lock_name.as_os_str(), index, ceiling },
289        &mut watch,
290        announce,
291        notes,
292    );
293    notes.placing = placing_began.elapsed();
294    let _removed = std::fs::remove_file(&staged);
295    outcome?;
296    remove_dead_staging(&lock);
297    return Ok(IndexLock::placed(lock, index, marker, held, (platform.inspect, platform.release)));
298}
299
300/// Controls for the removal of a stale lock stay out of the release executable.
301#[cfg(test)]
302#[path = "service_index_reclaim_tests.rs"]
303mod reclaim_tests;
304
305/// Controls for the claim chain stay out of the release executable.
306#[cfg(test)]
307#[path = "service_index_claim_tests.rs"]
308mod claim_tests;
309
310/// Controls for the machine ID in a marker stay out of the release executable.
311#[cfg(test)]
312#[path = "service_index_machine_tests.rs"]
313mod machine_tests;
314
315/// Index lock controls stay out of the release executable.
316#[cfg(test)]
317#[path = "service_index_lock_tests.rs"]
318mod tests;