Skip to content

Commit b48c41a

Browse files
committed
Auto merge of #12706 - ehuss:cache-lock-mode, r=epage
Add new package cache lock modes The way locking worked before this PR is that only one cargo could write to the package cache at once (otherwise it could cause corruption). However, it allowed cargo's to read from the package cache while running a build under the assumption that writers are append-only and won't affect reading. This allows multiple builds to run concurrently, only blocking on the part where it is not possible to run concurrently (downloading to the cache). This introduces a new package cache locking strategy to support the ability to safely modify existing cache entries while other cargos are potentially reading from the cache. It has different locking modes: - `MutateExclusive` (new) — Held when cargo wants to modify existing cache entries (such as being introduced for garbage collection in #12634), and ensures only one cargo has access to the cache during that time. - `DownloadExclusive` (renamed) — This is a more specialized name for the lock that was before this PR. A caller should acquire this when downloading into the cache and doing resolution. It ensures that only one cargo can append to the cache, but allows other cargos to concurrently read from the cache. - `Shared` (new) — This is to preserve the old concurrent build behavior by allowing multiple concurrent cargos to hold this while a build is running when it is reading from the cache **Reviewing suggestions:** There are a few commits needed to help with testing which are first. The main commit has the following: - `src/cargo/util/cache_lock.rs` is an abstraction around package cache locks, and is the heart of the change. It should have comments and notes which should guide what it is doing. The `CacheLocker` is stored in `Config` along with all our other global stuff. - Every call to `config.acquire_package_cache_lock()` has been changed to explicitly state which lock mode it wants to lock the package cache in. - `Context::compile` is the key point where the `Shared` lock is acquired, ensuring that no mutation is done while the cache is being read. - `MutateExclusive` is not used in this PR, but is being added in preparation for #12634. - The non-blocking `try_acquire_package_cache_lock` API is not used in this PR, but is being added in preparation for #12634 to allow automatic gc to skip running if another cargo is already running (to avoid unnecessary blocking). - `src/cargo/util/flock.rs` has been updated with some code cleanup (removing unused stuff), adds support for non-blocking locks, and renames some functions to make their operation clearer. - `tests/testsuite/cache_lock.rs` contains tests for all the different permutations of ways of acquiring locks.
2 parents 0871c0e + 78bb7c5 commit b48c41a

29 files changed

+1239
-228
lines changed

crates/cargo-test-support/src/compare.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ fn substitute_macros(input: &str) -> String {
236236
("[SKIPPING]", " Skipping"),
237237
("[WAITING]", " Waiting"),
238238
("[PUBLISHED]", " Published"),
239+
("[BLOCKING]", " Blocking"),
239240
];
240241
let mut result = input.to_owned();
241242
for &(pat, subst) in &macros {

crates/cargo-test-support/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
1313
use std::process::{Command, Output};
1414
use std::str;
1515
use std::sync::OnceLock;
16+
use std::thread::JoinHandle;
1617
use std::time::{self, Duration};
1718

1819
use anyhow::{bail, Result};
@@ -1470,3 +1471,50 @@ pub fn symlink_supported() -> bool {
14701471
pub fn no_such_file_err_msg() -> String {
14711472
std::io::Error::from_raw_os_error(2).to_string()
14721473
}
1474+
1475+
/// Helper to retry a function `n` times.
1476+
///
1477+
/// The function should return `Some` when it is ready.
1478+
pub fn retry<F, R>(n: u32, mut f: F) -> R
1479+
where
1480+
F: FnMut() -> Option<R>,
1481+
{
1482+
let mut count = 0;
1483+
let start = std::time::Instant::now();
1484+
loop {
1485+
if let Some(r) = f() {
1486+
return r;
1487+
}
1488+
count += 1;
1489+
if count > n {
1490+
panic!(
1491+
"test did not finish within {n} attempts ({:?} total)",
1492+
start.elapsed()
1493+
);
1494+
}
1495+
sleep_ms(100);
1496+
}
1497+
}
1498+
1499+
#[test]
1500+
#[should_panic(expected = "test did not finish")]
1501+
fn retry_fails() {
1502+
retry(2, || None::<()>);
1503+
}
1504+
1505+
/// Helper that waits for a thread to finish, up to `n` tenths of a second.
1506+
pub fn thread_wait_timeout<T>(n: u32, thread: JoinHandle<T>) -> T {
1507+
retry(n, || thread.is_finished().then_some(()));
1508+
thread.join().unwrap()
1509+
}
1510+
1511+
/// Helper that runs some function, and waits up to `n` tenths of a second for
1512+
/// it to finish.
1513+
pub fn threaded_timeout<F, R>(n: u32, f: F) -> R
1514+
where
1515+
F: FnOnce() -> R + Send + 'static,
1516+
R: Send + 'static,
1517+
{
1518+
let thread = std::thread::spawn(|| f());
1519+
thread_wait_timeout(n, thread)
1520+
}

crates/xtask-bump-check/src/xtask.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use cargo::core::Registry;
2222
use cargo::core::SourceId;
2323
use cargo::core::Workspace;
2424
use cargo::sources::source::QueryKind;
25+
use cargo::util::cache_lock::CacheLockMode;
2526
use cargo::util::command_prelude::*;
2627
use cargo::util::ToSemver;
2728
use cargo::CargoResult;
@@ -347,7 +348,7 @@ fn check_crates_io<'a>(
347348
) -> CargoResult<()> {
348349
let source_id = SourceId::crates_io(config)?;
349350
let mut registry = PackageRegistry::new(config)?;
350-
let _lock = config.acquire_package_cache_lock()?;
351+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
351352
registry.lock_patches();
352353
config.shell().status(
353354
STATUS,

src/cargo/core/compiler/context/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex};
77
use crate::core::compiler::compilation::{self, UnitOutput};
88
use crate::core::compiler::{self, artifact, Unit};
99
use crate::core::PackageId;
10+
use crate::util::cache_lock::CacheLockMode;
1011
use crate::util::errors::CargoResult;
1112
use crate::util::profile;
1213
use anyhow::{bail, Context as _};
@@ -132,6 +133,13 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
132133
///
133134
/// [`ops::cargo_compile`]: ../../../ops/cargo_compile/index.html
134135
pub fn compile(mut self, exec: &Arc<dyn Executor>) -> CargoResult<Compilation<'cfg>> {
136+
// A shared lock is held during the duration of the build since rustc
137+
// needs to read from the `src` cache, and we don't want other
138+
// commands modifying the `src` cache while it is running.
139+
let _lock = self
140+
.bcx
141+
.config
142+
.acquire_package_cache_lock(CacheLockMode::Shared)?;
135143
let mut queue = JobQueue::new(self.bcx);
136144
let mut plan = BuildPlan::new();
137145
let build_plan = self.bcx.build_config.build_plan;

src/cargo/core/compiler/future_incompat.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::core::compiler::BuildContext;
3737
use crate::core::{Dependency, PackageId, Workspace};
3838
use crate::sources::source::QueryKind;
3939
use crate::sources::SourceConfigMap;
40+
use crate::util::cache_lock::CacheLockMode;
4041
use crate::util::{iter_join, CargoResult};
4142
use anyhow::{bail, format_err, Context};
4243
use serde::{Deserialize, Serialize};
@@ -166,7 +167,7 @@ impl OnDiskReports {
166167
let on_disk = serde_json::to_vec(&self).unwrap();
167168
if let Err(e) = ws
168169
.target_dir()
169-
.open_rw(
170+
.open_rw_exclusive_create(
170171
FUTURE_INCOMPAT_FILE,
171172
ws.config(),
172173
"Future incompatibility report",
@@ -190,7 +191,7 @@ impl OnDiskReports {
190191

191192
/// Loads the on-disk reports.
192193
pub fn load(ws: &Workspace<'_>) -> CargoResult<OnDiskReports> {
193-
let report_file = match ws.target_dir().open_ro(
194+
let report_file = match ws.target_dir().open_ro_shared(
194195
FUTURE_INCOMPAT_FILE,
195196
ws.config(),
196197
"Future incompatible report",
@@ -297,7 +298,10 @@ fn render_report(per_package_reports: &[FutureIncompatReportPackage]) -> BTreeMa
297298
/// This is best-effort - if an error occurs, `None` will be returned.
298299
fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet<PackageId>) -> Option<String> {
299300
// This in general ignores all errors since this is opportunistic.
300-
let _lock = ws.config().acquire_package_cache_lock().ok()?;
301+
let _lock = ws
302+
.config()
303+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)
304+
.ok()?;
301305
// Create a set of updated registry sources.
302306
let map = SourceConfigMap::new(ws.config()).ok()?;
303307
let mut package_ids: BTreeSet<_> = package_ids

src/cargo/core/compiler/layout.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ impl Layout {
166166
// For now we don't do any more finer-grained locking on the artifact
167167
// directory, so just lock the entire thing for the duration of this
168168
// compile.
169-
let lock = dest.open_rw(".cargo-lock", ws.config(), "build directory")?;
169+
let lock = dest.open_rw_exclusive_create(".cargo-lock", ws.config(), "build directory")?;
170170
let root = root.into_path_unlocked();
171171
let dest = dest.into_path_unlocked();
172172
let deps = dest.join("deps");

src/cargo/core/package.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::core::resolver::{HasDevUnits, Resolve};
2424
use crate::core::{Dependency, Manifest, PackageId, SourceId, Target};
2525
use crate::core::{Summary, Workspace};
2626
use crate::sources::source::{MaybePackage, SourceMap};
27-
use crate::util::config::PackageCacheLock;
27+
use crate::util::cache_lock::{CacheLock, CacheLockMode};
2828
use crate::util::errors::{CargoResult, HttpNotSuccessful};
2929
use crate::util::interning::InternedString;
3030
use crate::util::network::http::http_handle_and_timeout;
@@ -367,7 +367,7 @@ pub struct Downloads<'a, 'cfg> {
367367
next_speed_check_bytes_threshold: Cell<u64>,
368368
/// Global filesystem lock to ensure only one Cargo is downloading at a
369369
/// time.
370-
_lock: PackageCacheLock<'cfg>,
370+
_lock: CacheLock<'cfg>,
371371
}
372372

373373
struct Download<'cfg> {
@@ -465,7 +465,9 @@ impl<'cfg> PackageSet<'cfg> {
465465
timeout,
466466
next_speed_check: Cell::new(Instant::now()),
467467
next_speed_check_bytes_threshold: Cell::new(0),
468-
_lock: self.config.acquire_package_cache_lock()?,
468+
_lock: self
469+
.config
470+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?,
469471
})
470472
}
471473

@@ -478,6 +480,9 @@ impl<'cfg> PackageSet<'cfg> {
478480

479481
pub fn get_many(&self, ids: impl IntoIterator<Item = PackageId>) -> CargoResult<Vec<&Package>> {
480482
let mut pkgs = Vec::new();
483+
let _lock = self
484+
.config
485+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
481486
let mut downloads = self.enable_download()?;
482487
for id in ids {
483488
pkgs.extend(downloads.start(id)?);

src/cargo/ops/cargo_add/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::core::Shell;
2323
use crate::core::Summary;
2424
use crate::core::Workspace;
2525
use crate::sources::source::QueryKind;
26+
use crate::util::cache_lock::CacheLockMode;
2627
use crate::util::style;
2728
use crate::util::toml_mut::dependency::Dependency;
2829
use crate::util::toml_mut::dependency::GitSource;
@@ -77,7 +78,9 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<(
7778
let mut registry = PackageRegistry::new(options.config)?;
7879

7980
let deps = {
80-
let _lock = options.config.acquire_package_cache_lock()?;
81+
let _lock = options
82+
.config
83+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
8184
registry.lock_patches();
8285
options
8386
.dependencies

src/cargo/ops/cargo_generate_lockfile.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::core::resolver::features::{CliFeatures, HasDevUnits};
33
use crate::core::{PackageId, PackageIdSpec};
44
use crate::core::{Resolve, SourceId, Workspace};
55
use crate::ops;
6+
use crate::util::cache_lock::CacheLockMode;
67
use crate::util::config::Config;
78
use crate::util::style;
89
use crate::util::CargoResult;
@@ -48,7 +49,9 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
4849

4950
// Updates often require a lot of modifications to the registry, so ensure
5051
// that we're synchronized against other Cargos.
51-
let _lock = ws.config().acquire_package_cache_lock()?;
52+
let _lock = ws
53+
.config()
54+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
5255

5356
let max_rust_version = ws.rust_version();
5457

src/cargo/ops/cargo_package.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::core::{registry::PackageRegistry, resolver::HasDevUnits};
1313
use crate::core::{Feature, Shell, Verbosity, Workspace};
1414
use crate::core::{Package, PackageId, PackageSet, Resolve, SourceId};
1515
use crate::sources::PathSource;
16+
use crate::util::cache_lock::CacheLockMode;
1617
use crate::util::config::JobsConfig;
1718
use crate::util::errors::CargoResult;
1819
use crate::util::toml::TomlManifest;
@@ -132,7 +133,7 @@ pub fn package_one(
132133
let dir = ws.target_dir().join("package");
133134
let mut dst = {
134135
let tmp = format!(".{}", filename);
135-
dir.open_rw(&tmp, config, "package scratch space")?
136+
dir.open_rw_exclusive_create(&tmp, config, "package scratch space")?
136137
};
137138

138139
// Package up and test a temporary tarball and only move it to the final
@@ -806,7 +807,7 @@ pub fn check_yanked(
806807
) -> CargoResult<()> {
807808
// Checking the yanked status involves taking a look at the registry and
808809
// maybe updating files, so be sure to lock it here.
809-
let _lock = config.acquire_package_cache_lock()?;
810+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
810811

811812
let mut sources = pkg_set.sources_mut();
812813
let mut pending: Vec<PackageId> = resolve.iter().collect();

src/cargo/ops/common_for_install_and_uninstall.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::ops::{self, CompileFilter, CompileOptions};
1717
use crate::sources::source::QueryKind;
1818
use crate::sources::source::Source;
1919
use crate::sources::PathSource;
20+
use crate::util::cache_lock::CacheLockMode;
2021
use crate::util::errors::CargoResult;
2122
use crate::util::Config;
2223
use crate::util::{FileLock, Filesystem};
@@ -97,8 +98,10 @@ pub struct CrateListingV1 {
9798
impl InstallTracker {
9899
/// Create an InstallTracker from information on disk.
99100
pub fn load(config: &Config, root: &Filesystem) -> CargoResult<InstallTracker> {
100-
let v1_lock = root.open_rw(Path::new(".crates.toml"), config, "crate metadata")?;
101-
let v2_lock = root.open_rw(Path::new(".crates2.json"), config, "crate metadata")?;
101+
let v1_lock =
102+
root.open_rw_exclusive_create(Path::new(".crates.toml"), config, "crate metadata")?;
103+
let v2_lock =
104+
root.open_rw_exclusive_create(Path::new(".crates2.json"), config, "crate metadata")?;
102105

103106
let v1 = (|| -> CargoResult<_> {
104107
let mut contents = String::new();
@@ -536,7 +539,7 @@ where
536539
// This operation may involve updating some sources or making a few queries
537540
// which may involve frobbing caches, as a result make sure we synchronize
538541
// with other global Cargos
539-
let _lock = config.acquire_package_cache_lock()?;
542+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
540543

541544
if needs_update {
542545
source.invalidate_cache();
@@ -604,7 +607,7 @@ where
604607
// This operation may involve updating some sources or making a few queries
605608
// which may involve frobbing caches, as a result make sure we synchronize
606609
// with other global Cargos
607-
let _lock = config.acquire_package_cache_lock()?;
610+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
608611

609612
source.invalidate_cache();
610613

src/cargo/ops/lockfile.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub fn load_pkg_lockfile(ws: &Workspace<'_>) -> CargoResult<Option<Resolve>> {
1212
return Ok(None);
1313
}
1414

15-
let mut f = lock_root.open_ro("Cargo.lock", ws.config(), "Cargo.lock file")?;
15+
let mut f = lock_root.open_ro_shared("Cargo.lock", ws.config(), "Cargo.lock file")?;
1616

1717
let mut s = String::new();
1818
f.read_to_string(&mut s)
@@ -79,7 +79,7 @@ pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &mut Resolve) -> CargoRes
7979

8080
// Ok, if that didn't work just write it out
8181
lock_root
82-
.open_rw("Cargo.lock", ws.config(), "Cargo.lock file")
82+
.open_rw_exclusive_create("Cargo.lock", ws.config(), "Cargo.lock file")
8383
.and_then(|mut f| {
8484
f.file().set_len(0)?;
8585
f.write_all(out.as_bytes())?;
@@ -100,7 +100,7 @@ fn resolve_to_string_orig(
100100
) -> (Option<String>, String, Filesystem) {
101101
// Load the original lock file if it exists.
102102
let lock_root = lock_root(ws);
103-
let orig = lock_root.open_ro("Cargo.lock", ws.config(), "Cargo.lock file");
103+
let orig = lock_root.open_ro_shared("Cargo.lock", ws.config(), "Cargo.lock file");
104104
let orig = orig.and_then(|mut f| {
105105
let mut s = String::new();
106106
f.read_to_string(&mut s)?;

src/cargo/ops/registry/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::core::SourceId;
2222
use crate::sources::source::Source;
2323
use crate::sources::{RegistrySource, SourceConfigMap};
2424
use crate::util::auth;
25+
use crate::util::cache_lock::CacheLockMode;
2526
use crate::util::config::{Config, PathAndArgs};
2627
use crate::util::errors::CargoResult;
2728
use crate::util::network::http::http_handle;
@@ -131,7 +132,7 @@ fn registry(
131132
}
132133

133134
let cfg = {
134-
let _lock = config.acquire_package_cache_lock()?;
135+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
135136
let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), config)?;
136137
// Only update the index if `force_update` is set.
137138
if force_update {

src/cargo/ops/registry/publish.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::sources::source::QueryKind;
3030
use crate::sources::SourceConfigMap;
3131
use crate::sources::CRATES_IO_REGISTRY;
3232
use crate::util::auth;
33+
use crate::util::cache_lock::CacheLockMode;
3334
use crate::util::config::JobsConfig;
3435
use crate::util::Progress;
3536
use crate::util::ProgressStyle;
@@ -233,7 +234,7 @@ fn wait_for_publish(
233234
progress.tick_now(0, max, "")?;
234235
let is_available = loop {
235236
{
236-
let _lock = config.acquire_package_cache_lock()?;
237+
let _lock = config.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
237238
// Force re-fetching the source
238239
//
239240
// As pulling from a git source is expensive, we track when we've done it within the

src/cargo/ops/resolve.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use crate::core::Feature;
6868
use crate::core::{GitReference, PackageId, PackageIdSpec, PackageSet, SourceId, Workspace};
6969
use crate::ops;
7070
use crate::sources::PathSource;
71+
use crate::util::cache_lock::CacheLockMode;
7172
use crate::util::errors::CargoResult;
7273
use crate::util::RustVersion;
7374
use crate::util::{profile, CanonicalUrl};
@@ -289,7 +290,9 @@ pub fn resolve_with_previous<'cfg>(
289290
) -> CargoResult<Resolve> {
290291
// We only want one Cargo at a time resolving a crate graph since this can
291292
// involve a lot of frobbing of the global caches.
292-
let _lock = ws.config().acquire_package_cache_lock()?;
293+
let _lock = ws
294+
.config()
295+
.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
293296

294297
// Here we place an artificial limitation that all non-registry sources
295298
// cannot be locked at more than one revision. This means that if a Git

src/cargo/sources/git/source.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::sources::source::MaybePackage;
88
use crate::sources::source::QueryKind;
99
use crate::sources::source::Source;
1010
use crate::sources::PathSource;
11+
use crate::util::cache_lock::CacheLockMode;
1112
use crate::util::errors::CargoResult;
1213
use crate::util::hex::short_hash;
1314
use crate::util::Config;
@@ -212,7 +213,9 @@ impl<'cfg> Source for GitSource<'cfg> {
212213
// Ignore errors creating it, in case this is a read-only filesystem:
213214
// perhaps the later operations can succeed anyhow.
214215
let _ = git_fs.create_dir();
215-
let git_path = self.config.assert_package_cache_locked(&git_fs);
216+
let git_path = self
217+
.config
218+
.assert_package_cache_locked(CacheLockMode::DownloadExclusive, &git_fs);
216219

217220
// Before getting a checkout, make sure that `<cargo_home>/git` is
218221
// marked as excluded from indexing and backups. Older versions of Cargo

0 commit comments

Comments
 (0)