Skip to main content

git_policy_cli/
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 limit (`CLI_GIT_LOCK_WAIT_SECONDS`) and 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 super::service_index_attempts::{Placing, wait_and_place};
30use super::service_protocol::FailureCode;
31use std::path::{Path, PathBuf};
32use std::time::Duration;
33
34use super::service_index_prestage::{Prestaged, take};
35pub use super::service_index_found::{WaitNotes, describe_lock};
36pub use super::service_index_hold::IndexLock;
37pub use super::service_index_marker::{LOCK_MARKER, Marked, marker_text, parse_marker};
38pub use super::service_index_ownership::{Ownership, inspect_ownership, unsettled_locks};
39#[cfg(target_os = "linux")]
40pub use super::service_index_watch::{events_end_watch, events_name};
41pub use super::service_index_placement::{Platform, SYSTEM, placement_failure_text};
42pub use super::service_index_staging::{STAGED_INFIX, staged_path_for};
43pub use super::service_index_watch::Watch;
44use super::service_index_marker::{holder_is_gone, is_reclaimable, marker_of};
45use super::service_index_placement::{log_placement_failure, place_marked, reclaim};
46use super::service_index_staging::{refresh_age, remove_dead_staging, stage_marker_synced};
47
48/// How often the holder's notice is repeated while the wait lasts.
49pub const NOTICE_EVERY: Duration = Duration::from_secs(10);
50
51/// The environment variable that sets how often the holder's line is repeated, in whole seconds.
52pub const NOTICE_VARIABLE: &str = "CLI_GIT_WAIT_NOTICE_SECONDS";
53
54/// How often the holder's line is repeated: the variable when it holds a positive number of seconds, ten seconds otherwise.
55pub(super) fn notice_every() -> Duration {
56    return notice_interval(std::env::var(NOTICE_VARIABLE).ok().as_deref());
57}
58
59/// The interval a setting of `CLI_GIT_WAIT_NOTICE_SECONDS` stands for:
60/// a positive whole number of seconds,
61/// else ten seconds.
62/// Why:
63///   Zero would repeat the holder's line without a pause.
64pub(super) fn notice_interval(setting: Option<&str>) -> Duration {
65    return setting.and_then(|text| { return text.trim().parse::<u64>().ok(); }).filter(|seconds| { return *seconds > 0; }).map_or(NOTICE_EVERY, Duration::from_secs);
66}
67
68/// What the wait tells the clients of the batch.
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub enum Announce {
71    /// A line for the person's terminal: who holds the lock and how long the wait lasts.
72    Waiting(String),
73    /// Who held the lock when the wait ran out, in words that fit "... is using this repository's index".
74    Holder(String),
75}
76
77/// Why the lock was not taken.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct LockFailure {
80    /// The code the clients are answered with.
81    pub code: FailureCode,
82}
83
84/// What:
85///  One attempt to take the lock, with no wait.
86/// Why:
87///   Finishing a batch a killed service left open must not hold up the next batch for the whole limit; a foreign holder, or
88///  a service holder that still runs, leaves the batch open for the next round.
89///
90/// In TS you'd write (pseudocode):
91/// ```ts
92/// tryAcquireIndexLock(index, seq)
93/// ```
94pub fn try_acquire(index: &Path, seq: u64) -> Option<IndexLock> {
95    return try_acquire_on(&SYSTEM, index, seq);
96}
97
98/// `try_acquire` with the calls of `platform`.
99pub fn try_acquire_on(platform: &Platform, index: &Path, seq: u64) -> Option<IndexLock> {
100    let lock: PathBuf = lock_path_of(index);
101    let marker: String = match marker_text(seq, platform.identity) {
102        Ok(text) => text,
103        Err(error) => {
104            crate::service_error!("cannot take the index lock {}: this process has no birth identity to name in the lock ({error})", lock.display());
105            return None;
106        }
107    };
108    let (staged, held) = match stage_marker_synced(platform.sync, &lock, seq, marker.as_str()) {
109        Ok(made) => made,
110        Err(error) => {
111            log_placement_failure(&lock, &error);
112            return None;
113        }
114    };
115    refresh_age(&held);
116    let placed: std::io::Result<()> = match place_marked(platform, &staged, &lock) {
117        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists && is_reclaimable(&lock) => {
118            if reclaim(platform, &lock) { place_marked(platform, &staged, &lock) } else { Err(error) }
119        }
120        other => other,
121    };
122    let _removed = std::fs::remove_file(&staged);
123    if let Err(error) = &placed {
124        log_placement_failure(&lock, error);
125        return None;
126    }
127    remove_dead_staging(&lock);
128    return Some(IndexLock::placed(lock, index, marker, held, (platform.inspect, platform.release)));
129}
130
131/// The path of the lock of an index.
132pub fn lock_path_of(index: &Path) -> PathBuf {
133    let mut name = index.as_os_str().to_os_string();
134    name.push(".lock");
135    return PathBuf::from(name);
136}
137
138/// What:
139///  Takes `index.lock` by exclusive creation, waiting for a foreign holder until the limit.
140/// Why:
141///   Git's own protocol: creation fails while any process holds the lock. The wait blocks on an inotify watch set before the
142///  first attempt, so a release between the attempt and the wait is not missed, and it wakes only for the lock itself. The
143///  marker is written once, before the first attempt, so the attempts themselves create and remove nothing in the watched
144///  directory. When the limit passes, nothing was changed; the clients learn who held the lock.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// await acquireIndexLock(index, marker, limit, announce)
149/// ```
150///
151/// # Errors
152///
153/// `LockFailure` with `IndexInUse` when the lock stayed held to the limit (or cannot be waited for), and with the storage
154///  code when the lock cannot be created.
155pub fn acquire(index: &Path, seq: u64, limit: Duration, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
156    return acquire_noting_holder(index, seq, limit, announce, &mut None);
157}
158
159/// What:
160///  `acquire`,
161/// leaving the words for the holder the evidence named in `holder` while the lock was waited for.
162/// Why:
163///   The landing's log line names who held the index when a commit waited for it,
164/// also when the wait ended in a success,
165///  which `acquire` alone reports to no one (the clients are told only when the wait fails).
166///
167/// In TS you'd write (pseudocode):
168/// ```ts
169/// await acquireIndexLock(index, marker, limit, announce, (who) => { holder = who })
170/// ```
171///
172/// # Errors
173///
174/// As `acquire`.
175pub fn acquire_noting_holder(index: &Path, seq: u64, limit: Duration, announce: &dyn Fn(Announce), holder: &mut Option<String>) -> Result<IndexLock, LockFailure> {
176    let mut notes: WaitNotes = WaitNotes::default();
177    let taken: Result<IndexLock, LockFailure> = acquire_placing(&SYSTEM, index, seq, limit, announce, &mut notes, None);
178    holder.clone_from(&notes.holder);
179    return taken;
180}
181
182/// What:
183///  `acquire`,
184/// leaving in `notes` what the wait was for:
185/// the holder's words,
186/// what the first and the last look at the held lock found,
187/// and how many looks there were.
188/// Why:
189///   The landing's log says who held the index when a commit waited for it,
190/// whatever the wait ended in.
191///
192/// In TS you'd write (pseudocode):
193/// ```ts
194/// await acquireIndexLock(index, marker, limit, announce, notes)
195/// ```
196///
197/// # Errors
198///
199/// As `acquire`.
200pub fn acquire_noting_wait(index: &Path, seq: u64, limit: Duration, announce: &dyn Fn(Announce), notes: &mut WaitNotes) -> Result<IndexLock, LockFailure> {
201    // The whole of the preparation counts as staging time: taking the marker made ahead, waiting for a flush that fails or is
202    //  for another batch, and the cold staging that follows; `acquire_placing` adds its own cold staging to this.
203    let preparing: std::time::Instant = std::time::Instant::now();
204    let prepared: Option<Prestaged> = take(index, seq);
205    notes.staging = preparing.elapsed();
206    return acquire_placing(&SYSTEM, index, seq, limit, announce, notes, prepared);
207}
208
209/// `acquire` with the calls of `platform`.
210///
211/// # Errors
212///
213/// As `acquire`;
214/// the log of the service says why a lock that could not be created or removed stays,
215/// and why a process with no birth identity takes none.
216pub fn acquire_on(platform: &Platform, index: &Path, seq: u64, limit: Duration, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
217    return acquire_placing(platform, index, seq, limit, announce, &mut WaitNotes::default(), None);
218}
219
220/// `acquire_noting_holder` with the calls of `platform`.
221fn acquire_placing(
222    platform: &Platform,
223    index: &Path,
224    seq: u64,
225    limit: Duration,
226    announce: &dyn Fn(Announce),
227    notes: &mut WaitNotes,
228    prestaged: Option<Prestaged>,
229) -> Result<IndexLock, LockFailure> {
230    let lock: PathBuf = lock_path_of(index);
231    let directory: &Path = lock.parent().unwrap_or(Path::new("."));
232    let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
233    let mut watch: Option<Watch> = (platform.watch)(directory);
234    let (marker, staged, held) = match prestaged {
235        Some(made) => {
236            // The file was flushed while the service was idle: what was waited for is what the preparation measured.
237            notes.stage_kind = "pre";
238            (made.marker, made.staged, made.held)
239        }
240        None => {
241            let marker: String = match marker_text(seq, platform.identity) {
242                Ok(text) => text,
243                Err(error) => {
244                    crate::service_error!("cannot take the index lock {}: this process has no birth identity to name in the lock ({error})", lock.display());
245                    return Err(LockFailure { code: FailureCode::Unavailable });
246                }
247            };
248            let staging_began: std::time::Instant = std::time::Instant::now();
249            let staged_result = stage_marker_synced(platform.sync, &lock, seq, marker.as_str());
250            notes.staging += staging_began.elapsed();
251            notes.stage_kind = "cold";
252            let (staged, held) = staged_result.map_err(|error| {
253                log_placement_failure(&lock, &error);
254                return LockFailure { code: super::service_land_follow::storage_failure(&error) };
255            })?;
256            (marker, staged, held)
257        }
258    };
259    let placing_began: std::time::Instant = std::time::Instant::now();
260    let outcome: Result<(), LockFailure> = wait_and_place(
261        platform,
262        &Placing { staged: &staged, held: &held, lock: &lock, lock_name: lock_name.as_os_str(), index, limit },
263        &mut watch,
264        announce,
265        notes,
266    );
267    notes.placing = placing_began.elapsed();
268    let _removed = std::fs::remove_file(&staged);
269    outcome?;
270    remove_dead_staging(&lock);
271    return Ok(IndexLock::placed(lock, index, marker, held, (platform.inspect, platform.release)));
272}
273
274/// What:
275///  Removes an index lock left by a service that was killed, when it still carries the service's marker for `seq` and its
276///  holder is gone.
277/// Why:
278///   Only the marker proves the lock is ours; a foreign lock, one a Git holds, or one whose holder still runs is never touched.
279///
280/// In TS you'd write (pseudocode):
281/// ```ts
282/// removeStaleLock(index, seq)
283/// ```
284pub fn remove_stale(index: &Path, seq: u64) {
285    let lock: PathBuf = lock_path_of(index);
286    if marker_of(lock.as_path()).is_some_and(|marked| { return marked.seq == seq && holder_is_gone(marked.holder.as_ref()); }) {
287        let _removed = std::fs::remove_file(&lock);
288    }
289}
290
291/// Index lock controls stay out of the release executable.
292#[cfg(test)]
293#[path = "service_index_lock_tests.rs"]
294mod tests;