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

File tree

29 files changed

+1239
-228
lines changed

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();

0 commit comments

Comments
 (0)