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