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