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}