Skip to content

Commit eee8ef7

Browse files
committed
feat(locking): Added build unit level locking
1 parent 4fbe249 commit eee8ef7

9 files changed

Lines changed: 254 additions & 37 deletions

File tree

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,16 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
250250
false => "-",
251251
};
252252
let name = unit.pkg.package_id().name();
253-
let meta = self.metas[unit];
254-
let hash = meta
253+
let hash = self.unit_hash(unit);
254+
format!("{name}{separator}{hash}")
255+
}
256+
257+
/// The directory hash to use for a given unit
258+
pub fn unit_hash(&self, unit: &Unit) -> String {
259+
self.metas[unit]
255260
.pkg_dir()
256261
.map(|h| h.to_string())
257-
.unwrap_or_else(|| self.target_short_hash(unit));
258-
format!("{name}{separator}{hash}")
262+
.unwrap_or_else(|| self.target_short_hash(unit))
259263
}
260264

261265
/// Returns the final artifact path for the host (`/…/target/debug`)
@@ -296,6 +300,15 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
296300
self.layout(unit.kind).build_dir().fingerprint(&dir)
297301
}
298302

303+
/// The lock location for a given build unit.
304+
pub fn build_unit_lock(&self, unit: &Unit) -> PathBuf {
305+
let dir = self.pkg_dir(unit);
306+
self.layout(unit.kind)
307+
.build_dir()
308+
.build_unit(&dir)
309+
.join(".lock")
310+
}
311+
299312
/// Directory where incremental output for the given unit should go.
300313
pub fn incremental_dir(&self, unit: &Unit) -> &Path {
301314
self.layout(unit.kind).build_dir().incremental()

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

Lines changed: 5 additions & 0 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::LockManager;
910
use crate::core::compiler::{self, Unit, UserIntent, artifact};
1011
use crate::util::cache_lock::CacheLockMode;
1112
use crate::util::errors::CargoResult;
@@ -87,6 +88,9 @@ pub struct BuildRunner<'a, 'gctx> {
8788
/// because the target has a type error. This is in an Arc<Mutex<..>>
8889
/// because it is continuously updated as the job progresses.
8990
pub failed_scrape_units: Arc<Mutex<HashSet<UnitHash>>>,
91+
92+
/// Manages locks for build units when fine grain locking is enabled.
93+
pub lock_manager: Arc<LockManager>,
9094
}
9195

9296
impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
@@ -126,6 +130,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
126130
lto: HashMap::new(),
127131
metadata_for_doc_units: HashMap::new(),
128132
failed_scrape_units: Arc::new(Mutex::new(HashSet::new())),
133+
lock_manager: Arc::new(LockManager::new()),
129134
})
130135
}
131136

src/cargo/core/compiler/job_queue/job.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ impl Job {
8585
let prev = mem::replace(&mut self.work, Work::noop());
8686
self.work = next.then(prev);
8787
}
88+
89+
/// Chains the given work by putting it after of our own unit of work.
90+
pub fn after(&mut self, next: Work) {
91+
let prev = mem::replace(&mut self.work, Work::noop());
92+
self.work = prev.then(next);
93+
}
8894
}
8995

9096
impl fmt::Debug for Job {

src/cargo/core/compiler/job_queue/job_state.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ use std::{cell::Cell, marker, sync::Arc};
44

55
use cargo_util::ProcessBuilder;
66

7-
use crate::CargoResult;
87
use crate::core::compiler::future_incompat::FutureBreakageItem;
8+
use crate::core::compiler::locking::LockKey;
99
use crate::core::compiler::timings::SectionTiming;
1010
use crate::util::Queue;
11+
use crate::{CargoResult, core::compiler::locking::LockManager};
1112

1213
use super::{Artifact, DiagDedupe, Job, JobId, Message};
1314

@@ -47,6 +48,9 @@ pub struct JobState<'a, 'gctx> {
4748
/// sending a double message later on.
4849
rmeta_required: Cell<bool>,
4950

51+
/// Manages locks for build units when fine grain locking is enabled.
52+
lock_manager: Arc<LockManager>,
53+
5054
// Historical versions of Cargo made use of the `'a` argument here, so to
5155
// leave the door open to future refactorings keep it here.
5256
_marker: marker::PhantomData<&'a ()>,
@@ -58,12 +62,14 @@ impl<'a, 'gctx> JobState<'a, 'gctx> {
5862
messages: Arc<Queue<Message>>,
5963
output: Option<&'a DiagDedupe<'gctx>>,
6064
rmeta_required: bool,
65+
lock_manager: Arc<LockManager>,
6166
) -> Self {
6267
Self {
6368
id,
6469
messages,
6570
output,
6671
rmeta_required: Cell::new(rmeta_required),
72+
lock_manager,
6773
_marker: marker::PhantomData,
6874
}
6975
}
@@ -141,6 +147,14 @@ impl<'a, 'gctx> JobState<'a, 'gctx> {
141147
.push(Message::Finish(self.id, Artifact::Metadata, Ok(())));
142148
}
143149

150+
pub fn lock_exclusive(&self, lock: &LockKey) -> CargoResult<()> {
151+
self.lock_manager.lock(lock)
152+
}
153+
154+
pub fn downgrade_to_shared(&self, lock: &LockKey) -> CargoResult<()> {
155+
self.lock_manager.downgrade_to_shared(lock)
156+
}
157+
144158
pub fn on_section_timing_emitted(&self, section: SectionTiming) {
145159
self.messages.push(Message::SectionTiming(self.id, section));
146160
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -951,9 +951,10 @@ impl<'gctx> DrainState<'gctx> {
951951
let messages = self.messages.clone();
952952
let is_fresh = job.freshness().is_fresh();
953953
let rmeta_required = build_runner.rmeta_required(unit);
954+
let lock_manager = build_runner.lock_manager.clone();
954955

955956
let doit = move |diag_dedupe| {
956-
let state = JobState::new(id, messages, diag_dedupe, rmeta_required);
957+
let state = JobState::new(id, messages, diag_dedupe, rmeta_required, lock_manager);
957958
state.run_to_finish(job);
958959
};
959960

src/cargo/core/compiler/layout.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,19 @@ impl Layout {
160160
{
161161
None
162162
} else {
163-
Some(build_dest.open_rw_exclusive_create(
164-
".cargo-lock",
165-
ws.gctx(),
166-
"build directory",
167-
)?)
163+
if ws.gctx().cli_unstable().fine_grain_locking {
164+
Some(build_dest.open_ro_shared_create(
165+
".cargo-lock",
166+
ws.gctx(),
167+
"build directory",
168+
)?)
169+
} else {
170+
Some(build_dest.open_rw_exclusive_create(
171+
".cargo-lock",
172+
ws.gctx(),
173+
"build directory",
174+
)?)
175+
}
168176
};
169177
let build_root = build_root.into_path_unlocked();
170178
let build_dest = build_dest.as_path_unlocked();

src/cargo/core/compiler/locking.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! This module handles the locking logic during compilation.
2+
3+
use crate::{
4+
CargoResult,
5+
core::compiler::{BuildRunner, Unit},
6+
util::{FileLock, Filesystem},
7+
};
8+
use anyhow::bail;
9+
use std::{
10+
collections::HashMap,
11+
fmt::{Display, Formatter},
12+
path::PathBuf,
13+
sync::Mutex,
14+
};
15+
use tracing::instrument;
16+
17+
/// A struct to store the lock handles for build units during compilation.
18+
pub struct LockManager {
19+
locks: Mutex<HashMap<LockKey, FileLock>>,
20+
}
21+
22+
impl LockManager {
23+
pub fn new() -> Self {
24+
Self {
25+
locks: Mutex::new(HashMap::new()),
26+
}
27+
}
28+
29+
/// Takes a shared lock on a given [`Unit`]
30+
/// This prevents other Cargo instances from compiling (writing) to
31+
/// this build unit.
32+
///
33+
/// This function returns a [`LockKey`] which can be used to
34+
/// upgrade/unlock the lock.
35+
#[instrument(skip_all, fields(key))]
36+
pub fn lock_shared(
37+
&self,
38+
build_runner: &BuildRunner<'_, '_>,
39+
unit: &Unit,
40+
) -> CargoResult<LockKey> {
41+
let key = LockKey::from_unit(build_runner, unit);
42+
tracing::Span::current().record("key", key.0.to_str());
43+
44+
let mut locks = self.locks.lock().unwrap();
45+
if let Some(lock) = locks.get_mut(&key) {
46+
lock.file().lock_shared()?;
47+
} else {
48+
let fs = Filesystem::new(key.0.clone());
49+
let lock_msg = format!(
50+
"{} ({})",
51+
unit.pkg.name(),
52+
build_runner.files().unit_hash(unit)
53+
);
54+
let lock = fs.open_ro_shared_create(&key.0, build_runner.bcx.gctx, &lock_msg)?;
55+
locks.insert(key.clone(), lock);
56+
}
57+
58+
Ok(key)
59+
}
60+
61+
#[instrument(skip(self))]
62+
pub fn lock(&self, key: &LockKey) -> CargoResult<()> {
63+
let mut locks = self.locks.lock().unwrap();
64+
if let Some(lock) = locks.get_mut(&key) {
65+
lock.file().lock()?;
66+
} else {
67+
bail!("lock was not found in lock manager: {key}");
68+
}
69+
70+
Ok(())
71+
}
72+
73+
/// Upgrades an existing exclusive lock into a shared lock.
74+
#[instrument(skip(self))]
75+
pub fn downgrade_to_shared(&self, key: &LockKey) -> CargoResult<()> {
76+
let mut locks = self.locks.lock().unwrap();
77+
let Some(lock) = locks.get_mut(key) else {
78+
bail!("lock was not found in lock manager: {key}");
79+
};
80+
lock.file().lock_shared()?;
81+
Ok(())
82+
}
83+
84+
#[instrument(skip(self))]
85+
pub fn unlock(&self, key: &LockKey) -> CargoResult<()> {
86+
let mut locks = self.locks.lock().unwrap();
87+
if let Some(lock) = locks.get_mut(key) {
88+
lock.file().unlock()?;
89+
};
90+
91+
Ok(())
92+
}
93+
}
94+
95+
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
96+
pub struct LockKey(PathBuf);
97+
98+
impl LockKey {
99+
fn from_unit(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> Self {
100+
Self(build_runner.files().build_unit_lock(unit))
101+
}
102+
}
103+
104+
impl Display for LockKey {
105+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
106+
write!(f, "{}", self.0.display())
107+
}
108+
}

src/cargo/core/compiler/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub mod future_incompat;
4141
pub(crate) mod job_queue;
4242
pub(crate) mod layout;
4343
mod links;
44+
mod locking;
4445
mod lto;
4546
mod output_depinfo;
4647
mod output_sbom;
@@ -94,6 +95,7 @@ use self::output_sbom::build_sbom;
9495
use self::unit_graph::UnitDep;
9596

9697
use crate::core::compiler::future_incompat::FutureIncompatReport;
98+
use crate::core::compiler::locking::LockKey;
9799
use crate::core::compiler::timings::SectionTiming;
98100
pub use crate::core::compiler::unit::{Unit, UnitInterner};
99101
use crate::core::manifest::TargetSourcePath;
@@ -187,6 +189,12 @@ fn compile<'gctx>(
187189
return Ok(());
188190
}
189191

192+
let lock = if build_runner.bcx.gctx.cli_unstable().fine_grain_locking {
193+
Some(build_runner.lock_manager.lock_shared(build_runner, unit)?)
194+
} else {
195+
None
196+
};
197+
190198
// If we are in `--compile-time-deps` and the given unit is not a compile time
191199
// dependency, skip compiling the unit and jumps to dependencies, which still
192200
// have chances to be compile time dependencies
@@ -228,6 +236,23 @@ fn compile<'gctx>(
228236
work.then(link_targets(build_runner, unit, true)?)
229237
});
230238

239+
// If -Zfine-grain-locking is enabled, we wrap the job with an upgrade to exclusive
240+
// lock before starting, then downgrade to a shared lock after the job is finished.
241+
if build_runner.bcx.gctx.cli_unstable().fine_grain_locking && job.freshness().is_dirty()
242+
{
243+
if let Some(lock) = lock {
244+
// Here we unlock the current shared lock to avoid deadlocking with other cargo
245+
// processes. Then we configure our compile job to take an exclusive lock
246+
// before starting. Once we are done compiling (including both rmeta and rlib)
247+
// we downgrade to a shared lock to allow other cargo's to read the build unit.
248+
// We will hold this shared lock for the remainder of compilation to prevent
249+
// other cargo from re-compiling while we are still using the unit.
250+
build_runner.lock_manager.unlock(&lock)?;
251+
job.before(prebuild_lock_exclusive(lock.clone()));
252+
job.after(downgrade_lock_to_shared(lock));
253+
}
254+
}
255+
231256
job
232257
};
233258
jobs.enqueue(build_runner, unit, job)?;
@@ -589,6 +614,20 @@ fn verbose_if_simple_exit_code(err: Error) -> Error {
589614
}
590615
}
591616

617+
fn prebuild_lock_exclusive(lock: LockKey) -> Work {
618+
Work::new(move |state| {
619+
state.lock_exclusive(&lock)?;
620+
Ok(())
621+
})
622+
}
623+
624+
fn downgrade_lock_to_shared(lock: LockKey) -> Work {
625+
Work::new(move |state| {
626+
state.downgrade_to_shared(&lock)?;
627+
Ok(())
628+
})
629+
}
630+
592631
/// Link the compiled target (often of form `foo-{metadata_hash}`) to the
593632
/// final target. This must happen during both "Fresh" and "Compile".
594633
fn link_targets(

0 commit comments

Comments
 (0)