xianyun_deploy/registrycache/lock.rs
1//! Cross-process serialization for registry-cache state mutations.
2//!
3//! Ported from `internal/registrycache/lock.go`.
4
5use std::fs::{self, OpenOptions};
6use std::os::fd::AsRawFd;
7
8use thiserror::Error;
9
10use super::paths::{self, PathError};
11
12/// The basename of the flock used to serialize all state-mutating
13/// operations (`ensure`, `ensure_stopped_for_profile`).
14const LOCK_FILE: &str = ".lock";
15
16/// Errors from acquiring the registry-cache flock.
17#[derive(Debug, Error)]
18pub enum LockError {
19 /// Failed resolving the registry-cache directory.
20 #[error(transparent)]
21 Path(#[from] PathError),
22 /// Failed creating the registry-cache directory.
23 #[error("creating registry cache dir: {0}")]
24 CreateDir(std::io::Error),
25 /// Failed opening (or creating) the lock file.
26 #[error("opening lock file: {0}")]
27 OpenLockFile(std::io::Error),
28 /// The `flock(2)` syscall itself failed.
29 #[error("acquiring flock: {0}")]
30 Flock(std::io::Error),
31}
32
33/// Acquires an exclusive flock on the registry-cache lock file for the
34/// duration of `f`. The lock file is created on first use. Released
35/// when `f` returns (including via panic-unwind, since the underlying
36/// file descriptor is closed by `Drop`).
37///
38/// Use this around every operation that touches refs, the PID file,
39/// the config file, or the zot process state. Generic over the
40/// caller's error type `E` (which must be constructible from
41/// [`LockError`]) so callers can propagate their own error variants
42/// from `f` without a boxed catch-all.
43pub fn with_lock<T, E, F>(f: F) -> Result<T, E>
44where
45 F: FnOnce() -> Result<T, E>,
46 E: From<LockError>,
47{
48 let dir = paths::global_dir().map_err(LockError::from)?;
49
50 fs::create_dir_all(&dir).map_err(LockError::CreateDir)?;
51
52 let file = OpenOptions::new()
53 .create(true)
54 .truncate(false)
55 .read(true)
56 .write(true)
57 .open(dir.join(LOCK_FILE))
58 .map_err(LockError::OpenLockFile)?;
59
60 // SAFETY: `file` stays alive (and thus its fd valid) for the
61 // duration of the flock call; flock(2) is a well-defined syscall
62 // given a valid fd.
63 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
64 if rc != 0 {
65 return Err(LockError::Flock(std::io::Error::last_os_error()).into());
66 }
67
68 let result = f();
69
70 // Best-effort unlock; the fd closing on drop would release the lock
71 // anyway, but do it explicitly (and ignore errors) to mirror the Go
72 // `defer` semantics exactly.
73 // SAFETY: same fd, still valid.
74 let _ = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
75
76 result
77}
78
79#[cfg(test)]
80mod tests {
81 use std::sync::atomic::{AtomicI64, Ordering};
82 use std::sync::{Arc, Mutex};
83 use std::thread;
84 use std::time::Duration;
85
86 use super::*;
87 use crate::registrycache::test_support::with_home;
88
89 // Ported from internal/registrycache/lock_test.go:12 (TestWithLockSerializesCalls)
90 #[test]
91 fn test_with_lock_serializes_calls() {
92 with_home(|_home| {
93 const N: usize = 5;
94
95 let concurrent = Arc::new(AtomicI64::new(0));
96 let max_seen = Arc::new(AtomicI64::new(0));
97 let errs = Arc::new(Mutex::new(Vec::new()));
98
99 let handles: Vec<_> = (0..N)
100 .map(|_| {
101 let concurrent = Arc::clone(&concurrent);
102 let max_seen = Arc::clone(&max_seen);
103 let errs = Arc::clone(&errs);
104
105 thread::spawn(move || {
106 let result: Result<(), LockError> = with_lock(|| {
107 let now = concurrent.fetch_add(1, Ordering::SeqCst) + 1;
108
109 loop {
110 let old_max = max_seen.load(Ordering::SeqCst);
111 if now <= old_max
112 || max_seen
113 .compare_exchange(
114 old_max,
115 now,
116 Ordering::SeqCst,
117 Ordering::SeqCst,
118 )
119 .is_ok()
120 {
121 break;
122 }
123 }
124
125 thread::sleep(Duration::from_millis(50));
126 concurrent.fetch_sub(1, Ordering::SeqCst);
127
128 Ok(())
129 });
130
131 if let Err(err) = result {
132 errs.lock().unwrap().push(err.to_string());
133 }
134 })
135 })
136 .collect();
137
138 for h in handles {
139 h.join().unwrap();
140 }
141
142 let errs = errs.lock().unwrap();
143 assert!(errs.is_empty(), "unexpected errors: {errs:?}");
144
145 // Critical section must have been single-threaded.
146 assert_eq!(max_seen.load(Ordering::SeqCst), 1);
147 });
148 }
149}