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, ¤t_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;