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