Skip to content

Commit b493b7d

Browse files
committed
feat(locking): Added build unit level locking
1 parent f93929d commit b493b7d

File tree

7 files changed

+427
-42
lines changed

7 files changed

+427
-42
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use lazycell::LazyCell;
1010
use tracing::debug;
1111

1212
use super::{BuildContext, BuildRunner, CompileKind, FileFlavor, Layout};
13+
use crate::core::compiler::layout::BuildUnitLockLocation;
1314
use crate::core::compiler::{CompileMode, CompileTarget, CrateType, FileType, Unit};
1415
use crate::core::{Target, TargetKind, Workspace};
1516
use crate::util::{self, CargoResult, StableHasher};
@@ -277,6 +278,13 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
277278
self.layout(unit.kind).build_dir().fingerprint(&dir)
278279
}
279280

281+
/// The path of the primary and secondary locks for a given build unit
282+
/// when fine grain locking is enabled.
283+
pub fn build_unit_lock(&self, unit: &Unit) -> BuildUnitLockLocation {
284+
let dir = self.pkg_dir(unit);
285+
self.layout(unit.kind).build_dir().build_unit_lock(&dir)
286+
}
287+
280288
/// Directory where incremental output for the given unit should go.
281289
pub fn incremental_dir(&self, unit: &Unit) -> &Path {
282290
self.layout(unit.kind).build_dir().incremental()

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::sync::{Arc, Mutex};
66

77
use crate::core::PackageId;
88
use crate::core::compiler::compilation::{self, UnitOutput};
9+
use crate::core::compiler::locking::LockingMode;
910
use crate::core::compiler::{self, Unit, artifact};
1011
use crate::util::cache_lock::CacheLockMode;
1112
use crate::util::errors::CargoResult;
@@ -89,6 +90,10 @@ pub struct BuildRunner<'a, 'gctx> {
8990
/// because the target has a type error. This is in an Arc<Mutex<..>>
9091
/// because it is continuously updated as the job progresses.
9192
pub failed_scrape_units: Arc<Mutex<HashSet<UnitHash>>>,
93+
94+
/// The locking mode to use for this build.
95+
/// We use fine grain by default, but fallback to coarse grain for some systems.
96+
pub locking_mode: LockingMode,
9297
}
9398

9499
impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
@@ -111,6 +116,12 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
111116
}
112117
};
113118

119+
let locking_mode = if bcx.gctx.cli_unstable().fine_grain_locking {
120+
LockingMode::Fine
121+
} else {
122+
LockingMode::Coarse
123+
};
124+
114125
Ok(Self {
115126
bcx,
116127
compilation: Compilation::new(bcx)?,
@@ -128,6 +139,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
128139
lto: HashMap::new(),
129140
metadata_for_doc_units: HashMap::new(),
130141
failed_scrape_units: Arc::new(Mutex::new(HashSet::new())),
142+
locking_mode,
131143
})
132144
}
133145

@@ -360,11 +372,11 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
360372
#[tracing::instrument(skip_all)]
361373
pub fn prepare_units(&mut self) -> CargoResult<()> {
362374
let dest = self.bcx.profiles.get_dir_name();
363-
let host_layout = Layout::new(self.bcx.ws, None, &dest)?;
375+
let host_layout = Layout::new(self.bcx.ws, None, &dest, &self.locking_mode)?;
364376
let mut targets = HashMap::new();
365377
for kind in self.bcx.all_kinds.iter() {
366378
if let CompileKind::Target(target) = *kind {
367-
let layout = Layout::new(self.bcx.ws, Some(target), &dest)?;
379+
let layout = Layout::new(self.bcx.ws, Some(target), &dest, &self.locking_mode)?;
368380
targets.insert(target, layout);
369381
}
370382
}

src/cargo/core/compiler/layout.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
104104
use crate::core::Workspace;
105105
use crate::core::compiler::CompileTarget;
106+
use crate::core::compiler::locking::LockingMode;
106107
use crate::util::{CargoResult, FileLock};
107108
use cargo_util::paths;
108109
use std::path::{Path, PathBuf};
@@ -126,6 +127,7 @@ impl Layout {
126127
ws: &Workspace<'_>,
127128
target: Option<CompileTarget>,
128129
dest: &str,
130+
locking_mode: &LockingMode,
129131
) -> CargoResult<Layout> {
130132
let is_new_layout = ws.gctx().cli_unstable().build_dir_new_layout;
131133
let mut root = ws.target_dir();
@@ -152,15 +154,26 @@ impl Layout {
152154
// For now we don't do any more finer-grained locking on the artifact
153155
// directory, so just lock the entire thing for the duration of this
154156
// compile.
155-
let artifact_dir_lock =
156-
dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?;
157+
let artifact_dir_lock = match locking_mode {
158+
LockingMode::Fine => {
159+
dest.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?
160+
}
161+
LockingMode::Coarse => {
162+
dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?
163+
}
164+
};
157165

158166
let build_dir_lock = if root != build_root {
159-
Some(build_dest.open_rw_exclusive_create(
160-
".cargo-lock",
161-
ws.gctx(),
162-
"build directory",
163-
)?)
167+
Some(match locking_mode {
168+
LockingMode::Fine => {
169+
build_dest.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?
170+
}
171+
LockingMode::Coarse => build_dest.open_rw_exclusive_create(
172+
".cargo-lock",
173+
ws.gctx(),
174+
"build directory",
175+
)?,
176+
})
164177
} else {
165178
None
166179
};
@@ -345,6 +358,14 @@ impl BuildDirLayout {
345358
self.build().join(pkg_dir)
346359
}
347360
}
361+
/// Fetch the lock paths for a build unit
362+
pub fn build_unit_lock(&self, pkg_dir: &str) -> BuildUnitLockLocation {
363+
let dir = self.build_unit(pkg_dir);
364+
BuildUnitLockLocation {
365+
primary: dir.join("primary.lock"),
366+
secondary: dir.join("secondary.lock"),
367+
}
368+
}
348369
/// Fetch the artifact path.
349370
pub fn artifact(&self) -> &Path {
350371
&self.artifact
@@ -359,3 +380,8 @@ impl BuildDirLayout {
359380
Ok(&self.tmp)
360381
}
361382
}
383+
384+
pub struct BuildUnitLockLocation {
385+
pub primary: PathBuf,
386+
pub secondary: PathBuf,
387+
}

src/cargo/core/compiler/locking.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//! This module handles the locking logic during compilation.
2+
//!
3+
//! The locking scheme is based on build unit level locking.
4+
//! Each build unit consists of a primary and secondary lock used to represent multiple lock states.
5+
//!
6+
//! | State | Primary | Secondary |
7+
//! |------------------------|-------------|-------------|
8+
//! | Unlocked | `unlocked` | `unlocked` |
9+
//! | Building Exclusive | `exclusive` | `exclusive` |
10+
//! | Building Non-Exclusive | `shared` | `exclusive` |
11+
//! | Shared Partial | `shared` | `unlocked` |
12+
//! | Shared Full | `shared` | `shared` |
13+
//!
14+
//! Generally a build unit will full the following flow:
15+
//! 1. Acquire a "building exclusive" lock for the current build unit.
16+
//! 2. Acquire "shared" locks on all dependency build units.
17+
//! 3. Begin building with rustc
18+
//! 4. If we are building a library, downgrade to a "building non-exclusive" lock when the `.rmeta` has been generated.
19+
//! 5. Once complete release all locks.
20+
//!
21+
//! Most build units only require metadata (.rmeta) from dependencies, so they can begin building
22+
//! once the dependency units have produced the .rmeta. These units take a "shared partial" lock
23+
//! which can be taken while the dependency still holds the "build non-exclusive" lock.
24+
//!
25+
//! Note that some build unit types like bin and proc-macros require the full dependency build
26+
//! (.rlib). For these unit types they must take a "shared full" lock on dependency units which will
27+
//! block until the dependency unit is fully built.
28+
//!
29+
//! The primary reason for the complexity here it to enable fine grain locking while also allowing pipelined builds.
30+
//!
31+
//! [`CompilationLock`] is the primary interface for locking.
32+
33+
use std::{
34+
collections::HashSet,
35+
fs::{File, OpenOptions},
36+
path::{Path, PathBuf},
37+
};
38+
39+
use anyhow::Context;
40+
use itertools::Itertools;
41+
use tracing::{instrument, trace};
42+
43+
use crate::{
44+
CargoResult,
45+
core::compiler::{BuildRunner, Unit, layout::BuildUnitLockLocation},
46+
};
47+
48+
/// The locking mode that will be used for output directories.
49+
#[derive(Debug)]
50+
pub enum LockingMode {
51+
/// Fine grain locking (Build unit level)
52+
Fine,
53+
/// Coarse grain locking (Profile level)
54+
Coarse,
55+
}
56+
57+
/// The type of lock to take when taking a shared lock.
58+
/// See the module documentation for more information about shared lock types.
59+
#[derive(Debug)]
60+
pub enum SharedLockType {
61+
/// A shared lock that might still be compiling a .rlib
62+
Partial,
63+
/// A shared lock that is guaranteed to not be compiling
64+
Full,
65+
}
66+
67+
/// A lock for compiling a build unit.
68+
///
69+
/// Internally this lock is made up of many [`UnitLock`]s for the unit and it's dependencies.
70+
pub struct CompilationLock {
71+
/// The path to the lock file of the unit to compile
72+
unit: UnitLock,
73+
/// The paths to lock files of the unit's dependencies
74+
dependency_units: Vec<UnitLock>,
75+
}
76+
77+
impl CompilationLock {
78+
pub fn new(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> Self {
79+
let unit_lock = UnitLock::new(build_runner.files().build_unit_lock(unit));
80+
81+
let dependency_units = all_dependency_units(build_runner, unit)
82+
.into_iter()
83+
.map(|unit| UnitLock::new(build_runner.files().build_unit_lock(&unit)))
84+
.collect_vec();
85+
86+
Self {
87+
unit: unit_lock,
88+
dependency_units,
89+
}
90+
}
91+
92+
#[instrument(skip(self))]
93+
pub fn lock(&mut self, ty: &SharedLockType) -> CargoResult<()> {
94+
self.unit.lock_exclusive()?;
95+
96+
for d in self.dependency_units.iter_mut() {
97+
d.lock_shared(ty)?;
98+
}
99+
100+
trace!("acquired lock: {:?}", self.unit.primary.parent());
101+
102+
Ok(())
103+
}
104+
105+
pub fn rmeta_produced(&mut self) -> CargoResult<()> {
106+
trace!("downgrading lock: {:?}", self.unit.primary.parent());
107+
108+
// Downgrade the lock on the unit we are building so that we can unblock other units to
109+
// compile. We do not need to downgrade our dependency locks since they should always be a
110+
// shared lock.
111+
self.unit.downgrade()?;
112+
113+
Ok(())
114+
}
115+
}
116+
117+
/// A lock for a single build unit.
118+
struct UnitLock {
119+
primary: PathBuf,
120+
secondary: PathBuf,
121+
guard: Option<UnitLockGuard>,
122+
}
123+
124+
struct UnitLockGuard {
125+
primary: File,
126+
_secondary: Option<File>,
127+
}
128+
129+
impl UnitLock {
130+
pub fn new(location: BuildUnitLockLocation) -> Self {
131+
Self {
132+
primary: location.primary,
133+
secondary: location.secondary,
134+
guard: None,
135+
}
136+
}
137+
138+
pub fn lock_exclusive(&mut self) -> CargoResult<()> {
139+
assert!(self.guard.is_none());
140+
141+
let primary_lock = open_file(&self.primary)?;
142+
primary_lock.lock()?;
143+
144+
let secondary_lock = open_file(&self.secondary)?;
145+
secondary_lock.lock()?;
146+
147+
self.guard = Some(UnitLockGuard {
148+
primary: primary_lock,
149+
_secondary: Some(secondary_lock),
150+
});
151+
Ok(())
152+
}
153+
154+
pub fn lock_shared(&mut self, ty: &SharedLockType) -> CargoResult<()> {
155+
assert!(self.guard.is_none());
156+
157+
let primary_lock = open_file(&self.primary)?;
158+
primary_lock.lock_shared()?;
159+
160+
let secondary_lock = if matches!(ty, SharedLockType::Full) {
161+
let secondary_lock = open_file(&self.secondary)?;
162+
secondary_lock.lock_shared()?;
163+
Some(secondary_lock)
164+
} else {
165+
None
166+
};
167+
168+
self.guard = Some(UnitLockGuard {
169+
primary: primary_lock,
170+
_secondary: secondary_lock,
171+
});
172+
Ok(())
173+
}
174+
175+
pub fn downgrade(&mut self) -> CargoResult<()> {
176+
let guard = self
177+
.guard
178+
.as_ref()
179+
.context("guard was None while calling downgrade")?;
180+
181+
// NOTE:
182+
// > Subsequent flock() calls on an already locked file will convert an existing lock to the new lock mode.
183+
// https://man7.org/linux/man-pages/man2/flock.2.html
184+
//
185+
// However, the `std::file::File::lock/lock_shared` is allowed to change this in the
186+
// future. So its probably up to us if we are okay with using this or if we want to use a
187+
// different interface to flock.
188+
guard.primary.lock_shared()?;
189+
190+
Ok(())
191+
}
192+
}
193+
194+
fn open_file<T: AsRef<Path>>(f: T) -> CargoResult<File> {
195+
Ok(OpenOptions::new()
196+
.read(true)
197+
.create(true)
198+
.write(true)
199+
.append(true)
200+
.open(f)?)
201+
}
202+
203+
fn all_dependency_units<'a>(
204+
build_runner: &'a BuildRunner<'a, '_>,
205+
unit: &Unit,
206+
) -> HashSet<&'a Unit> {
207+
fn inner<'a>(
208+
build_runner: &'a BuildRunner<'a, '_>,
209+
unit: &Unit,
210+
results: &mut HashSet<&'a Unit>,
211+
) {
212+
for dep in build_runner.unit_deps(unit) {
213+
if results.insert(&dep.unit) {
214+
inner(&build_runner, &dep.unit, results);
215+
}
216+
}
217+
}
218+
219+
let mut results = HashSet::new();
220+
inner(build_runner, unit, &mut results);
221+
return results;
222+
}

0 commit comments

Comments
 (0)