Skip to content

Commit 3fc0143

Browse files
committed
feat(locking): Added build unit level locking
1 parent 6b40446 commit 3fc0143

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
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
@@ -277,6 +277,11 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
277277
self.layout(unit.kind).build_dir().fingerprint(&dir)
278278
}
279279

280+
pub fn build_unit_lock(&self, unit: &Unit) -> PathBuf {
281+
let dir = self.pkg_dir(unit);
282+
self.layout(unit.kind).build_dir().build_unit_lock(&dir)
283+
}
284+
280285
/// Directory where incremental output for the given unit should go.
281286
pub fn incremental_dir(&self, unit: &Unit) -> &Path {
282287
self.layout(unit.kind).build_dir().incremental()

src/cargo/core/compiler/layout.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,13 @@ impl Layout {
152152
// For now we don't do any more finer-grained locking on the artifact
153153
// directory, so just lock the entire thing for the duration of this
154154
// compile.
155-
let artifact_dir_lock =
156-
dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?;
155+
let artifact_dir_lock = if !is_new_layout {
156+
Some(dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?)
157+
} else {
158+
None
159+
};
157160

158-
let build_dir_lock = if root != build_root {
161+
let build_dir_lock = if root != build_root && !is_new_layout {
159162
Some(build_dest.open_rw_exclusive_create(
160163
".cargo-lock",
161164
ws.gctx(),
@@ -222,7 +225,7 @@ pub struct ArtifactDirLayout {
222225
timings: PathBuf,
223226
/// The lockfile for a build (`.cargo-lock`). Will be unlocked when this
224227
/// struct is `drop`ped.
225-
_lock: FileLock,
228+
_lock: Option<FileLock>,
226229
}
227230

228231
impl ArtifactDirLayout {
@@ -345,6 +348,9 @@ impl BuildDirLayout {
345348
self.build().join(pkg_dir)
346349
}
347350
}
351+
pub fn build_unit_lock(&self, pkg_dir: &str) -> PathBuf {
352+
self.build_unit(pkg_dir).join("primary.lck")
353+
}
348354
/// Fetch the artifact path.
349355
pub fn artifact(&self) -> &Path {
350356
&self.artifact

src/cargo/core/compiler/locking.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! This module handles the locking logic during compilation.
2+
//!
3+
//! The locking scheme is based on build unit level locking.
4+
//! Generally a build unit will follow the following flow:
5+
//! 1. Acquire an exclusive lock for the current build unit.
6+
//! 2. Acquire shared locks on all dependency build units.
7+
//! 3. Begin building with rustc
8+
//! 5. Once complete release all locks.
9+
//!
10+
//! [`CompilationLock`] is the primary interface for locking.
11+
12+
use std::{
13+
fs::{File, OpenOptions},
14+
path::{Path, PathBuf},
15+
};
16+
17+
use itertools::Itertools;
18+
use tracing::instrument;
19+
20+
use crate::{
21+
CargoResult,
22+
core::compiler::{BuildRunner, Unit},
23+
};
24+
25+
/// A lock for compiling a build unit.
26+
///
27+
/// Internally this lock is made up of many [`UnitLock`]s for the unit and it's dependencies.
28+
pub struct CompilationLock {
29+
/// The path to the lock file of the unit to compile
30+
unit: UnitLock,
31+
/// The paths to lock files of the unit's dependencies
32+
dependency_units: Vec<UnitLock>,
33+
}
34+
35+
impl CompilationLock {
36+
pub fn new(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> Self {
37+
let unit_lock = build_runner.files().build_unit_lock(unit).into();
38+
39+
let dependency_units = build_runner
40+
.unit_deps(unit)
41+
.into_iter()
42+
.map(|unit| build_runner.files().build_unit_lock(&unit.unit).into())
43+
.collect_vec();
44+
45+
Self {
46+
unit: unit_lock,
47+
dependency_units,
48+
}
49+
}
50+
51+
#[instrument(skip(self))]
52+
pub fn lock(&mut self) -> CargoResult<()> {
53+
self.unit.lock_exclusive()?;
54+
55+
for d in self.dependency_units.iter_mut() {
56+
d.lock_shared()?;
57+
}
58+
59+
Ok(())
60+
}
61+
}
62+
63+
/// A lock for a single build unit.
64+
struct UnitLock {
65+
lock: PathBuf,
66+
gaurd: Option<UnitLockGuard>,
67+
}
68+
69+
struct UnitLockGuard {
70+
_handle: File,
71+
}
72+
73+
impl UnitLock {
74+
pub fn lock_exclusive(&mut self) -> CargoResult<()> {
75+
assert!(self.gaurd.is_none());
76+
77+
let lock = file_lock(&self.lock)?;
78+
lock.lock()?;
79+
80+
self.gaurd = Some(UnitLockGuard { _handle: lock });
81+
Ok(())
82+
}
83+
84+
pub fn lock_shared(&mut self) -> CargoResult<()> {
85+
assert!(self.gaurd.is_none());
86+
87+
let lock = file_lock(&self.lock)?;
88+
lock.lock_shared()?;
89+
90+
self.gaurd = Some(UnitLockGuard { _handle: lock });
91+
Ok(())
92+
}
93+
}
94+
95+
impl From<PathBuf> for UnitLock {
96+
fn from(value: PathBuf) -> Self {
97+
Self {
98+
lock: value,
99+
gaurd: None,
100+
}
101+
}
102+
}
103+
104+
fn file_lock<T: AsRef<Path>>(f: T) -> CargoResult<File> {
105+
Ok(OpenOptions::new()
106+
.create(true)
107+
.write(true)
108+
.append(true)
109+
.open(f)?)
110+
}

src/cargo/core/compiler/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub mod future_incompat;
4545
pub(crate) mod job_queue;
4646
pub(crate) mod layout;
4747
mod links;
48+
mod locking;
4849
mod lto;
4950
mod output_depinfo;
5051
mod output_sbom;
@@ -95,6 +96,7 @@ use self::output_depinfo::output_depinfo;
9596
use self::output_sbom::build_sbom;
9697
use self::unit_graph::UnitDep;
9798
use crate::core::compiler::future_incompat::FutureIncompatReport;
99+
use crate::core::compiler::locking::CompilationLock;
98100
use crate::core::compiler::timings::SectionTiming;
99101
pub use crate::core::compiler::unit::{Unit, UnitInterner};
100102
use crate::core::manifest::TargetSourcePath;
@@ -351,7 +353,22 @@ fn rustc(
351353
output_options.show_diagnostics = false;
352354
}
353355
let env_config = Arc::clone(build_runner.bcx.gctx.env_config()?);
356+
357+
let mut lock = if build_runner.bcx.gctx.cli_unstable().build_dir_new_layout {
358+
Some(CompilationLock::new(build_runner, unit))
359+
} else {
360+
None
361+
};
362+
354363
return Ok(Work::new(move |state| {
364+
if let Some(lock) = &mut lock {
365+
lock.lock().expect("failed to take lock");
366+
367+
// TODO: We need to revalidate the fingerprint here as another Cargo instance could
368+
// have already compiled the crate before we recv'd the lock.
369+
// For large crates re-compiling here would be quiet costly.
370+
}
371+
355372
// Artifacts are in a different location than typical units,
356373
// hence we must assure the crate- and target-dependent
357374
// directory is present.

0 commit comments

Comments
 (0)