Skip to main content

git_policy_cli/
owner_lock.rs

1//! What:
2//!  Rename-published owner-lock directories with process-birth ownership:
3//!  acquisition
4//!       with dead-owner retirement,
5//!  an unbounded wait while the owner lives,
6//!  and release by
7//!       rename before deletion.
8//! Why:
9//!  The landing,
10//!  reservation,
11//!  hook,
12//!  capture and push locks all work this way in the
13//!      incumbent (`src/owner-lock/owner-lock.ts`) and in its hook dispatcher,
14//!  and both
15//!      wrappers contend on the same directories while both are installed.
16//!  A published lock
17//!      always names its owner because its candidate is complete before the rename.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! await using lock = await acquireOwnerLock({ lockDirectory });
22//! ```
23
24/// The owner record,
25///  its reading and owner liveness.
26use super::owner_lock_record::{
27    OWNER_LOCK_RECORD_FILENAME, OwnerLockRecord, OwnerRecordError, PublishedOwner,
28    encode_owner_lock_record, owner_lock_holder_is_alive, read_owner_lock_record,
29};
30/// Private creation and tree removal.
31use super::private_storage::{create_private_directory, remove_tree, write_exclusive_synced};
32/// The PID namespace this process records as its own.
33use super::pid_namespace::current_pid_namespace;
34/// The current process's birth identity.
35use super::process_identity::{ProcessIdentityError, current_birth_identity};
36/// Unguessable tokens and unique candidate names.
37use super::random_id::{RandomSourceError, random_uuid};
38/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
39use std::path::{Path, PathBuf};
40/// When a wait's notices are due.
41use super::wait_progress::{Cadence, Ticker};
42/// `Duration` is a span of time; `Instant` is a point on a clock that never goes backwards.
43use std::time::{Duration, Instant};
44
45/// Delay between acquisition attempts while a live owner holds the lock.
46pub const DEFAULT_POLL_DELAY: Duration = Duration::from_millis(20);
47
48/// What:
49///  Why an owner-lock operation failed.
50/// Why:
51///   Each failure stops the caller with the lock named;
52///  none is a reason to delete a
53///       lock that might belong to a live owner.
54///
55/// In TS you'd write (pseudocode):
56/// ```ts
57/// class OwnerLockError extends Error {}
58/// ```
59#[derive(Debug)]
60pub enum OwnerLockError {
61    /// The blocking lock's record is malformed or unreadable.
62    Record(OwnerRecordError),
63    /// The current process has no identity,
64    ///  or an owner's liveness could not be decided.
65    Identity(ProcessIdentityError),
66    /// No random token could be made.
67    Random(RandomSourceError),
68    /// A filesystem step failed.
69    Io {
70        /// What the step was doing,
71        ///  in words.
72        operation: &'static str,
73        /// The path it acted on.
74        path: PathBuf,
75        /// The operating system's refusal.
76        error: std::io::Error,
77    },
78    /// The lock named another owner when this holder released it.
79    OwnershipChanged(PathBuf),
80    /// A live owner still held the lock when the caller's wait limit ran out.
81    WaitTimedOut {
82        /// The lock that stayed held.
83        path: PathBuf,
84        /// How long the caller waited.
85        waited: Duration,
86        /// The live holder's process ID,
87        ///  when its record could be read.
88        holder_pid: Option<i64>,
89    },
90}
91
92/// What:
93///  Human-readable text for each failure.
94/// Why:
95///   Diagnostics name the lock and the reason.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// error.message
100/// ```
101impl std::fmt::Display for OwnerLockError {
102    /// Writes one sentence naming the lock and what went wrong.
103    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            Self::Record(OwnerRecordError::Malformed(path)) => {
106                return write!(
107                    formatter,
108                    "the owner lock record {} is malformed",
109                    path.display()
110                );
111            }
112            Self::Record(OwnerRecordError::Unreadable(path, error)) => {
113                return write!(
114                    formatter,
115                    "the owner lock record {} could not be read: {error}",
116                    path.display()
117                );
118            }
119            Self::Identity(error) => return write!(formatter, "{error}"),
120            Self::Random(error) => return write!(formatter, "{}", error.0),
121            Self::Io {
122                operation,
123                path,
124                error,
125            } => {
126                return write!(formatter, "{operation} {} failed: {error}", path.display());
127            }
128            Self::OwnershipChanged(path) => {
129                return write!(
130                    formatter,
131                    "the owner lock {} changed owner while it was held",
132                    path.display()
133                );
134            }
135            Self::WaitTimedOut {
136                path,
137                waited,
138                holder_pid,
139            } => {
140                let holder: String = match holder_pid {
141                    Some(pid) => format!("process {pid}"),
142                    None => String::from("a process whose record could not be read"),
143                };
144                return write!(
145                    formatter,
146                    "gave up after waiting {} seconds for the owner lock {}, which {holder} still holds; \
147                     the lock was left in place. To go on, let that process finish and run the command again, \
148                     check with `ps` whether it is stuck and stop it if so, or raise the limit with \
149                     CLI_GIT_LOCK_WAIT_SECONDS (CLI_GIT_PUSH_LOCK_WAIT_SECONDS for the push lock)",
150                    waited.as_secs(),
151                    path.display()
152                );
153            }
154        }
155    }
156}
157
158/// What:
159///  Wrap a filesystem error with the step and path that failed.
160/// Why:
161///   One constructor keeps every filesystem failure named the same way.
162///
163/// In TS you'd write (pseudocode):
164/// ```ts
165/// new OwnerLockError(`${operation} ${path} failed: ${error.message}`)
166/// ```
167fn io_error(operation: &'static str, path: &Path, error: std::io::Error) -> OwnerLockError {
168    return OwnerLockError::Io {
169        operation,
170        path: path.to_path_buf(),
171        error,
172    };
173}
174
175/// What:
176///  Whether a rename failed because another directory already holds the name.
177/// Why:
178///   `rename` onto a non-empty directory fails with `EEXIST` or `ENOTEMPTY` on Unix and
179///       with an access-denied error on Windows (Node's `EPERM`);
180///  the incumbent's
181///       `isOccupiedError` accepts exactly these.
182///
183/// In TS you'd write (pseudocode):
184/// ```ts
185/// function isOccupiedError(error: unknown): boolean;
186/// ```
187pub fn is_occupied_error(error: &std::io::Error) -> bool {
188    return matches!(
189        error.kind(),
190        std::io::ErrorKind::AlreadyExists
191            | std::io::ErrorKind::DirectoryNotEmpty
192            | std::io::ErrorKind::PermissionDenied
193    );
194}
195
196/// What:
197///  A unique sibling name `<lock>.<uuid>.<suffix>` for a candidate or a retired lock.
198/// Why:
199///   Candidates (`.pending`) and retired locks (`.stale`) never collide with each other,
200///       and every reader ignores both names.
201///
202/// In TS you'd write (pseudocode):
203/// ```ts
204/// `${lockDirectory}.${randomUUID()}.${suffix}`
205/// ```
206fn sibling_name(lock_directory: &Path, suffix: &str) -> Result<PathBuf, OwnerLockError> {
207    let unique: String = random_uuid().map_err(OwnerLockError::Random)?;
208    // `as_os_str().to_owned()` copies the raw path bytes so the suffix can be appended.
209    let mut name: std::ffi::OsString = lock_directory.as_os_str().to_owned();
210    name.push(format!(".{unique}.{suffix}"));
211    return Ok(PathBuf::from(name));
212}
213
214/// What:
215///  Read a lock's owner,
216///  turning record failures into lock failures.
217/// Why:
218///   Every step of acquisition and release reads the owner the same way.
219///
220/// In TS you'd write (pseudocode):
221/// ```ts
222/// await readOwnerLockRecord(lockDirectory)
223/// ```
224fn read_owner(lock_directory: &Path) -> Result<PublishedOwner, OwnerLockError> {
225    return read_owner_lock_record(lock_directory).map_err(OwnerLockError::Record);
226}
227
228/// What:
229///  A held owner lock.
230///  Releasing it proves the lock still names this holder first.
231///       Dropping an unreleased lock releases it and ignores a failure.
232/// Why:
233///   An early return or an unwinding error must not leave a live-looking lock behind;
234///       an explicit `release` reports a changed owner.
235///
236/// In TS you'd write (pseudocode):
237/// ```ts
238/// type OwnerLock = AsyncDisposable & { lockDirectory: string; token: string };
239/// ```
240#[derive(Debug)]
241pub struct OwnerLock {
242    /// Published lock directory.
243    directory: PathBuf,
244    /// Token published in the lock.
245    token: String,
246    /// Whether the lock was already released.
247    released: bool,
248}
249
250/// Accessors and release of a held lock.
251impl OwnerLock {
252    /// What:
253    ///  A view of a lock an ancestor process holds, which releases nothing.
254    /// Why:
255    ///   An invocation nested under a landing-lock holder proves it with an inherited lease and proceeds under the
256    ///  ancestor's lock instead of waiting for it (`inheritedLandingLock` in the incumbent).
257    ///
258    /// In TS you'd write (pseudocode):
259    /// ```ts
260    /// { lockDirectory, token: process.env[LANDING_LEASE_ENV], [Symbol.asyncDispose]: async () => {} }
261    /// ```
262    pub fn inherited(directory: &Path, token: &str) -> OwnerLock {
263        return OwnerLock {
264            directory: directory.to_path_buf(),
265            token: String::from(token),
266            released: true,
267        };
268    }
269
270    /// What:
271    ///  The published lock directory.
272    /// Why:
273    ///   Leases and diagnostics name the lock.
274    ///
275    /// In TS you'd write (pseudocode):
276    /// ```ts
277    /// lock.lockDirectory
278    /// ```
279    pub fn directory(&self) -> &Path {
280        return self.directory.as_path();
281    }
282
283    /// What:
284    ///  The token this holder published.
285    /// Why:
286    ///   A lease proves which acquisition it belongs to.
287    ///
288    /// In TS you'd write (pseudocode):
289    /// ```ts
290    /// lock.token
291    /// ```
292    pub fn token(&self) -> &str {
293        return self.token.as_str();
294    }
295
296    /// What:
297    ///  Release the lock,
298    ///  failing when it no longer names this holder.
299    ///       `mut self` takes ownership:
300    ///  a released lock cannot be used again.
301    /// Why:
302    ///   The release is the incumbent's `releaseHeldLock`.
303    ///
304    /// In TS you'd write (pseudocode):
305    /// ```ts
306    /// await lock[Symbol.asyncDispose]();
307    /// ```
308    pub fn release(mut self) -> Result<(), OwnerLockError> {
309        // A view of an ancestor's lock was never taken by this holder, so it has nothing to release.
310        if self.released {
311            return Ok(());
312        }
313        self.released = true;
314        return release_held_lock(self.directory.as_path(), self.token.as_str());
315    }
316}
317
318/// Dropping an unreleased lock releases it.
319impl Drop for OwnerLock {
320    /// What:
321    ///  Release the lock unless `release` already did;
322    ///  a failure is ignored.
323    /// Why:
324    ///   Nothing can be reported from a drop,
325    ///  and a lock that changed owner is not this
326    ///       holder's to remove.
327    ///
328    /// In TS you'd write (pseudocode):
329    /// ```ts
330    /// // `await using` disposal on an early exit.
331    /// ```
332    fn drop(&mut self) {
333        if !self.released {
334            self.released = true;
335            let _ = release_held_lock(self.directory.as_path(), self.token.as_str());
336        }
337    }
338}
339
340/// What:
341///  Move a renamed-away directory back to the published name when it turned out to
342///       belong to someone else.
343/// Why:
344///   A race can move a live replacement aside;
345///  putting it back is all that can be done,
346///       and an occupied name means a newer owner already published,
347///  which is left alone.
348///
349/// In TS you'd write (pseudocode):
350/// ```ts
351/// try { await rename(moved, lockDirectory); } catch (e) { if (!isOccupiedError(e)) throw e; }
352/// ```
353fn restore_moved(moved: &Path, lock_directory: &Path) -> Result<(), OwnerLockError> {
354    match std::fs::rename(moved, lock_directory) {
355        Ok(()) => return Ok(()),
356        Err(error) if is_occupied_error(&error) => return Ok(()),
357        Err(error) => return Err(io_error("restoring the owner lock", lock_directory, error)),
358    }
359}
360
361/// What:
362///  Release a held lock by renaming it away,
363///  then deleting the renamed copy.
364/// Why:
365///   Emptying the published directory in place would let a concurrent acquirer's rename
366///       replace it mid-removal and lose the next owner's record;
367///  the rename frees the name
368///       atomically.
369///  A lock that names another owner before or after the move is reported.
370///
371/// In TS you'd write (pseudocode):
372/// ```ts
373/// async function releaseHeldLock({ lockDirectory, token }): Promise<void>;
374/// ```
375fn release_held_lock(lock_directory: &Path, token: &str) -> Result<(), OwnerLockError> {
376    if !names_token(&read_owner(lock_directory)?, token) {
377        return Err(OwnerLockError::OwnershipChanged(
378            lock_directory.to_path_buf(),
379        ));
380    }
381    let retired: PathBuf = sibling_name(lock_directory, "stale")?;
382    if let Err(error) = std::fs::rename(lock_directory, retired.as_path()) {
383        return Err(io_error("releasing the owner lock", lock_directory, error));
384    }
385    return settle_moved_release(retired.as_path(), lock_directory, token);
386}
387
388/// What:
389///  Finish a release after the lock was renamed away:
390///  delete it when it is this holder's,
391///       or put it back and report the change when a race moved someone else's lock.
392/// Why:
393///   Between the owner check and the rename another process may have replaced the lock;
394///       the moved copy is checked again before anything is deleted.
395///
396/// In TS you'd write (pseudocode):
397/// ```ts
398/// const moved = await readOwnerLockRecord(retired); if (moved.token !== token) { await restore(); throw ...; } await rm(retired);
399/// ```
400fn settle_moved_release(
401    retired: &Path,
402    lock_directory: &Path,
403    token: &str,
404) -> Result<(), OwnerLockError> {
405    if !names_token(&read_owner(retired)?, token) {
406        restore_moved(retired, lock_directory)?;
407        return Err(OwnerLockError::OwnershipChanged(
408            lock_directory.to_path_buf(),
409        ));
410    }
411    if let Err(error) = remove_tree(retired) {
412        return Err(io_error("removing the released lock", retired, error));
413    }
414    return Ok(());
415}
416
417/// What:
418///  Whether a read owner is present and published `token`.
419/// Why:
420///   Release and retirement both ask whether the lock still holds a given acquisition.
421///
422/// In TS you'd write (pseudocode):
423/// ```ts
424/// (owner !== LOCK_BUSY) && (owner.token === token)
425/// ```
426fn names_token(owner: &PublishedOwner, token: &str) -> bool {
427    match owner {
428        PublishedOwner::Owner(record) => return record.token == token,
429        PublishedOwner::Busy => return false,
430    }
431}
432
433/// What:
434///  Retire a lock whose owner was found dead,
435///  unless it changed owner meanwhile.
436/// Why:
437///   The liveness check took time,
438///  during which the dead-looking owner may have released
439///       and a live acquirer published.
440///  The lock is renamed to a unique stale name,
441///  and a live
442///       replacement moved by the race is restored.
443///  The caller retries afterwards either way.
444///
445/// In TS you'd write (pseudocode):
446/// ```ts
447/// async function retireDeadLock({ lockDirectory, deadToken }): Promise<typeof LOCK_BUSY>;
448/// ```
449fn retire_dead_lock(lock_directory: &Path, dead_token: &str) -> Result<(), OwnerLockError> {
450    return retire_dead_lock_with(lock_directory, dead_token, rename_directory);
451}
452
453/// What:
454///  Rename a directory on the real filesystem.
455/// Why:
456///   `std::fs::rename` is generic over its path types, so the retirement's injectable step needs this plain function.
457///
458/// In TS you'd write (pseudocode):
459/// ```ts
460/// await rename(from, to);
461/// ```
462fn rename_directory(from: &Path, to: &Path) -> std::io::Result<()> {
463    return std::fs::rename(from, to);
464}
465
466/// What:
467///  Retire a dead owner's lock with the rename step supplied by the caller.
468/// Why:
469///   The lock can vanish between the owner check and the rename, a window only a racing process opens;
470///  a rename that fails the way a vanished lock fails lets a test reach that arm without a race.
471///
472/// In TS you'd write (pseudocode):
473/// ```ts
474/// async function retireDeadLockWith({ lockDirectory, deadToken, rename }): Promise<void>;
475/// ```
476fn retire_dead_lock_with(
477    lock_directory: &Path,
478    dead_token: &str,
479    rename: fn(&Path, &Path) -> std::io::Result<()>,
480) -> Result<(), OwnerLockError> {
481    if !names_token(&read_owner(lock_directory)?, dead_token) {
482        return Ok(());
483    }
484    let stale: PathBuf = sibling_name(lock_directory, "stale")?;
485    match rename(lock_directory, stale.as_path()) {
486        Ok(()) => {}
487        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
488        Err(error) => {
489            return Err(io_error(
490                "retiring the dead owner lock",
491                lock_directory,
492                error,
493            ));
494        }
495    }
496    return settle_moved_retirement(stale.as_path(), lock_directory, dead_token);
497}
498
499/// What:
500///  Finish a retirement after the lock was renamed away:
501///  delete the dead owner's lock,
502///       or put back a live replacement that a race moved instead.
503/// Why:
504///   A concurrent acquirer may have published between the owner check and the rename;
505///  a
506///       moved lock that names another token is not the dead owner's to delete.
507///
508/// In TS you'd write (pseudocode):
509/// ```ts
510/// const moved = await readOwnerLockRecord(stale); if (moved !== LOCK_BUSY && moved.token !== deadToken) { await restore(); return; } await rm(stale);
511/// ```
512fn settle_moved_retirement(
513    stale: &Path,
514    lock_directory: &Path,
515    dead_token: &str,
516) -> Result<(), OwnerLockError> {
517    if let PublishedOwner::Owner(record) = read_owner(stale)?
518        && record.token != dead_token
519    {
520        return restore_moved(stale, lock_directory);
521    }
522    if let Err(error) = remove_tree(stale) {
523        return Err(io_error("removing the retired lock", stale, error));
524    }
525    return Ok(());
526}
527
528/// What:
529///  Write a complete unpublished candidate:
530///  a private directory and its owner record.
531/// Why:
532///   Publication by rename makes a lock appear with its owner record already complete.
533///
534/// In TS you'd write (pseudocode):
535/// ```ts
536/// async function writeCandidate({ candidateDirectory, record }): Promise<void>;
537/// ```
538fn write_candidate(candidate: &Path, record: &OwnerLockRecord) -> Result<(), OwnerLockError> {
539    if let Err(error) = create_private_directory(candidate) {
540        return Err(io_error("creating the lock candidate", candidate, error));
541    }
542    let record_path: PathBuf = candidate.join(OWNER_LOCK_RECORD_FILENAME);
543    let bytes: String = encode_owner_lock_record(record);
544    if let Err(error) = write_exclusive_synced(record_path.as_path(), bytes.as_bytes()) {
545        // The candidate was never published and nothing else names it, so it is removed with the failure: left
546        // beside the lock, it would stay for good.
547        if let Err(removal) = remove_tree(candidate) {
548            super::diagnostic_log::debug(
549                "writeCandidate",
550                format!("removing the unpublished lock candidate {} failed: {removal}", candidate.display()).as_str(),
551            );
552        }
553        return Err(io_error(
554            "writing the lock record",
555            record_path.as_path(),
556            error,
557        ));
558    }
559    return Ok(());
560}
561
562/// What:
563///  One publication attempt:
564///  publish a fresh candidate,
565///  or retire a dead owner's lock
566///       that blocks it.
567///  `Ok(None)` means busy:
568///  try again.
569/// Why:
570///   The incumbent's `attemptAcquire`,
571///  step for step.
572///
573/// In TS you'd write (pseudocode):
574/// ```ts
575/// async function attemptAcquire({ lockDirectory, record }): Promise<OwnerLock | typeof LOCK_BUSY>;
576/// ```
577fn attempt_acquire(
578    lock_directory: &Path,
579    record: &OwnerLockRecord,
580) -> Result<Option<OwnerLock>, OwnerLockError> {
581    let candidate: PathBuf = sibling_name(lock_directory, "pending")?;
582    write_candidate(candidate.as_path(), record)?;
583    match std::fs::rename(candidate.as_path(), lock_directory) {
584        Ok(()) => {
585            return Ok(Some(OwnerLock {
586                directory: lock_directory.to_path_buf(),
587                token: record.token.clone(),
588                released: false,
589            }));
590        }
591        Err(error) => {
592            if let Err(removal) = remove_tree(candidate.as_path()) {
593                return Err(io_error(
594                    "removing the lock candidate",
595                    candidate.as_path(),
596                    removal,
597                ));
598            }
599            if !is_occupied_error(&error) {
600                return Err(io_error("publishing the owner lock", lock_directory, error));
601            }
602        }
603    }
604    let PublishedOwner::Owner(published) = read_owner(lock_directory)? else {
605        return Ok(None);
606    };
607    if owner_lock_holder_is_alive(&published, lock_directory).map_err(OwnerLockError::Identity)? {
608        return Ok(None);
609    }
610    retire_dead_lock(lock_directory, published.token.as_str())?;
611    return Ok(None);
612}
613
614/// What:
615///  A fresh owner record for the current process.
616/// Why:
617///   Every acquisition publishes a new token with this process's PID and birth identity.
618///
619/// In TS you'd write (pseudocode):
620/// ```ts
621/// async function currentOwnerRecord(transactionId?: string): Promise<OwnerLockRecord>;
622/// ```
623fn current_owner_record(transaction_id: Option<&str>) -> Result<OwnerLockRecord, OwnerLockError> {
624    return Ok(OwnerLockRecord {
625        token: random_uuid().map_err(OwnerLockError::Random)?,
626        owner_pid: i64::from(std::process::id()),
627        owner_birth_identity: current_birth_identity().map_err(OwnerLockError::Identity)?,
628        // `.map(String::from)` copies the borrowed ID into owned text when there is one.
629        transaction_id: transaction_id.map(String::from),
630        owner_pid_namespace: current_pid_namespace(),
631    });
632}
633
634/// What:
635///  One publication attempt without waiting.
636///  `Ok(None)` means another owner holds the
637///       lock,
638///  or a dead owner's lock was just retired and the caller should try again.
639/// Why:
640///   The reservation is taken this way,
641///  by a requester that re-checks its turn between
642///       attempts.
643///
644/// In TS you'd write (pseudocode):
645/// ```ts
646/// async function tryAcquireOwnerLock({ lockDirectory, transactionId }): Promise<OwnerLock | typeof LOCK_BUSY>;
647/// ```
648pub fn try_acquire_owner_lock(
649    lock_directory: &Path,
650    transaction_id: Option<&str>,
651) -> Result<Option<OwnerLock>, OwnerLockError> {
652    return attempt_acquire(lock_directory, &current_owner_record(transaction_id)?);
653}
654
655/// What:
656///  Retire a lock whose recorded owner is dead;
657///  an absent lock or a live owner's lock is
658///       left alone.
659/// Why:
660///   Recovery retires a dead reservation holder's lock this way.
661///
662/// In TS you'd write (pseudocode):
663/// ```ts
664/// async function retireLockOfDeadOwner(lockDirectory: string): Promise<void>;
665/// ```
666pub fn retire_lock_of_dead_owner(lock_directory: &Path) -> Result<(), OwnerLockError> {
667    let PublishedOwner::Owner(published) = read_owner(lock_directory)? else {
668        return Ok(());
669    };
670    if owner_lock_holder_is_alive(&published, lock_directory).map_err(OwnerLockError::Identity)? {
671        return Ok(());
672    }
673    return retire_dead_lock(lock_directory, published.token.as_str());
674}
675
676/// What:
677///  Something told once that an acquirer has to wait,
678///  such as a stderr notice.
679///       A `trait` is an interface a type implements.
680/// Why:
681///   A waiter may tell the user whose lock it waits for;
682///  the lock module stays silent.
683///
684/// In TS you'd write (pseudocode):
685/// ```ts
686/// type OnWait = () => void;
687/// ```
688pub trait WaitNotice {
689    /// Called once,
690    ///  when the first attempt finds the lock held.
691    fn waiting(&mut self);
692
693    /// Called when the wait has lasted the cadence's grace,
694    ///  and again at every interval after it,
695    ///  with how long the wait has lasted.
696    ///  Brief contention never reaches it;
697    ///  a notice that only needs the first moment ignores it.
698    fn slow(&mut self, _waited: Duration) {}
699
700    /// Whether `waiting` already said so out loud,
701    ///  so the first `slow` call comes one interval after the start instead of after the grace.
702    fn announces_at_start(&self) -> bool {
703        return false;
704    }
705}
706
707/// What:
708///  The notice that says nothing.
709/// Why:
710///   Most acquirers wait silently,
711///  as the incumbent's default.
712///
713/// In TS you'd write (pseudocode):
714/// ```ts
715/// const onWait = undefined;
716/// ```
717#[derive(Clone, Copy, Debug, Default)]
718pub struct SilentWait;
719
720/// Waiting silently does nothing.
721impl WaitNotice for SilentWait {
722    /// Does nothing.
723    fn waiting(&mut self) {}
724}
725
726/// What:
727///  Acquire a lock,
728///  waiting at most `limit` while a live owner holds it,
729///  and telling `notice.slow` once the wait has lasted the cadence's grace and again at every interval.
730/// Why:
731///   No acquisition can wait forever,
732///  so a holder that is alive but stuck fails its waiters instead of stalling them.
733///  The elapsed time is measured on a clock that never goes backwards,
734///  and tests choose a short cadence instead of waiting out the standard one.
735///
736/// In TS you'd write (pseudocode):
737/// ```ts
738/// async function acquireWithin({ lockDirectory, pollDelayMs, onWait, limitMs, cadence }): Promise<OwnerLock>;
739/// ```
740pub fn acquire_within(
741    lock_directory: &Path,
742    poll_delay: Duration,
743    notice: &mut dyn WaitNotice,
744    limit: Duration,
745    cadence: Cadence,
746) -> Result<OwnerLock, OwnerLockError> {
747    let record: OwnerLockRecord = current_owner_record(None)?;
748    let started: Instant = Instant::now();
749    // `mut` lets the loop remember that the wait was announced and when its next notice is due.
750    let mut notified: bool = false;
751    let mut ticker: Ticker = if notice.announces_at_start() {
752        Ticker::announced_at_start(cadence)
753    } else {
754        Ticker::new(cadence)
755    };
756    loop {
757        if let Some(lock) = attempt_acquire(lock_directory, &record)? {
758            return Ok(lock);
759        }
760        if !notified {
761            notified = true;
762            notice.waiting();
763        }
764        if ticker.due(started.elapsed()) {
765            notice.slow(started.elapsed());
766        }
767        if started.elapsed() >= limit {
768            let holder_pid: Option<i64> = match read_owner(lock_directory) {
769                Ok(PublishedOwner::Owner(published)) => Some(published.owner_pid),
770                Ok(PublishedOwner::Busy) | Err(_) => None,
771            };
772            return Err(OwnerLockError::WaitTimedOut {
773                path: lock_directory.to_path_buf(),
774                waited: started.elapsed(),
775                holder_pid,
776            });
777        }
778        std::thread::sleep(poll_delay);
779    }
780}
781
782/// What:
783///  Acquire a lock in a test,
784///  waiting at most `TEST_ACQUIRE_LIMIT` while a live owner holds it.
785///       `notice` is told once,
786///  when the first attempt finds the lock busy;
787///       `&mut dyn WaitNotice` lends any value implementing that interface.
788/// Why:
789///   Tests take locks to set up or to prove a release;
790///  a lock a mutant never releases must fail the test within the limit,
791///  not hang it until the mutation run's timeout.
792///  A dead owner's lock is retired on the next attempt.
793///
794/// In TS you'd write (pseudocode):
795/// ```ts
796/// async function acquireOwnerLock({ lockDirectory, pollDelayMs = 20, onWait }): Promise<OwnerLock>;
797/// ```
798#[cfg(test)]
799pub fn acquire_owner_lock(
800    lock_directory: &Path,
801    poll_delay: Duration,
802    notice: &mut dyn WaitNotice,
803) -> Result<OwnerLock, OwnerLockError> {
804    return acquire_within(lock_directory, poll_delay, notice, TEST_ACQUIRE_LIMIT, Cadence::standard());
805}
806
807/// How long a test waits for a lock before failing.
808#[cfg(test)]
809const TEST_ACQUIRE_LIMIT: Duration = Duration::from_secs(20);
810
811/// What:
812///  Acquire a lock,
813///  giving up with `OwnerLockError::WaitTimedOut` once `limit` has passed while a live owner holds it.
814/// Why:
815///   A holder that is alive but stuck would otherwise stall every waiter forever.
816///  The held lock is never taken over,
817///  because a live owner may still be inside its critical section;
818///  the caller decides what a refusal means.
819///
820/// In TS you'd write (pseudocode):
821/// ```ts
822/// async function acquireOwnerLockFor({ lockDirectory, pollDelayMs, onWait, limitMs }): Promise<OwnerLock>; // rejects after limitMs
823/// ```
824pub fn acquire_owner_lock_for(
825    lock_directory: &Path,
826    poll_delay: Duration,
827    notice: &mut dyn WaitNotice,
828    limit: Duration,
829) -> Result<OwnerLock, OwnerLockError> {
830    return acquire_within(lock_directory, poll_delay, notice, limit, Cadence::from_environment());
831}
832
833/// Lock contention,
834///  retirement and release controls stay out of the release executable.
835#[cfg(test)]
836#[path = "owner_lock_tests.rs"]
837mod tests;