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/// `Duration` is a span of time; `Instant` is a point on a clock that never goes backwards.
41use std::time::{Duration, Instant};
42
43/// Delay between acquisition attempts while a live owner holds the lock.
44pub const DEFAULT_POLL_DELAY: Duration = Duration::from_millis(20);
45
46/// How long a wait lasts before `WaitNotice::slow` runs:
47///  long enough that ordinary contention stays silent.
48pub const SLOW_WAIT: Duration = Duration::from_secs(5);
49
50/// What:
51///  Why an owner-lock operation failed.
52/// Why:
53///   Each failure stops the caller with the lock named;
54///  none is a reason to delete a
55///       lock that might belong to a live owner.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// class OwnerLockError extends Error {}
60/// ```
61#[derive(Debug)]
62pub enum OwnerLockError {
63    /// The blocking lock's record is malformed or unreadable.
64    Record(OwnerRecordError),
65    /// The current process has no identity,
66    ///  or an owner's liveness could not be decided.
67    Identity(ProcessIdentityError),
68    /// No random token could be made.
69    Random(RandomSourceError),
70    /// A filesystem step failed.
71    Io {
72        /// What the step was doing,
73        ///  in words.
74        operation: &'static str,
75        /// The path it acted on.
76        path: PathBuf,
77        /// The operating system's refusal.
78        error: std::io::Error,
79    },
80    /// The lock named another owner when this holder released it.
81    OwnershipChanged(PathBuf),
82    /// A live owner still held the lock when the caller's wait limit ran out.
83    WaitTimedOut {
84        /// The lock that stayed held.
85        path: PathBuf,
86        /// How long the caller waited.
87        waited: Duration,
88        /// The live holder's process ID,
89        ///  when its record could be read.
90        holder_pid: Option<i64>,
91    },
92}
93
94/// What:
95///  Human-readable text for each failure.
96/// Why:
97///   Diagnostics name the lock and the reason.
98///
99/// In TS you'd write (pseudocode):
100/// ```ts
101/// error.message
102/// ```
103impl std::fmt::Display for OwnerLockError {
104    /// Writes one sentence naming the lock and what went wrong.
105    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            Self::Record(OwnerRecordError::Malformed(path)) => {
108                return write!(
109                    formatter,
110                    "the owner lock record {} is malformed",
111                    path.display()
112                );
113            }
114            Self::Record(OwnerRecordError::Unreadable(path, error)) => {
115                return write!(
116                    formatter,
117                    "the owner lock record {} could not be read: {error}",
118                    path.display()
119                );
120            }
121            Self::Identity(error) => return write!(formatter, "{error}"),
122            Self::Random(error) => return write!(formatter, "{}", error.0),
123            Self::Io {
124                operation,
125                path,
126                error,
127            } => {
128                return write!(formatter, "{operation} {} failed: {error}", path.display());
129            }
130            Self::OwnershipChanged(path) => {
131                return write!(
132                    formatter,
133                    "the owner lock {} changed owner while it was held",
134                    path.display()
135                );
136            }
137            Self::WaitTimedOut {
138                path,
139                waited,
140                holder_pid,
141            } => {
142                let holder: String = match holder_pid {
143                    Some(pid) => format!("process {pid}"),
144                    None => String::from("a process whose record could not be read"),
145                };
146                return write!(
147                    formatter,
148                    "gave up after waiting {} seconds for the owner lock {}, which {holder} still holds; \
149                     the lock was left in place",
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        return Err(io_error(
546            "writing the lock record",
547            record_path.as_path(),
548            error,
549        ));
550    }
551    return Ok(());
552}
553
554/// What:
555///  One publication attempt:
556///  publish a fresh candidate,
557///  or retire a dead owner's lock
558///       that blocks it.
559///  `Ok(None)` means busy:
560///  try again.
561/// Why:
562///   The incumbent's `attemptAcquire`,
563///  step for step.
564///
565/// In TS you'd write (pseudocode):
566/// ```ts
567/// async function attemptAcquire({ lockDirectory, record }): Promise<OwnerLock | typeof LOCK_BUSY>;
568/// ```
569fn attempt_acquire(
570    lock_directory: &Path,
571    record: &OwnerLockRecord,
572) -> Result<Option<OwnerLock>, OwnerLockError> {
573    let candidate: PathBuf = sibling_name(lock_directory, "pending")?;
574    write_candidate(candidate.as_path(), record)?;
575    match std::fs::rename(candidate.as_path(), lock_directory) {
576        Ok(()) => {
577            return Ok(Some(OwnerLock {
578                directory: lock_directory.to_path_buf(),
579                token: record.token.clone(),
580                released: false,
581            }));
582        }
583        Err(error) => {
584            if let Err(removal) = remove_tree(candidate.as_path()) {
585                return Err(io_error(
586                    "removing the lock candidate",
587                    candidate.as_path(),
588                    removal,
589                ));
590            }
591            if !is_occupied_error(&error) {
592                return Err(io_error("publishing the owner lock", lock_directory, error));
593            }
594        }
595    }
596    let PublishedOwner::Owner(published) = read_owner(lock_directory)? else {
597        return Ok(None);
598    };
599    if owner_lock_holder_is_alive(&published, lock_directory).map_err(OwnerLockError::Identity)? {
600        return Ok(None);
601    }
602    retire_dead_lock(lock_directory, published.token.as_str())?;
603    return Ok(None);
604}
605
606/// What:
607///  A fresh owner record for the current process.
608/// Why:
609///   Every acquisition publishes a new token with this process's PID and birth identity.
610///
611/// In TS you'd write (pseudocode):
612/// ```ts
613/// async function currentOwnerRecord(transactionId?: string): Promise<OwnerLockRecord>;
614/// ```
615fn current_owner_record(transaction_id: Option<&str>) -> Result<OwnerLockRecord, OwnerLockError> {
616    return Ok(OwnerLockRecord {
617        token: random_uuid().map_err(OwnerLockError::Random)?,
618        owner_pid: i64::from(std::process::id()),
619        owner_birth_identity: current_birth_identity().map_err(OwnerLockError::Identity)?,
620        // `.map(String::from)` copies the borrowed ID into owned text when there is one.
621        transaction_id: transaction_id.map(String::from),
622        owner_pid_namespace: current_pid_namespace(),
623    });
624}
625
626/// What:
627///  One publication attempt without waiting.
628///  `Ok(None)` means another owner holds the
629///       lock,
630///  or a dead owner's lock was just retired and the caller should try again.
631/// Why:
632///   The reservation is taken this way,
633///  by a requester that re-checks its turn between
634///       attempts.
635///
636/// In TS you'd write (pseudocode):
637/// ```ts
638/// async function tryAcquireOwnerLock({ lockDirectory, transactionId }): Promise<OwnerLock | typeof LOCK_BUSY>;
639/// ```
640pub fn try_acquire_owner_lock(
641    lock_directory: &Path,
642    transaction_id: Option<&str>,
643) -> Result<Option<OwnerLock>, OwnerLockError> {
644    return attempt_acquire(lock_directory, &current_owner_record(transaction_id)?);
645}
646
647/// What:
648///  Retire a lock whose recorded owner is dead;
649///  an absent lock or a live owner's lock is
650///       left alone.
651/// Why:
652///   Recovery retires a dead reservation holder's lock this way.
653///
654/// In TS you'd write (pseudocode):
655/// ```ts
656/// async function retireLockOfDeadOwner(lockDirectory: string): Promise<void>;
657/// ```
658pub fn retire_lock_of_dead_owner(lock_directory: &Path) -> Result<(), OwnerLockError> {
659    let PublishedOwner::Owner(published) = read_owner(lock_directory)? else {
660        return Ok(());
661    };
662    if owner_lock_holder_is_alive(&published, lock_directory).map_err(OwnerLockError::Identity)? {
663        return Ok(());
664    }
665    return retire_dead_lock(lock_directory, published.token.as_str());
666}
667
668/// What:
669///  Something told once that an acquirer has to wait,
670///  such as a stderr notice.
671///       A `trait` is an interface a type implements.
672/// Why:
673///   A waiter may tell the user whose lock it waits for;
674///  the lock module stays silent.
675///
676/// In TS you'd write (pseudocode):
677/// ```ts
678/// type OnWait = () => void;
679/// ```
680pub trait WaitNotice {
681    /// Called once,
682    ///  when the first attempt finds the lock held.
683    fn waiting(&mut self);
684
685    /// Called once,
686    ///  when the wait has lasted `SLOW_WAIT`,
687    ///  with how long it has lasted.
688    ///  Brief contention never reaches it;
689    ///  a notice that only needs the first moment ignores it.
690    fn slow(&mut self, _waited: Duration) {}
691}
692
693/// What:
694///  The notice that says nothing.
695/// Why:
696///   Most acquirers wait silently,
697///  as the incumbent's default.
698///
699/// In TS you'd write (pseudocode):
700/// ```ts
701/// const onWait = undefined;
702/// ```
703#[derive(Clone, Copy, Debug, Default)]
704pub struct SilentWait;
705
706/// Waiting silently does nothing.
707impl WaitNotice for SilentWait {
708    /// Does nothing.
709    fn waiting(&mut self) {}
710}
711
712/// What:
713///  Acquire a lock,
714///  waiting at most `limit` while a live owner holds it,
715///  and telling `notice.slow` once the wait has lasted `slow_after`.
716/// Why:
717///   No acquisition can wait forever,
718///  so a holder that is alive but stuck fails its waiters instead of stalling them.
719///  The elapsed time is measured on a clock that never goes backwards,
720///  and tests choose a short `slow_after` instead of waiting out `SLOW_WAIT`.
721///
722/// In TS you'd write (pseudocode):
723/// ```ts
724/// async function acquireWithin({ lockDirectory, pollDelayMs, onWait, limitMs }): Promise<OwnerLock>;
725/// ```
726pub fn acquire_within(
727    lock_directory: &Path,
728    poll_delay: Duration,
729    notice: &mut dyn WaitNotice,
730    limit: Duration,
731    slow_after: Duration,
732) -> Result<OwnerLock, OwnerLockError> {
733    let record: OwnerLockRecord = current_owner_record(None)?;
734    let started: Instant = Instant::now();
735    // `mut` lets the loop remember that the wait was announced.
736    let mut notified: bool = false;
737    let mut slow_noticed: bool = false;
738    loop {
739        if let Some(lock) = attempt_acquire(lock_directory, &record)? {
740            return Ok(lock);
741        }
742        if !notified {
743            notified = true;
744            notice.waiting();
745        }
746        if !slow_noticed && started.elapsed() >= slow_after {
747            slow_noticed = true;
748            notice.slow(started.elapsed());
749        }
750        if started.elapsed() >= limit {
751            let holder_pid: Option<i64> = match read_owner(lock_directory) {
752                Ok(PublishedOwner::Owner(published)) => Some(published.owner_pid),
753                Ok(PublishedOwner::Busy) | Err(_) => None,
754            };
755            return Err(OwnerLockError::WaitTimedOut {
756                path: lock_directory.to_path_buf(),
757                waited: started.elapsed(),
758                holder_pid,
759            });
760        }
761        std::thread::sleep(poll_delay);
762    }
763}
764
765/// What:
766///  Acquire a lock in a test,
767///  waiting at most `TEST_ACQUIRE_LIMIT` while a live owner holds it.
768///       `notice` is told once,
769///  when the first attempt finds the lock busy;
770///       `&mut dyn WaitNotice` lends any value implementing that interface.
771/// Why:
772///   Tests take locks to set up or to prove a release;
773///  a lock a mutant never releases must fail the test within the limit,
774///  not hang it until the mutation run's timeout.
775///  A dead owner's lock is retired on the next attempt.
776///
777/// In TS you'd write (pseudocode):
778/// ```ts
779/// async function acquireOwnerLock({ lockDirectory, pollDelayMs = 20, onWait }): Promise<OwnerLock>;
780/// ```
781#[cfg(test)]
782pub fn acquire_owner_lock(
783    lock_directory: &Path,
784    poll_delay: Duration,
785    notice: &mut dyn WaitNotice,
786) -> Result<OwnerLock, OwnerLockError> {
787    return acquire_within(lock_directory, poll_delay, notice, TEST_ACQUIRE_LIMIT, SLOW_WAIT);
788}
789
790/// How long a test waits for a lock before failing.
791#[cfg(test)]
792const TEST_ACQUIRE_LIMIT: Duration = Duration::from_secs(20);
793
794/// What:
795///  Acquire a lock,
796///  giving up with `OwnerLockError::WaitTimedOut` once `limit` has passed while a live owner holds it.
797/// Why:
798///   A holder that is alive but stuck would otherwise stall every waiter forever.
799///  The held lock is never taken over,
800///  because a live owner may still be inside its critical section;
801///  the caller decides what a refusal means.
802///
803/// In TS you'd write (pseudocode):
804/// ```ts
805/// async function acquireOwnerLockFor({ lockDirectory, pollDelayMs, onWait, limitMs }): Promise<OwnerLock>; // rejects after limitMs
806/// ```
807pub fn acquire_owner_lock_for(
808    lock_directory: &Path,
809    poll_delay: Duration,
810    notice: &mut dyn WaitNotice,
811    limit: Duration,
812) -> Result<OwnerLock, OwnerLockError> {
813    return acquire_within(lock_directory, poll_delay, notice, limit, SLOW_WAIT);
814}
815
816/// Lock contention,
817///  retirement and release controls stay out of the release executable.
818#[cfg(test)]
819#[path = "owner_lock_tests.rs"]
820mod tests;