1use std::fs::{self, File, OpenOptions};
16use std::io::{self, Write};
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20
21use thiserror::Error;
22
23use crate::config::ConfigSet;
24use crate::environment::Environment;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum LockWait {
29 Immediately,
31 Backoff {
33 budget: Duration,
35 },
36 Indefinitely,
38}
39
40impl LockWait {
41 #[must_use]
43 pub fn files_ref_lock(config: Option<&ConfigSet>) -> Self {
44 Self::from_files_ref_lock_timeout_ms(Self::parse_files_ref_lock_timeout_ms(config))
45 }
46
47 #[must_use]
49 pub fn parse_files_ref_lock_timeout_ms(config: Option<&ConfigSet>) -> i64 {
50 config
51 .and_then(|cfg| cfg.get("core.filesreflocktimeout"))
52 .and_then(|v| v.trim().parse::<i64>().ok())
53 .unwrap_or(100)
54 }
55
56 #[must_use]
57 fn from_files_ref_lock_timeout_ms(timeout_ms: i64) -> Self {
58 match timeout_ms {
59 0 => Self::Immediately,
60 ms if ms < 0 => Self::Indefinitely,
61 ms => Self::Backoff {
62 budget: Duration::from_millis(ms as u64),
63 },
64 }
65 }
66
67 #[must_use]
69 pub fn packed_refs(config: Option<&ConfigSet>) -> Self {
70 Self::from_milliseconds_config(config, "core.packedrefstimeout", 1_000)
71 }
72
73 #[must_use]
75 pub fn reftable_tables_list(config: Option<&ConfigSet>) -> Self {
76 Self::from_milliseconds_config(config, "reftable.locktimeout", 1_000)
77 }
78
79 #[must_use]
81 pub fn files_ref_lock_for_git_dir(git_dir: &Path) -> Self {
82 let config = ConfigSet::load(&Environment::empty(), Some(git_dir), true).ok();
83 Self::files_ref_lock(config.as_ref())
84 }
85
86 #[must_use]
88 pub fn packed_refs_for_git_dir(git_dir: &Path) -> Self {
89 let config = ConfigSet::load(&Environment::empty(), Some(git_dir), true).ok();
90 Self::packed_refs(config.as_ref())
91 }
92
93 fn from_milliseconds_config(config: Option<&ConfigSet>, key: &str, default_ms: u64) -> Self {
94 let ms = config
95 .and_then(|cfg| cfg.get(key))
96 .and_then(|v| v.trim().parse::<i64>().ok())
97 .map(|v| v.max(0) as u64)
98 .unwrap_or(default_ms);
99 if ms == 0 {
100 Self::Immediately
101 } else {
102 Self::Backoff {
103 budget: Duration::from_millis(ms),
104 }
105 }
106 }
107
108 #[must_use]
110 pub fn remaining_after(self, elapsed: Duration) -> Self {
111 match self {
112 Self::Immediately => Self::Immediately,
113 Self::Indefinitely => Self::Indefinitely,
114 Self::Backoff { budget } => {
115 if elapsed >= budget {
116 Self::Immediately
117 } else {
118 Self::Backoff {
119 budget: budget - elapsed,
120 }
121 }
122 }
123 }
124 }
125
126 #[must_use]
128 pub fn remaining_from(self, start: Instant, now: Instant) -> Self {
129 self.remaining_after(now.saturating_duration_since(start))
130 }
131}
132
133#[derive(Debug, Error)]
135#[non_exhaustive]
136pub enum LockError {
137 #[error("lock held at {path}")]
139 Held {
140 path: PathBuf,
142 },
143
144 #[error("lock I/O at {path}")]
146 Io {
147 path: PathBuf,
149 #[source]
151 source: io::Error,
152 },
153}
154
155pub trait LockClock: Send + Sync {
157 fn now(&self) -> Instant;
159 fn sleep(&self, duration: Duration);
161}
162
163pub trait LockJitter: Send + Sync {
165 fn fraction(&self, lock_path: &Path, attempt: u32) -> f64;
167}
168
169#[derive(Debug, Default, Clone, Copy)]
171pub struct SystemLockJitter;
172
173impl LockJitter for SystemLockJitter {
174 fn fraction(&self, _lock_path: &Path, _attempt: u32) -> f64 {
175 let mut buf = [0u8; 8];
176 if getrandom::getrandom(&mut buf).is_ok() {
177 let n = u64::from_le_bytes(buf);
178 (n % 10_000) as f64 / 10_000.0
179 } else {
180 0.5
181 }
182 }
183}
184
185#[derive(Debug, Default, Clone, Copy)]
187pub struct SystemLockClock;
188
189impl LockClock for SystemLockClock {
190 fn now(&self) -> Instant {
191 Instant::now()
192 }
193
194 fn sleep(&self, duration: Duration) {
195 if !duration.is_zero() {
196 std::thread::sleep(duration);
197 }
198 }
199}
200
201pub struct LockFile {
206 lock_path: PathBuf,
207 commit_path: Option<PathBuf>,
208 file: Option<File>,
209 committed: bool,
210 wrote_holder_pid: bool,
211}
212
213impl LockFile {
214 pub fn acquire(target: &Path, wait: LockWait) -> Result<Self, LockError> {
225 Self::acquire_with_clock(
226 target,
227 wait,
228 Arc::new(SystemLockClock),
229 Arc::new(SystemLockJitter),
230 )
231 }
232
233 pub fn acquire_with_clock(
235 target: &Path,
236 wait: LockWait,
237 clock: Arc<dyn LockClock>,
238 jitter: Arc<dyn LockJitter>,
239 ) -> Result<Self, LockError> {
240 let to_lock_error = |source: io::Error| {
241 if crate::atomic_replace::is_lock_acquire_contention(&source, target) {
242 LockError::Held {
243 path: target.to_path_buf(),
244 }
245 } else {
246 LockError::Io {
247 path: target.to_path_buf(),
248 source,
249 }
250 }
251 };
252 let open = || OpenOptions::new().write(true).create_new(true).open(target);
253 let mut parent_ready = false;
256 retry_while_held(
257 target,
258 || wait,
259 clock.as_ref(),
260 jitter.as_ref(),
261 || match open() {
262 Err(_) if !parent_ready => {
263 parent_ready = true;
264 if let Some(parent) = target.parent() {
265 fs::create_dir_all(parent).map_err(|source| LockError::Io {
266 path: target.to_path_buf(),
267 source,
268 })?;
269 }
270 open().map_err(to_lock_error)
271 }
272 other => other.map_err(to_lock_error),
273 },
274 |err| matches!(err, LockError::Held { .. }),
275 )
276 .map(|file| Self {
277 lock_path: target.to_path_buf(),
278 commit_path: None,
279 file: Some(file),
280 committed: false,
281 wrote_holder_pid: false,
282 })
283 }
284
285 #[must_use]
287 pub fn with_commit_target(mut self, path: &Path) -> Self {
288 self.commit_path = Some(path.to_path_buf());
289 self
290 }
291
292 #[must_use]
294 pub fn lock_path(&self) -> &Path {
295 &self.lock_path
296 }
297
298 pub fn as_file(&self) -> Result<&File, LockError> {
305 self.file.as_ref().ok_or_else(|| LockError::Io {
306 path: self.lock_path.clone(),
307 source: io::Error::new(io::ErrorKind::NotConnected, "lock file handle missing"),
308 })
309 }
310
311 pub fn as_file_mut(&mut self) -> Result<&mut File, LockError> {
317 self.file.as_mut().ok_or_else(|| LockError::Io {
318 path: self.lock_path.clone(),
319 source: io::Error::new(io::ErrorKind::NotConnected, "lock file handle missing"),
320 })
321 }
322
323 pub(crate) fn into_open_file(mut self) -> Result<(PathBuf, File), LockError> {
325 self.committed = true;
326 let file = self.file.take().ok_or_else(|| LockError::Io {
327 path: self.lock_path.clone(),
328 source: io::Error::new(io::ErrorKind::NotConnected, "lock file handle missing"),
329 })?;
330 Ok((self.lock_path.clone(), file))
331 }
332
333 pub fn commit(self) -> Result<(), LockError> {
340 let dest = self.commit_path.clone().ok_or_else(|| LockError::Io {
341 path: self.lock_path.clone(),
342 source: io::Error::new(
343 io::ErrorKind::InvalidInput,
344 "lock commit target not configured",
345 ),
346 })?;
347 self.commit_to(&dest)
348 }
349
350 pub fn commit_to(mut self, other_path: &Path) -> Result<(), LockError> {
356 let file = self.file.take().ok_or_else(|| LockError::Io {
357 path: self.lock_path.clone(),
358 source: io::Error::new(io::ErrorKind::NotConnected, "lock file handle missing"),
359 })?;
360 file.sync_all().map_err(|source| LockError::Io {
361 path: self.lock_path.clone(),
362 source,
363 })?;
364 drop(file);
365 crate::atomic_replace::replace_file(&self.lock_path, other_path).map_err(|source| {
366 LockError::Io {
367 path: self.lock_path.clone(),
368 source,
369 }
370 })?;
371 self.remove_holder_pid_sidecar();
372 self.committed = true;
373 Ok(())
374 }
375
376 pub fn write_holder_pid(&mut self) -> Result<(), LockError> {
382 write_lock_pid_file(&pid_path_for_lock(&self.lock_path)).map_err(|source| {
383 LockError::Io {
384 path: self.lock_path.clone(),
385 source,
386 }
387 })?;
388 self.wrote_holder_pid = true;
389 Ok(())
390 }
391
392 pub fn rollback(mut self) {
394 if self.committed {
395 return;
396 }
397 drop(self.file.take());
398 let _ = fs::remove_file(&self.lock_path);
399 self.remove_holder_pid_sidecar();
400 self.committed = true;
401 }
402
403 fn remove_holder_pid_sidecar(&self) {
404 if self.wrote_holder_pid {
405 let _ = fs::remove_file(pid_path_for_lock(&self.lock_path));
406 }
407 }
408
409 pub fn detach(mut self) {
411 self.committed = true;
412 }
413}
414
415impl Write for LockFile {
416 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
417 match self.file.as_mut() {
418 Some(file) => file.write(buf),
419 None => Err(io::Error::new(
420 io::ErrorKind::NotConnected,
421 "lock file handle missing",
422 )),
423 }
424 }
425
426 fn flush(&mut self) -> io::Result<()> {
427 match self.file.as_mut() {
428 Some(file) => file.flush(),
429 None => Err(io::Error::new(
430 io::ErrorKind::NotConnected,
431 "lock file handle missing",
432 )),
433 }
434 }
435}
436
437impl Drop for LockFile {
438 fn drop(&mut self) {
439 if self.committed {
440 return;
441 }
442 drop(self.file.take());
443 let _ = fs::remove_file(&self.lock_path);
444 self.remove_holder_pid_sidecar();
445 }
446}
447
448pub(crate) fn retry_while_held<T, E>(
461 key: &Path,
462 wait: impl FnOnce() -> LockWait,
463 clock: &dyn LockClock,
464 jitter: &dyn LockJitter,
465 mut attempt: impl FnMut() -> Result<T, E>,
466 is_held: impl Fn(&E) -> bool,
467) -> Result<T, E> {
468 let start = clock.now();
469 let mut err = match attempt() {
470 Err(err) if is_held(&err) => err,
471 other => return other,
472 };
473 let deadline = match wait() {
474 LockWait::Immediately => return Err(err),
475 LockWait::Backoff { budget } => Some(start + budget),
476 LockWait::Indefinitely => None,
477 };
478 let mut tries: u32 = 0;
479 loop {
480 if deadline.is_some_and(|deadline| clock.now() >= deadline) {
481 return Err(err);
482 }
483 let delay = backoff_delay(key, tries, jitter);
484 tries = tries.saturating_add(1);
485 clock.sleep(match deadline {
486 Some(deadline) => delay.min(deadline.saturating_duration_since(clock.now())),
487 None => delay,
488 });
489 match attempt() {
490 Err(held) if is_held(&held) => err = held,
491 other => return other,
492 }
493 }
494}
495
496pub(crate) fn backoff_delay(lock_path: &Path, attempt: u32, jitter: &dyn LockJitter) -> Duration {
497 const INITIAL_MS: u64 = 1;
498 const CAP_MS: u64 = 50;
499 let shift = attempt.min(16);
500 let base_ms = (INITIAL_MS.saturating_mul(1_u64 << shift)).min(CAP_MS);
501 let span_ms = base_ms / 2 + 1;
502 let jitter_ms = (jitter.fraction(lock_path, attempt) * span_ms as f64) as u64;
503 Duration::from_millis(base_ms.saturating_add(jitter_ms))
504}
505
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub struct LockHolder {
512 pub pid: u64,
514 pub running: bool,
519}
520
521impl LockHolder {
522 #[must_use]
528 pub fn read(lock_path: &Path) -> Option<Self> {
529 let pid = read_lock_pid(&pid_path_for_lock(lock_path))?;
530 Some(Self {
531 pid,
532 running: is_process_running(pid),
533 })
534 }
535
536 pub fn remove_if_stale(lock_path: &Path) -> io::Result<bool> {
547 let Some(holder) = Self::read(lock_path) else {
548 return Ok(false);
549 };
550 if holder.running {
551 return Ok(false);
552 }
553 let pid_path = pid_path_for_lock(lock_path);
554 if lock_path.is_file() && pid_path.is_file() {
555 let lock_mtime = fs::metadata(lock_path)?.modified()?;
556 let pid_mtime = fs::metadata(&pid_path)?.modified()?;
557 if lock_mtime > pid_mtime {
558 return Ok(false);
559 }
560 }
561 for path in [lock_path, &pid_path] {
562 match fs::remove_file(path) {
563 Ok(()) => {}
564 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
565 Err(e) => return Err(e),
566 }
567 }
568 Ok(true)
569 }
570}
571
572#[must_use]
579pub fn grit_lockfile_pid_enabled(config: Option<&ConfigSet>, git_dir: Option<&Path>) -> bool {
580 fn from_config(cfg: &ConfigSet) -> bool {
581 cfg.bool_or("core.lockfilepid", true)
582 }
583 if let Some(cfg) = config {
584 return from_config(cfg);
585 }
586 if let Some(dir) = git_dir {
587 if let Ok(cfg) = ConfigSet::load(&Environment::empty(), Some(dir), true) {
588 return from_config(&cfg);
589 }
590 }
591 true
592}
593
594pub fn pid_path_for_lock(lock_path: &Path) -> PathBuf {
596 let file_name = lock_path
597 .file_name()
598 .map(|s| s.to_string_lossy().into_owned())
599 .unwrap_or_else(|| "index.lock".to_owned());
600 let base = file_name.strip_suffix(".lock").unwrap_or(&file_name);
601 lock_path.with_file_name(format!("{base}~pid.lock"))
602}
603
604pub(crate) fn write_lock_pid_file(pid_path: &Path) -> io::Result<()> {
605 let mut file = OpenOptions::new()
606 .write(true)
607 .create(true)
608 .truncate(true)
609 .open(pid_path)?;
610 writeln!(file, "pid {}", std::process::id())?;
611 Ok(())
612}
613
614fn read_lock_pid(pid_path: &Path) -> Option<u64> {
615 let raw = fs::read_to_string(pid_path).ok()?;
616 let trimmed = raw.trim();
617 trimmed
618 .strip_prefix("pid ")
619 .unwrap_or(trimmed)
620 .trim()
621 .parse::<u64>()
622 .ok()
623}
624
625pub(crate) fn is_process_running(pid: u64) -> bool {
627 if pid == 0 || pid > i32::MAX as u64 {
628 return false;
629 }
630 #[cfg(windows)]
631 {
632 use windows_sys::Win32::Foundation::CloseHandle;
633 use windows_sys::Win32::System::Threading::{
634 OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
635 };
636 unsafe {
638 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid as u32);
639 if handle.is_null() {
640 return false;
641 }
642 CloseHandle(handle);
643 true
644 }
645 }
646 #[cfg(all(unix, target_os = "linux"))]
647 {
648 Path::new(&format!("/proc/{pid}")).exists()
649 }
650 #[cfg(all(unix, not(target_os = "linux")))]
651 {
652 use rustix::io::Errno;
653 use rustix::process::{test_kill_process, Pid};
654 let Some(pid) = Pid::from_raw(pid as i32) else {
655 return false;
656 };
657 match test_kill_process(pid) {
658 Ok(()) => true,
659 Err(Errno::SRCH) => false,
660 Err(Errno::PERM) => true,
661 Err(_) => false,
662 }
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use std::sync::Mutex;
670
671 struct FakeLockClock {
672 now: Mutex<Instant>,
673 sleeps: Mutex<Vec<Duration>>,
674 }
675
676 impl FakeLockClock {
677 fn new(start: Instant) -> Arc<Self> {
678 Arc::new(Self {
679 now: Mutex::new(start),
680 sleeps: Mutex::new(Vec::new()),
681 })
682 }
683
684 fn sleeps(&self) -> Vec<Duration> {
685 self.sleeps.lock().unwrap().clone()
686 }
687 }
688
689 impl LockClock for FakeLockClock {
690 fn now(&self) -> Instant {
691 *self.now.lock().unwrap()
692 }
693
694 fn sleep(&self, duration: Duration) {
695 self.sleeps.lock().unwrap().push(duration);
696 *self.now.lock().unwrap() += duration;
697 }
698 }
699
700 #[test]
701 fn drop_without_commit_removes_own_lock() {
702 let dir = tempfile::tempdir().unwrap();
703 let lock = dir.path().join("resource.lock");
704 {
705 let _lock = LockFile::acquire(&lock, LockWait::Immediately).unwrap();
706 assert!(lock.is_file());
707 }
708 assert!(!lock.exists());
709 }
710
711 #[test]
712 fn commit_to_replaces_target() {
713 let dir = tempfile::tempdir().unwrap();
714 let target = dir.path().join("resource");
715 let lock_path = dir.path().join("resource.lock");
716 let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately).unwrap();
717 lock.write_all(b"payload\n").unwrap();
718 lock.commit_to(&target).unwrap();
719 assert!(!lock_path.exists());
720 assert_eq!(fs::read_to_string(&target).unwrap(), "payload\n");
721 }
722
723 struct FixedJitter(f64);
724
725 impl LockJitter for FixedJitter {
726 fn fraction(&self, _lock_path: &Path, _attempt: u32) -> f64 {
727 self.0
728 }
729 }
730
731 #[test]
732 fn held_when_budget_exhausted() {
733 let dir = tempfile::tempdir().unwrap();
734 let lock_path = dir.path().join("x.lock");
735 fs::write(&lock_path, b"foreign").unwrap();
736 let start = Instant::now();
737 let clock = FakeLockClock::new(start);
738 let err = match LockFile::acquire_with_clock(
739 &lock_path,
740 LockWait::Backoff {
741 budget: Duration::from_millis(10),
742 },
743 clock.clone(),
744 Arc::new(FixedJitter(0.5)),
745 ) {
746 Err(e) => e,
747 Ok(_) => panic!("expected lock held error"),
748 };
749 assert!(matches!(err, LockError::Held { .. }));
750 assert!(lock_path.is_file());
751 assert_eq!(fs::read_to_string(&lock_path).unwrap(), "foreign");
752 assert!(!clock.sleeps().is_empty());
753 }
754
755 #[test]
756 fn pid_sidecar_path_replaces_lock_suffix() {
757 assert_eq!(
758 pid_path_for_lock(Path::new("/r/.git/index.lock")),
759 Path::new("/r/.git/index~pid.lock")
760 );
761 assert_eq!(
762 pid_path_for_lock(Path::new("/r/.git/refs/heads/main.lock")),
763 Path::new("/r/.git/refs/heads/main~pid.lock")
764 );
765 assert_eq!(
766 pid_path_for_lock(Path::new("/r/odd")),
767 Path::new("/r/odd~pid.lock")
768 );
769 }
770
771 #[test]
772 fn remove_if_stale_skips_when_lock_is_newer_than_sidecar() {
773 use std::thread;
774 use std::time::Duration;
775
776 let dir = tempfile::tempdir().unwrap();
777 let lock_path = dir.path().join("index.lock");
778 fs::write(&lock_path, b"").unwrap();
779 let pid_path = pid_path_for_lock(&lock_path);
780 fs::write(&pid_path, "pid 999999999\n").unwrap();
781 thread::sleep(Duration::from_millis(20));
782 fs::write(&lock_path, b"replaced").unwrap();
783
784 assert!(!LockHolder::remove_if_stale(&lock_path).unwrap());
785 assert!(lock_path.is_file());
786 assert!(pid_path.is_file());
787 }
788
789 #[test]
790 fn held_converts_to_typed_error_with_holder() {
791 let dir = tempfile::tempdir().unwrap();
792 let lock_path = dir.path().join("index.lock");
793 fs::write(&lock_path, b"").unwrap();
794 fs::write(
795 dir.path().join("index~pid.lock"),
796 format!("pid {}\n", std::process::id()),
797 )
798 .unwrap();
799 let err = LockFile::acquire(&lock_path, LockWait::Immediately)
800 .err()
801 .expect("held");
802 match crate::error::Error::from(err) {
803 crate::error::Error::LockHeld { path, holder } => {
804 assert_eq!(path, lock_path);
805 assert_eq!(
806 holder,
807 Some(LockHolder {
808 pid: u64::from(std::process::id()),
809 running: true,
810 })
811 );
812 }
813 other => panic!("expected LockHeld, got {other:?}"),
814 }
815 }
816
817 #[test]
818 fn io_failure_converts_to_io_error() {
819 let err = LockError::Io {
820 path: PathBuf::from("x.lock"),
821 source: io::Error::from(io::ErrorKind::PermissionDenied),
822 };
823 assert!(matches!(
824 crate::error::Error::from(err),
825 crate::error::Error::Io(e) if e.kind() == io::ErrorKind::PermissionDenied
826 ));
827 }
828
829 #[test]
830 fn rollback_removes_own_lock() {
831 let dir = tempfile::tempdir().unwrap();
832 let lock = dir.path().join("resource.lock");
833 let lock_file = LockFile::acquire(&lock, LockWait::Immediately).unwrap();
834 assert!(lock.is_file());
835 lock_file.rollback();
836 assert!(!lock.exists());
837 }
838
839 #[test]
840 fn files_ref_lock_timeout_uses_milliseconds() {
841 let mut cfg = ConfigSet::new();
842 cfg.add_command_override("core.filesreflocktimeout", "100")
843 .expect("set timeout");
844 assert_eq!(LockWait::parse_files_ref_lock_timeout_ms(Some(&cfg)), 100);
845 match LockWait::files_ref_lock(Some(&cfg)) {
846 LockWait::Backoff { budget } => {
847 assert_eq!(budget, Duration::from_millis(100));
848 }
849 other => panic!("expected backoff, got {other:?}"),
850 }
851 }
852
853 #[test]
854 fn files_ref_lock_timeout_negative_retries_indefinitely() {
855 let mut cfg = ConfigSet::new();
856 cfg.add_command_override("core.filesreflocktimeout", "-1")
857 .expect("set timeout");
858 assert!(matches!(
859 LockWait::files_ref_lock(Some(&cfg)),
860 LockWait::Indefinitely
861 ));
862 }
863
864 #[test]
865 fn indefinite_wait_succeeds_when_lock_cleared() {
866 let dir = tempfile::tempdir().unwrap();
867 let lock_path = dir.path().join("x.lock");
868 fs::write(&lock_path, b"foreign").unwrap();
869 let clock = FakeLockClock::new(Instant::now());
870 let lock_path_thread = lock_path.clone();
871 std::thread::spawn(move || {
872 std::thread::sleep(Duration::from_millis(5));
873 let _ = fs::remove_file(&lock_path_thread);
874 });
875 let _guard = LockFile::acquire_with_clock(
876 &lock_path,
877 LockWait::Indefinitely,
878 clock,
879 Arc::new(FixedJitter(0.0)),
880 )
881 .expect("indefinite wait should eventually acquire");
882 }
883
884 #[test]
885 fn retry_while_held_resolves_wait_lazily_and_counts_attempt_time() {
886 let key = Path::new("refs");
887 let jitter = FixedJitter(0.0);
888 let clock = FakeLockClock::new(Instant::now());
889
890 let ok: Result<u32, u32> = retry_while_held(
891 key,
892 || panic!("wait resolved without contention"),
893 clock.as_ref(),
894 &jitter,
895 || Ok(7),
896 |_| true,
897 );
898 assert_eq!(ok, Ok(7));
899
900 let mut calls = 0;
901 let other = retry_while_held(
902 key,
903 || LockWait::Indefinitely,
904 clock.as_ref(),
905 &jitter,
906 || {
907 calls += 1;
908 Err::<(), _>("io")
909 },
910 |e| *e == "held",
911 );
912 assert_eq!((other, calls), (Err("io"), 1));
913 assert!(clock.sleeps().is_empty());
914
915 let mut calls = 0;
917 let slow = retry_while_held(
918 key,
919 || LockWait::Backoff {
920 budget: Duration::from_millis(10),
921 },
922 clock.as_ref(),
923 &jitter,
924 || {
925 calls += 1;
926 clock.sleep(Duration::from_millis(10));
927 Err::<(), _>(calls)
928 },
929 |_| true,
930 );
931 assert_eq!(slow, Err(1));
932
933 let mut calls = 0;
934 let later = retry_while_held(
935 key,
936 || LockWait::Backoff {
937 budget: Duration::from_secs(1),
938 },
939 clock.as_ref(),
940 &jitter,
941 || {
942 calls += 1;
943 if calls < 4 {
944 Err(calls)
945 } else {
946 Ok(calls)
947 }
948 },
949 |_| true,
950 );
951 assert_eq!(later, Ok(4));
952 }
953}