#
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}