Skip to content

Commit e6af9a2

Browse files
committed
fix: Added an "acquisition lock" to prevent deadlocks (fine grain locking)
This commit adds the concept of an acquisition lock that is held exclusively during the fingerprint checking and job preparation. We release this lock prior to executing jobs in the queue to allow other cargos to proceed. This lock prevents deadlocks by only allowing a single Cargo to lock the build unit locks at a time effectively making this operation a "transaction" guarded by the acquisition lock. Note that we added a new `.acquisition-lock` instead of reusing `.cargo-lock` This is needed as we drop the `.acquisition-lock` during the compilation phase, however `cargo clean` relies on `.cargo-lock` to avoid cleaning during a build. If we were to change the behavior of `.cargo-lock` to be unlocked during the compilation phase, it would open us up to having `cargo clean` potentially corrupt an in progress build.
1 parent 39a24a5 commit e6af9a2

File tree

6 files changed

+93
-0
lines changed

6 files changed

+93
-0
lines changed

src/cargo/core/compiler/build_runner/compilation_files.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,11 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
444444
.map(Arc::clone)
445445
}
446446

447+
/// Returns the path to the acquisition lock, used to avoid multiple Cargos from deadlocking
448+
pub fn acquisition_lock(&self) -> &Path {
449+
self.host.build_dir().acquisition_lock()
450+
}
451+
447452
/// Returns the path where the output for the given unit and `FileType`
448453
/// should be uplifted to.
449454
///

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
178178
self.check_collisions()?;
179179
self.compute_metadata_for_doc_units();
180180

181+
// When -Zfine-grain-locking is enabled, we take an "acquisition lock" exclusively
182+
// that we hold while taking other locks. This lock will block other Cargos from also
183+
// taking locks possibly causing deadlocks. We drop the acquisition lock before we start
184+
// executing jobs allowing other Cargos to compile in parallel if they do not share build
185+
// units.
186+
if self.bcx.gctx.cli_unstable().fine_grain_locking {
187+
self.lock_manager.acquire_acquisition_lock(&self)?;
188+
}
189+
181190
// We need to make sure that if there were any previous docs already compiled,
182191
// they were compiled with the same Rustc version that we're currently using.
183192
// See the function doc comment for more.
@@ -200,6 +209,11 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
200209
fingerprint.clear_memoized();
201210
}
202211

212+
// Release acquisition lock allowing other Cargos to proceed
213+
if self.bcx.gctx.cli_unstable().fine_grain_locking {
214+
self.lock_manager.release_acquisition_lock()?;
215+
}
216+
203217
// Now that we've figured out everything that we're going to do, do it!
204218
queue.execute(&mut self)?;
205219

src/cargo/core/compiler/layout.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ impl Layout {
286286
let build_dest = build_dest.as_path_unlocked();
287287
let deps = build_dest.join("deps");
288288
let artifact = deps.join("artifact");
289+
let acquisition_lock = build_dest.join(".acquisition-lock");
289290

290291
let artifact_dir = if must_take_artifact_dir_lock {
291292
// For now we don't do any more finer-grained locking on the artifact
@@ -323,6 +324,7 @@ impl Layout {
323324
fingerprint: build_dest.join(".fingerprint"),
324325
examples: build_dest.join("examples"),
325326
tmp: build_root.join("tmp"),
327+
acquisition_lock,
326328
_lock: build_dir_lock,
327329
is_new_layout,
328330
},
@@ -405,6 +407,7 @@ pub struct BuildDirLayout {
405407
examples: PathBuf,
406408
/// The directory for temporary data of integration tests and benches
407409
tmp: PathBuf,
410+
acquisition_lock: PathBuf,
408411
/// The lockfile for a build (`.cargo-lock`). Will be unlocked when this
409412
/// struct is `drop`ped.
410413
///
@@ -505,4 +508,8 @@ impl BuildDirLayout {
505508
paths::create_dir_all(&self.tmp)?;
506509
Ok(&self.tmp)
507510
}
511+
// Fetch the acquisition lock path
512+
pub fn acquisition_lock(&self) -> &Path {
513+
&self.acquisition_lock
514+
}
508515
}

src/cargo/core/compiler/locking.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,52 @@ use tracing::instrument;
1616

1717
/// A struct to store the lock handles for build units during compilation.
1818
pub struct LockManager {
19+
acquisition: RwLock<Option<FileLock>>,
1920
locks: RwLock<HashMap<LockKey, FileLock>>,
2021
}
2122

2223
impl LockManager {
2324
pub fn new() -> Self {
2425
Self {
26+
acquisition: RwLock::new(None),
2527
locks: RwLock::new(HashMap::new()),
2628
}
2729
}
2830

31+
/// Acquires the acquisition lock required to call [`LockManager::lock`] and [`LockManager::lock_shared`]
32+
///
33+
/// This should be called prior to attempting lock build units and should be released prior to
34+
/// executing compilation jobs to allow other Cargos to proceed if they do not share any build
35+
/// units.
36+
#[instrument(skip_all)]
37+
pub fn acquire_acquisition_lock(&self, build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> {
38+
let path = build_runner.files().acquisition_lock();
39+
let fs = Filesystem::new(path.to_path_buf());
40+
41+
let lock = fs.open_rw_exclusive_create(&path, build_runner.bcx.gctx, "acquisition lock")?;
42+
43+
let Ok(mut acquisition_lock) = self.acquisition.write() else {
44+
bail!("failed to take acquisition write lock");
45+
};
46+
*acquisition_lock = Some(lock);
47+
48+
Ok(())
49+
}
50+
51+
/// Releases the acquisition lock, see [`LockManager::acquire_acquisition_lock`]
52+
#[instrument(skip_all)]
53+
pub fn release_acquisition_lock(&self) -> CargoResult<()> {
54+
let Ok(mut acquisition_lock) = self.acquisition.write() else {
55+
bail!("failed to take acquisition write lock");
56+
};
57+
assert!(
58+
acquisition_lock.is_some(),
59+
"attempted to release acquisition while it was not taken"
60+
);
61+
*acquisition_lock = None;
62+
Ok(())
63+
}
64+
2965
/// Takes a shared lock on a given [`Unit`]
3066
/// This prevents other Cargo instances from compiling (writing) to
3167
/// this build unit.
@@ -38,6 +74,10 @@ impl LockManager {
3874
build_runner: &BuildRunner<'_, '_>,
3975
unit: &Unit,
4076
) -> CargoResult<LockKey> {
77+
assert!(
78+
self.acquisition.read().unwrap().is_some(),
79+
"attempted to take shared lock without acquisition lock"
80+
);
4181
let key = LockKey::from_unit(build_runner, unit);
4282
tracing::Span::current().record("key", key.0.to_str());
4383

@@ -60,6 +100,10 @@ impl LockManager {
60100

61101
#[instrument(skip(self))]
62102
pub fn lock(&self, key: &LockKey) -> CargoResult<()> {
103+
assert!(
104+
self.acquisition.read().unwrap().is_some(),
105+
"attempted to take exclusive lock without acquisition lock"
106+
);
63107
let locks = self.locks.read().unwrap();
64108
if let Some(lock) = locks.get(&key) {
65109
lock.file().lock()?;

src/cargo/core/compiler/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ fn compile<'gctx>(
250250
// the user from running `cargo build` while developing. Generally, only a few
251251
// workspace units will be changing in each build and the units will not be
252252
// shared between `build` and `check` allowing them to run in parallel.
253+
//
254+
// Also note that we unlock before taking the exclusive lock as not all
255+
// platforms support lock upgrading. This is safe as we hold the acquisition
256+
// lock so we should be the only one operating on the locks so we can assume
257+
// that the lock will not be stolen by another instance.
258+
build_runner.lock_manager.unlock(&lock)?;
253259
build_runner.lock_manager.lock(&lock)?;
254260
job.after(downgrade_lock_to_shared(lock));
255261
}

tests/testsuite/build_dir.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fn binary_with_debug() {
4040
[ROOT]/foo/build-dir/.rustc_info.json
4141
[ROOT]/foo/build-dir/CACHEDIR.TAG
4242
[ROOT]/foo/build-dir/debug/.cargo-lock
43+
[ROOT]/foo/build-dir/debug/.acquisition-lock
4344
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
4445
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
4546
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -95,6 +96,7 @@ fn binary_with_release() {
9596
[ROOT]/foo/build-dir/.rustc_info.json
9697
[ROOT]/foo/build-dir/CACHEDIR.TAG
9798
[ROOT]/foo/build-dir/release/.cargo-lock
99+
[ROOT]/foo/build-dir/release/.acquisition-lock
98100
[ROOT]/foo/build-dir/release/build/foo/[HASH]/fingerprint/bin-foo
99101
[ROOT]/foo/build-dir/release/build/foo/[HASH]/fingerprint/bin-foo.json
100102
[ROOT]/foo/build-dir/release/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -205,6 +207,7 @@ fn should_default_to_target() {
205207
[ROOT]/foo/target/.rustc_info.json
206208
[ROOT]/foo/target/CACHEDIR.TAG
207209
[ROOT]/foo/target/debug/.cargo-lock
210+
[ROOT]/foo/target/debug/.acquisition-lock
208211
[ROOT]/foo/target/debug/build/foo/[HASH]/fingerprint/bin-foo
209212
[ROOT]/foo/target/debug/build/foo/[HASH]/fingerprint/bin-foo.json
210213
[ROOT]/foo/target/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -234,6 +237,7 @@ fn should_respect_env_var() {
234237
[ROOT]/foo/build-dir/.rustc_info.json
235238
[ROOT]/foo/build-dir/CACHEDIR.TAG
236239
[ROOT]/foo/build-dir/debug/.cargo-lock
240+
[ROOT]/foo/build-dir/debug/.acquisition-lock
237241
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
238242
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
239243
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -279,6 +283,7 @@ fn build_script_should_output_to_build_dir() {
279283
p.root().join("build-dir").assert_build_dir_layout(str![[r#"
280284
[ROOT]/foo/build-dir/CACHEDIR.TAG
281285
[ROOT]/foo/build-dir/debug/.cargo-lock
286+
[ROOT]/foo/build-dir/debug/.acquisition-lock
282287
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo.txt
283288
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/build_script_build[..].d
284289
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/build_script_build[..][EXE]
@@ -342,6 +347,7 @@ fn cargo_tmpdir_should_output_to_build_dir() {
342347
p.root().join("build-dir").assert_build_dir_layout(str![[r#"
343348
[ROOT]/foo/build-dir/CACHEDIR.TAG
344349
[ROOT]/foo/build-dir/debug/.cargo-lock
350+
[ROOT]/foo/build-dir/debug/.acquisition-lock
345351
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo-[HASH].d
346352
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo.d
347353
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo[..].d
@@ -402,6 +408,7 @@ fn examples_should_output_to_build_dir_and_uplift_to_target_dir() {
402408
[ROOT]/foo/build-dir/.rustc_info.json
403409
[ROOT]/foo/build-dir/CACHEDIR.TAG
404410
[ROOT]/foo/build-dir/debug/.cargo-lock
411+
[ROOT]/foo/build-dir/debug/.acquisition-lock
405412
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-example-foo
406413
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/example-foo
407414
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/example-foo.json
@@ -448,6 +455,7 @@ fn benches_should_output_to_build_dir() {
448455
p.root().join("build-dir").assert_build_dir_layout(str![[r#"
449456
[ROOT]/foo/build-dir/CACHEDIR.TAG
450457
[ROOT]/foo/build-dir/debug/.cargo-lock
458+
[ROOT]/foo/build-dir/debug/.acquisition-lock
451459
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo-[HASH].d
452460
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo[..].d
453461
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo-[HASH][EXE]
@@ -527,6 +535,7 @@ fn cargo_package_should_build_in_build_dir_and_output_to_target_dir() {
527535
p.root().join("build-dir").assert_build_dir_layout(str![[r#"
528536
[ROOT]/foo/build-dir/.rustc_info.json
529537
[ROOT]/foo/build-dir/debug/.cargo-lock
538+
[ROOT]/foo/build-dir/debug/.acquisition-lock
530539
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
531540
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
532541
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -608,6 +617,7 @@ fn cargo_clean_should_clean_the_target_dir_and_build_dir() {
608617
[ROOT]/foo/build-dir/.rustc_info.json
609618
[ROOT]/foo/build-dir/CACHEDIR.TAG
610619
[ROOT]/foo/build-dir/debug/.cargo-lock
620+
[ROOT]/foo/build-dir/debug/.acquisition-lock
611621
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
612622
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
613623
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -678,6 +688,7 @@ fn cargo_clean_should_remove_correct_files() {
678688
[ROOT]/foo/build-dir/.rustc_info.json
679689
[ROOT]/foo/build-dir/CACHEDIR.TAG
680690
[ROOT]/foo/build-dir/debug/.cargo-lock
691+
[ROOT]/foo/build-dir/debug/.acquisition-lock
681692
[ROOT]/foo/build-dir/debug/build/bar/[HASH]/out/bar-[HASH].d
682693
[ROOT]/foo/build-dir/debug/build/bar/[HASH]/out/libbar-[HASH].rlib
683694
[ROOT]/foo/build-dir/debug/build/bar/[HASH]/out/libbar-[HASH].rmeta
@@ -705,6 +716,7 @@ fn cargo_clean_should_remove_correct_files() {
705716
[ROOT]/foo/build-dir/.rustc_info.json
706717
[ROOT]/foo/build-dir/CACHEDIR.TAG
707718
[ROOT]/foo/build-dir/debug/.cargo-lock
719+
[ROOT]/foo/build-dir/debug/.acquisition-lock
708720
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo[..][EXE]
709721
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/out/foo[..].d
710722
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
@@ -841,6 +853,7 @@ fn template_workspace_root() {
841853
[ROOT]/foo/build-dir/.rustc_info.json
842854
[ROOT]/foo/build-dir/CACHEDIR.TAG
843855
[ROOT]/foo/build-dir/debug/.cargo-lock
856+
[ROOT]/foo/build-dir/debug/.acquisition-lock
844857
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
845858
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
846859
[ROOT]/foo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -889,6 +902,7 @@ fn template_cargo_cache_home() {
889902
[ROOT]/home/.cargo/build-dir/.rustc_info.json
890903
[ROOT]/home/.cargo/build-dir/CACHEDIR.TAG
891904
[ROOT]/home/.cargo/build-dir/debug/.cargo-lock
905+
[ROOT]/home/.cargo/build-dir/debug/.acquisition-lock
892906
[ROOT]/home/.cargo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
893907
[ROOT]/home/.cargo/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
894908
[ROOT]/home/.cargo/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -951,6 +965,7 @@ fn template_workspace_path_hash() {
951965
[ROOT]/foo/foo/[HASH]/build-dir/.rustc_info.json
952966
[ROOT]/foo/foo/[HASH]/build-dir/CACHEDIR.TAG
953967
[ROOT]/foo/foo/[HASH]/build-dir/debug/.cargo-lock
968+
[ROOT]/foo/foo/[HASH]/build-dir/debug/.acquisition-lock
954969
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo
955970
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/bin-foo.json
956971
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/dep-bin-foo
@@ -1019,6 +1034,7 @@ fn template_workspace_path_hash_should_handle_symlink() {
10191034
[ROOT]/foo/foo/[HASH]/build-dir/.rustc_info.json
10201035
[ROOT]/foo/foo/[HASH]/build-dir/CACHEDIR.TAG
10211036
[ROOT]/foo/foo/[HASH]/build-dir/debug/.cargo-lock
1037+
[ROOT]/foo/foo/[HASH]/build-dir/debug/.acquisition-lock
10221038
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/dep-lib-foo
10231039
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/invoked.timestamp
10241040
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/lib-foo
@@ -1058,6 +1074,7 @@ fn template_workspace_path_hash_should_handle_symlink() {
10581074
[ROOT]/foo/foo/[HASH]/build-dir/.rustc_info.json
10591075
[ROOT]/foo/foo/[HASH]/build-dir/CACHEDIR.TAG
10601076
[ROOT]/foo/foo/[HASH]/build-dir/debug/.cargo-lock
1077+
[ROOT]/foo/foo/[HASH]/build-dir/debug/.acquisition-lock
10611078
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/dep-lib-foo
10621079
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/invoked.timestamp
10631080
[ROOT]/foo/foo/[HASH]/build-dir/debug/build/foo/[HASH]/fingerprint/lib-foo

0 commit comments

Comments
 (0)