cargo/util/cache_lock.rs
1//! Support for locking the package and index caches.
2//!
3//! This implements locking on the package and index caches (source files,
4//! `.crate` files, and index caches) to coordinate when multiple cargos are
5//! running at the same time.
6//!
7//! ## Usage
8//!
9//! There is a global [`CacheLocker`] held inside cargo's venerable
10//! [`GlobalContext`]. The `CacheLocker` manages creating and tracking the locks
11//! being held. There are methods on [`GlobalContext`] for managing the locks:
12//!
13//! - [`GlobalContext::acquire_package_cache_lock`] --- Acquires a lock. May block if
14//! another process holds a lock.
15//! - [`GlobalContext::try_acquire_package_cache_lock`] --- Acquires a lock, returning
16//! immediately if it would block.
17//! - [`GlobalContext::assert_package_cache_locked`] --- This is used to ensure the
18//! proper lock is being held.
19//!
20//! Lower-level code that accesses the package cache typically just use
21//! `assert_package_cache_locked` to ensure that the correct lock is being
22//! held. Higher-level code is responsible for acquiring the appropriate lock,
23//! and holding it during the duration that it is performing its operation.
24//!
25//! ## Types of locking
26//!
27//! There are three styles of locks:
28//!
29//! * [`CacheLockMode::DownloadExclusive`] -- This is an exclusive lock
30//! acquired while downloading packages and doing resolution.
31//! * [`CacheLockMode::Shared`] -- This is a shared lock acquired while a
32//! build is running. In other words, whenever cargo just needs to read from
33//! the cache, it should hold this lock. This is here to ensure that no
34//! cargos are trying to read the source caches when cache garbage
35//! collection runs.
36//! * [`CacheLockMode::MutateExclusive`] -- This is an exclusive lock acquired
37//! whenever needing to modify existing source files (for example, with
38//! cache garbage collection). This is acquired to make sure that no other
39//! cargo is reading from the cache.
40//!
41//! Importantly, a `DownloadExclusive` lock does *not* interfere with a
42//! `Shared` lock. The download process generally does not modify source files
43//! (it only adds new ones), so other cargos should be able to safely proceed
44//! in reading source files[^1].
45//!
46//! See the [`CacheLockMode`] enum docs for more details on when the different
47//! modes should be used.
48//!
49//! ## Locking implementation details
50//!
51//! This is implemented by two separate lock files, the "download" one and the
52//! "mutate" one. The `MutateExclusive` lock acquired both the "mutate" and
53//! "download" locks. The `Shared` lock acquires the "mutate" lock in share
54//! mode.
55//!
56//! An important rule is that `MutateExclusive` acquires the locks in the
57//! order "mutate" first and then the "download". That helps prevent
58//! deadlocks. It is not allowed for a cargo to first acquire a
59//! `DownloadExclusive` lock and then a `Shared` lock because that would open
60//! it up for deadlock.
61//!
62//! Another rule is that there should be only one [`CacheLocker`] per process
63//! to uphold the ordering rules. You could in theory have multiple if you
64//! could ensure that other threads would make progress and drop a lock, but
65//! cargo is not architected that way.
66//!
67//! It is safe to recursively acquire a lock as many times as you want.
68//!
69//! ## Interaction with older cargos
70//!
71//! Before version 1.74, cargo only acquired the `DownloadExclusive` lock when
72//! downloading and doing resolution. Newer cargos that acquire
73//! `MutateExclusive` should still correctly block when an old cargo is
74//! downloading (because it also acquires `DownloadExclusive`), but they do
75//! not properly coordinate when an old cargo is in the build phase (because
76//! it holds no locks). This isn't expected to be much of a problem because
77//! the intended use of mutating the cache is only to delete old contents
78//! which aren't currently being used. It is possible for there to be a
79//! conflict, particularly if the user manually deletes the entire cache, but
80//! it is not expected for this scenario to happen too often, and the only
81//! consequence is that one side or the other encounters an error and needs to
82//! retry.
83//!
84//! [^1]: A minor caveat is that downloads will delete an existing `src`
85//! directory if it was extracted via an old cargo. See
86//! [`crate::sources::registry::RegistrySource::unpack_package`]. This
87//! should probably be fixed, but is unlikely to be a problem if the user is
88//! only using versions of cargo with the same deletion logic.
89
90use super::FileLock;
91use crate::CargoResult;
92use crate::GlobalContext;
93use anyhow::Context as _;
94use std::cell::RefCell;
95use std::io;
96
97/// The style of lock to acquire.
98#[derive(Copy, Clone, Debug, PartialEq)]
99pub enum CacheLockMode {
100 /// A `DownloadExclusive` lock ensures that only one cargo is doing
101 /// resolution and downloading new packages.
102 ///
103 /// You should use this when downloading new packages or doing resolution.
104 ///
105 /// If another cargo has a `MutateExclusive` lock, then an attempt to get
106 /// a `DownloadExclusive` lock will block.
107 ///
108 /// If another cargo has a `Shared` lock, then both can operate
109 /// concurrently.
110 DownloadExclusive,
111 /// A `Shared` lock allows multiple cargos to read from the source files.
112 ///
113 /// You should use this when cargo is reading source files from the
114 /// package cache. This is typically done during the build phase, since
115 /// cargo only needs to read files during that time. This allows multiple
116 /// cargo processes to build concurrently without interfering with one
117 /// another, while guarding against other cargos using `MutateExclusive`.
118 ///
119 /// If another cargo has a `MutateExclusive` lock, then an attempt to get
120 /// a `Shared` will block.
121 ///
122 /// If another cargo has a `DownloadExclusive` lock, then they both can
123 /// operate concurrently under the assumption that downloading does not
124 /// modify existing source files.
125 Shared,
126 /// A `MutateExclusive` lock ensures no other cargo is reading or writing
127 /// from the package caches.
128 ///
129 /// You should use this when modifying existing files in the package
130 /// cache. For example, things like garbage collection want to avoid
131 /// deleting files while other cargos are trying to read (`Shared`) or
132 /// resolve or download (`DownloadExclusive`).
133 ///
134 /// If another cargo has a `DownloadExclusive` or `Shared` lock, then this
135 /// will block until they all release their locks.
136 MutateExclusive,
137}
138
139/// Whether or not a lock attempt should block.
140#[derive(Copy, Clone)]
141enum BlockingMode {
142 Blocking,
143 NonBlocking,
144}
145
146use BlockingMode::*;
147
148/// Whether or not a lock attempt blocked or succeeded.
149#[derive(PartialEq, Copy, Clone)]
150#[must_use]
151enum LockingResult {
152 LockAcquired,
153 WouldBlock,
154}
155
156use LockingResult::*;
157
158/// A file lock, with a counter to assist with recursive locking.
159#[derive(Debug)]
160struct RecursiveLock {
161 /// The file lock.
162 ///
163 /// An important note is that locks can be `None` even when they are held.
164 /// This can happen on things like old NFS mounts where locking isn't
165 /// supported. We otherwise pretend we have a lock via the lock count. See
166 /// [`FileLock`] for more detail on that.
167 lock: Option<FileLock>,
168 /// Number locks held, to support recursive locking.
169 count: u32,
170 /// If this is `true`, it is an exclusive lock, otherwise it is shared.
171 is_exclusive: bool,
172 /// The filename of the lock.
173 filename: &'static str,
174}
175
176impl RecursiveLock {
177 fn new(filename: &'static str) -> RecursiveLock {
178 RecursiveLock {
179 lock: None,
180 count: 0,
181 is_exclusive: false,
182 filename,
183 }
184 }
185
186 /// Low-level lock count increment routine.
187 fn increment(&mut self) {
188 self.count = self.count.checked_add(1).unwrap();
189 }
190
191 /// Unlocks a previously acquired lock.
192 fn decrement(&mut self) {
193 let new_cnt = self.count.checked_sub(1).unwrap();
194 self.count = new_cnt;
195 if new_cnt == 0 {
196 // This will drop, releasing the lock.
197 self.lock = None;
198 }
199 }
200
201 /// Acquires a shared lock.
202 fn lock_shared(
203 &mut self,
204 gctx: &GlobalContext,
205 description: &'static str,
206 blocking: BlockingMode,
207 ) -> LockingResult {
208 match blocking {
209 Blocking => {
210 self.lock_shared_blocking(gctx, description);
211 LockAcquired
212 }
213 NonBlocking => self.lock_shared_nonblocking(gctx),
214 }
215 }
216
217 /// Acquires a shared lock, blocking if held by another locker.
218 fn lock_shared_blocking(&mut self, gctx: &GlobalContext, description: &'static str) {
219 if self.count == 0 {
220 self.is_exclusive = false;
221 self.lock = match gctx
222 .home()
223 .open_ro_shared_create(self.filename, gctx, description)
224 {
225 Ok(lock) => Some(lock),
226 Err(e) => {
227 // There is no error here because locking is mostly a
228 // best-effort attempt. If cargo home is read-only, we don't
229 // want to fail just because we couldn't create the lock file.
230 tracing::warn!("failed to acquire cache lock {}: {e:?}", self.filename);
231 None
232 }
233 };
234 }
235 self.increment();
236 }
237
238 /// Acquires a shared lock, returns [`WouldBlock`] if held by another locker.
239 fn lock_shared_nonblocking(&mut self, gctx: &GlobalContext) -> LockingResult {
240 if self.count == 0 {
241 self.is_exclusive = false;
242 self.lock = match gctx.home().try_open_ro_shared_create(self.filename) {
243 Ok(Some(lock)) => Some(lock),
244 Ok(None) => {
245 return WouldBlock;
246 }
247 Err(e) => {
248 // Pretend that the lock was acquired (see lock_shared_blocking).
249 tracing::warn!("failed to acquire cache lock {}: {e:?}", self.filename);
250 None
251 }
252 };
253 }
254 self.increment();
255 LockAcquired
256 }
257
258 /// Acquires an exclusive lock.
259 fn lock_exclusive(
260 &mut self,
261 gctx: &GlobalContext,
262 description: &'static str,
263 blocking: BlockingMode,
264 ) -> CargoResult<LockingResult> {
265 if self.count > 0 && !self.is_exclusive {
266 // Lock upgrades are dicey. It might be possible to support
267 // this but would take a bit of work, and so far it isn't
268 // needed.
269 panic!("lock upgrade from shared to exclusive not supported");
270 }
271 match blocking {
272 Blocking => {
273 self.lock_exclusive_blocking(gctx, description)?;
274 Ok(LockAcquired)
275 }
276 NonBlocking => self.lock_exclusive_nonblocking(gctx),
277 }
278 }
279
280 /// Acquires an exclusive lock, blocking if held by another locker.
281 fn lock_exclusive_blocking(
282 &mut self,
283 gctx: &GlobalContext,
284 description: &'static str,
285 ) -> CargoResult<()> {
286 if self.count == 0 {
287 self.is_exclusive = true;
288 match gctx
289 .home()
290 .open_rw_exclusive_create(self.filename, gctx, description)
291 {
292 Ok(lock) => self.lock = Some(lock),
293 Err(e) => {
294 if maybe_readonly(&e) {
295 // This is a best-effort attempt to at least try to
296 // acquire some sort of lock. This can help in the
297 // situation where this cargo only has read-only access,
298 // but maybe some other cargo has read-write. This will at
299 // least attempt to coordinate with it.
300 //
301 // We don't want to fail on a read-only mount because
302 // cargo grabs an exclusive lock in situations where it
303 // may only be reading from the package cache. In that
304 // case, cargo isn't writing anything, and we don't want
305 // to fail on that.
306 self.lock_shared_blocking(gctx, description);
307 // This has to pretend it is exclusive for recursive locks to work.
308 self.is_exclusive = true;
309 return Ok(());
310 } else {
311 return Err(e).context("failed to acquire package cache lock");
312 }
313 }
314 }
315 }
316 self.increment();
317 Ok(())
318 }
319
320 /// Acquires an exclusive lock, returns [`WouldBlock`] if held by another locker.
321 fn lock_exclusive_nonblocking(&mut self, gctx: &GlobalContext) -> CargoResult<LockingResult> {
322 if self.count == 0 {
323 self.is_exclusive = true;
324 match gctx.home().try_open_rw_exclusive_create(self.filename) {
325 Ok(Some(lock)) => self.lock = Some(lock),
326 Ok(None) => return Ok(WouldBlock),
327 Err(e) => {
328 if maybe_readonly(&e) {
329 let result = self.lock_shared_nonblocking(gctx);
330 // This has to pretend it is exclusive for recursive locks to work.
331 self.is_exclusive = true;
332 return Ok(result);
333 } else {
334 return Err(e).context("failed to acquire package cache lock");
335 }
336 }
337 }
338 }
339 self.increment();
340 Ok(LockAcquired)
341 }
342}
343
344/// The state of the [`CacheLocker`].
345#[derive(Debug)]
346struct CacheState {
347 /// The cache lock guards the package cache used for download and
348 /// resolution (append operations that should not interfere with reading
349 /// from existing src files).
350 cache_lock: RecursiveLock,
351 /// The mutate lock is used to either guard the entire package cache for
352 /// destructive modifications (in exclusive mode), or for reading the
353 /// package cache src files (in shared mode).
354 ///
355 /// Note that [`CacheLockMode::MutateExclusive`] holds both
356 /// [`CacheState::mutate_lock`] and [`CacheState::cache_lock`].
357 mutate_lock: RecursiveLock,
358}
359
360impl CacheState {
361 fn lock(
362 &mut self,
363 gctx: &GlobalContext,
364 mode: CacheLockMode,
365 blocking: BlockingMode,
366 ) -> CargoResult<LockingResult> {
367 use CacheLockMode::*;
368 if mode == Shared && self.cache_lock.count > 0 && self.mutate_lock.count == 0 {
369 // Shared lock, when a DownloadExclusive is held.
370 //
371 // This isn't supported because it could cause a deadlock. If
372 // one cargo is attempting to acquire a MutateExclusive lock,
373 // and acquires the mutate lock, but is blocked on the
374 // download lock, and the cargo that holds the download lock
375 // attempts to get a shared lock, they would end up blocking
376 // each other.
377 panic!("shared lock while holding download lock is not allowed");
378 }
379 match mode {
380 Shared => {
381 if self.mutate_lock.lock_shared(gctx, SHARED_DESCR, blocking) == WouldBlock {
382 return Ok(WouldBlock);
383 }
384 }
385 DownloadExclusive => {
386 if self
387 .cache_lock
388 .lock_exclusive(gctx, DOWNLOAD_EXCLUSIVE_DESCR, blocking)?
389 == WouldBlock
390 {
391 return Ok(WouldBlock);
392 }
393 }
394 MutateExclusive => {
395 if self
396 .mutate_lock
397 .lock_exclusive(gctx, MUTATE_EXCLUSIVE_DESCR, blocking)?
398 == WouldBlock
399 {
400 return Ok(WouldBlock);
401 }
402
403 // Part of the contract of MutateExclusive is that it doesn't
404 // allow any processes to have a lock on the package cache, so
405 // this acquires both locks.
406 match self
407 .cache_lock
408 .lock_exclusive(gctx, DOWNLOAD_EXCLUSIVE_DESCR, blocking)
409 {
410 Ok(LockAcquired) => {}
411 Ok(WouldBlock) => return Ok(WouldBlock),
412 Err(e) => {
413 self.mutate_lock.decrement();
414 return Err(e);
415 }
416 }
417 }
418 }
419 Ok(LockAcquired)
420 }
421}
422
423/// A held lock guard.
424///
425/// When this is dropped, the lock will be released.
426#[must_use]
427pub struct CacheLock<'lock> {
428 mode: CacheLockMode,
429 locker: &'lock CacheLocker,
430}
431
432impl Drop for CacheLock<'_> {
433 fn drop(&mut self) {
434 use CacheLockMode::*;
435 let mut state = self.locker.state.borrow_mut();
436 match self.mode {
437 Shared => {
438 state.mutate_lock.decrement();
439 }
440 DownloadExclusive => {
441 state.cache_lock.decrement();
442 }
443 MutateExclusive => {
444 state.cache_lock.decrement();
445 state.mutate_lock.decrement();
446 }
447 }
448 }
449}
450
451/// The filename for the [`CacheLockMode::DownloadExclusive`] lock.
452const CACHE_LOCK_NAME: &str = ".package-cache";
453/// The filename for the [`CacheLockMode::MutateExclusive`] and
454/// [`CacheLockMode::Shared`] lock.
455const MUTATE_NAME: &str = ".package-cache-mutate";
456
457// Descriptions that are displayed in the "Blocking" message shown to the user.
458const SHARED_DESCR: &str = "shared package cache";
459const DOWNLOAD_EXCLUSIVE_DESCR: &str = "package cache";
460const MUTATE_EXCLUSIVE_DESCR: &str = "package cache mutation";
461
462/// A locker that can be used to acquire locks.
463///
464/// See the [`crate::util::cache_lock`] module documentation for an overview
465/// of how cache locking works.
466#[derive(Debug)]
467pub struct CacheLocker {
468 /// The state of the locker.
469 ///
470 /// [`CacheLocker`] uses interior mutability because it is stuffed inside
471 /// [`GlobalContext`], which does not allow mutation.
472 state: RefCell<CacheState>,
473}
474
475impl CacheLocker {
476 /// Creates a new `CacheLocker`.
477 pub fn new() -> CacheLocker {
478 CacheLocker {
479 state: RefCell::new(CacheState {
480 cache_lock: RecursiveLock::new(CACHE_LOCK_NAME),
481 mutate_lock: RecursiveLock::new(MUTATE_NAME),
482 }),
483 }
484 }
485
486 /// Acquires a lock with the given mode, possibly blocking if another
487 /// cargo is holding the lock.
488 pub fn lock(&self, gctx: &GlobalContext, mode: CacheLockMode) -> CargoResult<CacheLock<'_>> {
489 let mut state = self.state.borrow_mut();
490 let _ = state.lock(gctx, mode, Blocking)?;
491 Ok(CacheLock { mode, locker: self })
492 }
493
494 /// Acquires a lock with the given mode, returning `None` if another cargo
495 /// is holding the lock.
496 pub fn try_lock(
497 &self,
498 gctx: &GlobalContext,
499 mode: CacheLockMode,
500 ) -> CargoResult<Option<CacheLock<'_>>> {
501 let mut state = self.state.borrow_mut();
502 if state.lock(gctx, mode, NonBlocking)? == LockAcquired {
503 Ok(Some(CacheLock { mode, locker: self }))
504 } else {
505 Ok(None)
506 }
507 }
508
509 /// Returns whether or not a lock is held for the given mode in this locker.
510 ///
511 /// This does not tell you whether or not it is locked in some other
512 /// locker (such as in another process).
513 ///
514 /// Note that `Shared` will return true if a `MutateExclusive` lock is
515 /// held, since `MutateExclusive` is just an upgraded `Shared`. Likewise,
516 /// `DownloadExclusive` will return true if a `MutateExclusive` lock is
517 /// held since they overlap.
518 pub fn is_locked(&self, mode: CacheLockMode) -> bool {
519 let state = self.state.borrow();
520 match (
521 mode,
522 state.cache_lock.count,
523 state.mutate_lock.count,
524 state.mutate_lock.is_exclusive,
525 ) {
526 (CacheLockMode::Shared, _, 1.., _) => true,
527 (CacheLockMode::MutateExclusive, _, 1.., true) => true,
528 (CacheLockMode::DownloadExclusive, 1.., _, _) => true,
529 _ => false,
530 }
531 }
532}
533
534/// Returns whether or not the error appears to be from a read-only filesystem.
535fn maybe_readonly(err: &anyhow::Error) -> bool {
536 err.chain().any(|err| {
537 if let Some(io) = err.downcast_ref::<io::Error>() {
538 if io.kind() == io::ErrorKind::PermissionDenied {
539 return true;
540 }
541
542 #[cfg(unix)]
543 return io.raw_os_error() == Some(libc::EROFS);
544 }
545
546 false
547 })
548}