Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/cargo/core/compiler/build_runner/compilation_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::sync::Arc;
use tracing::debug;

use super::{BuildContext, BuildRunner, CompileKind, FileFlavor, Layout};
use crate::core::compiler::layout::BuildUnitLockLocation;
use crate::core::compiler::{CompileMode, CompileTarget, CrateType, FileType, Unit};
use crate::core::{Target, TargetKind, Workspace};
use crate::util::{self, CargoResult, OnceExt, StableHasher};
Expand Down Expand Up @@ -281,6 +282,13 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
self.layout(unit.kind).build_dir().fingerprint(&dir)
}

/// The path of the partial and full locks for a given build unit
/// when fine grain locking is enabled.
pub fn build_unit_lock(&self, unit: &Unit) -> BuildUnitLockLocation {
let dir = self.pkg_dir(unit);
self.layout(unit.kind).build_dir().build_unit_lock(&dir)
}

/// Directory where incremental output for the given unit should go.
pub fn incremental_dir(&self, unit: &Unit) -> &Path {
self.layout(unit.kind).build_dir().incremental()
Expand Down
21 changes: 20 additions & 1 deletion src/cargo/core/compiler/build_runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::{Arc, Mutex};

use crate::core::PackageId;
use crate::core::compiler::compilation::{self, UnitOutput};
use crate::core::compiler::locking::LockingMode;
use crate::core::compiler::{self, Unit, UserIntent, artifact};
use crate::util::cache_lock::CacheLockMode;
use crate::util::errors::CargoResult;
Expand Down Expand Up @@ -88,6 +89,10 @@ pub struct BuildRunner<'a, 'gctx> {
/// because the target has a type error. This is in an Arc<Mutex<..>>
/// because it is continuously updated as the job progresses.
pub failed_scrape_units: Arc<Mutex<HashSet<UnitHash>>>,

/// The locking mode to use for this build.
/// We use fine grain by default, but fallback to coarse grain for some systems.
pub locking_mode: LockingMode,
}

impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
Expand All @@ -110,6 +115,12 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
}
};

let locking_mode = if bcx.gctx.cli_unstable().fine_grain_locking {
LockingMode::Fine
} else {
LockingMode::Coarse
};

Ok(Self {
bcx,
compilation: Compilation::new(bcx)?,
Expand All @@ -127,6 +138,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
lto: HashMap::new(),
metadata_for_doc_units: HashMap::new(),
failed_scrape_units: Arc::new(Mutex::new(HashSet::new())),
locking_mode,
})
}

Expand Down Expand Up @@ -363,7 +375,13 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
| UserIntent::Doctest
| UserIntent::Bench => true,
};
let host_layout = Layout::new(self.bcx.ws, None, &dest, must_take_artifact_dir_lock)?;
let host_layout = Layout::new(
self.bcx.ws,
None,
&dest,
must_take_artifact_dir_lock,
&self.locking_mode,
)?;
let mut targets = HashMap::new();
for kind in self.bcx.all_kinds.iter() {
if let CompileKind::Target(target) = *kind {
Expand All @@ -372,6 +390,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
Some(target),
&dest,
must_take_artifact_dir_lock,
&self.locking_mode,
)?;
targets.insert(target, layout);
}
Expand Down
34 changes: 29 additions & 5 deletions src/cargo/core/compiler/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@

use crate::core::Workspace;
use crate::core::compiler::CompileTarget;
use crate::core::compiler::locking::LockingMode;
use crate::util::flock::is_on_nfs_mount;
use crate::util::{CargoResult, FileLock};
use cargo_util::paths;
Expand All @@ -128,6 +129,7 @@ impl Layout {
target: Option<CompileTarget>,
dest: &str,
must_take_artifact_dir_lock: bool,
build_dir_locking_mode: &LockingMode,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we take a reference to something that could be made Copy?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no reason, I'll make it Copy :D

) -> CargoResult<Layout> {
let is_new_layout = ws.gctx().cli_unstable().build_dir_new_layout;
let mut root = ws.target_dir();
Expand Down Expand Up @@ -155,11 +157,18 @@ impl Layout {
{
None
} else {
Some(build_dest.open_rw_exclusive_create(
".cargo-lock",
ws.gctx(),
"build directory",
)?)
match build_dir_locking_mode {
LockingMode::Fine => Some(build_dest.open_ro_shared_create(
".cargo-lock",
ws.gctx(),
"build directory",
)?),
LockingMode::Coarse => Some(build_dest.open_rw_exclusive_create(
".cargo-lock",
ws.gctx(),
"build directory",
)?),
}
};
let build_root = build_root.into_path_unlocked();
let build_dest = build_dest.as_path_unlocked();
Expand Down Expand Up @@ -361,6 +370,14 @@ impl BuildDirLayout {
self.build().join(pkg_dir)
}
}
/// Fetch the lock paths for a build unit
pub fn build_unit_lock(&self, pkg_dir: &str) -> BuildUnitLockLocation {
let dir = self.build_unit(pkg_dir);
BuildUnitLockLocation {
partial: dir.join("partial.lock"),
full: dir.join("full.lock"),
}
}
/// Fetch the artifact path.
pub fn artifact(&self) -> &Path {
&self.artifact
Expand All @@ -375,3 +392,10 @@ impl BuildDirLayout {
Ok(&self.tmp)
}
}

/// See [crate::core::compiler::locking] module docs for details about build system locking
/// structure.
pub struct BuildUnitLockLocation {
pub partial: PathBuf,
pub full: PathBuf,
}
222 changes: 222 additions & 0 deletions src/cargo/core/compiler/locking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! This module handles the locking logic during compilation.
//!
//! The locking scheme is based on build unit level locking.
//! Each build unit consists of a partial and full lock used to represent multiple lock states.
//!
//! | State | `partial.lock` | `full.lock` |
//! |------------------------|----------------|--------------|
//! | Unlocked | `unlocked` | `unlocked` |
//! | Building Exclusive | `exclusive` | `exclusive` |
//! | Building Non-Exclusive | `shared` | `exclusive` |
//! | Shared Partial | `shared` | `unlocked` |
//! | Shared Full | `shared` | `shared` |
//!
//! Generally a build unit will full the following flow:
//! 1. Acquire a "building exclusive" lock for the current build unit.
//! 2. Acquire "shared" locks on all dependency build units.
//! 3. Begin building with rustc
//! 4. If we are building a library, downgrade to a "building non-exclusive" lock when the `.rmeta` has been generated.
//! 5. Once complete release all locks.
//!
//! Most build units only require metadata (.rmeta) from dependencies, so they can begin building
//! once the dependency units have produced the .rmeta. These units take a "shared partial" lock
//! which can be taken while the dependency still holds the "build non-exclusive" lock.
//!
//! Note that some build unit types like bin and proc-macros require the full dependency build
//! (.rlib). For these unit types they must take a "shared full" lock on dependency units which will
//! block until the dependency unit is fully built.
//!
//! The primary reason for the complexity here it to enable fine grain locking while also allowing pipelined builds.
//!
//! [`CompilationLock`] is the primary interface for locking.
use std::{
collections::HashSet,
fs::{File, OpenOptions},
path::{Path, PathBuf},
};

use anyhow::Context;
use itertools::Itertools;
use tracing::{instrument, trace};

use crate::{
CargoResult,
core::compiler::{BuildRunner, Unit, layout::BuildUnitLockLocation},
};

/// The locking mode that will be used for build-dir.
#[derive(Debug)]
pub enum LockingMode {
/// Fine grain locking (Build unit level)
Fine,
/// Coarse grain locking (Profile level)
Coarse,
Comment on lines +54 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mtime changes in fingerprints due to uplifting/downlifting order

This will also be a concern for #5931

For non-local packages, we don't check mtimes. Unsure what we do for their build script runs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't quiet thought through how we will handle mtime for the artifact cache.

Checksum freshness (#14136) would sure make this easier as mtimes are painful to deal with.
Might be worth exploring pushing that forward before starting on the artifact cache...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that mtimes are still used for build scripts

Comment on lines +54 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests/benches need to be ran before being uplifted OR uplifted and locked during execution which leads to more locking design needed. (also running pre-uplift introduces other potential side effects like the path displayed to the user being deleted as its temporary)

Do we hold any locks during test execution today? I'm not aware of any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, I was under the assumption that the lock in Layout was held while running the tests, but I never explicitly tested that. Upon further inspection, we do indeed release the lock before executing the tests.

}

/// The type of lock to take when taking a shared lock.
/// See the module documentation for more information about shared lock types.
#[derive(Debug)]
pub enum SharedLockType {
/// A shared lock that might still be compiling a .rlib
Partial,
/// A shared lock that is guaranteed to not be compiling
Full,
}

/// A lock for compiling a build unit.
///
/// Internally this lock is made up of many [`UnitLock`]s for the unit and it's dependencies.
pub struct CompilationLock {
/// The path to the lock file of the unit to compile
unit: UnitLock,
/// The paths to lock files of the unit's dependencies
dependency_units: Vec<UnitLock>,
}

impl CompilationLock {
pub fn new(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> Self {
let unit_lock = UnitLock::new(build_runner.files().build_unit_lock(unit));

let dependency_units = all_dependency_units(build_runner, unit)
.into_iter()
.map(|unit| UnitLock::new(build_runner.files().build_unit_lock(&unit)))
.collect_vec();

Self {
unit: unit_lock,
dependency_units,
}
}

#[instrument(skip(self))]
pub fn lock(&mut self, ty: &SharedLockType) -> CargoResult<()> {
self.unit.lock_exclusive()?;

for d in self.dependency_units.iter_mut() {
d.lock_shared(ty)?;
}

trace!("acquired lock: {:?}", self.unit.partial.parent());

Ok(())
}

pub fn rmeta_produced(&mut self) -> CargoResult<()> {
trace!("downgrading lock: {:?}", self.unit.partial.parent());

// Downgrade the lock on the unit we are building so that we can unblock other units to
// compile. We do not need to downgrade our dependency locks since they should always be a
// shared lock.
self.unit.downgrade()?;

Ok(())
}
}

/// A lock for a single build unit.
struct UnitLock {
partial: PathBuf,
full: PathBuf,
guard: Option<UnitLockGuard>,
}

struct UnitLockGuard {
partial: File,
_full: Option<File>,
}

impl UnitLock {
pub fn new(location: BuildUnitLockLocation) -> Self {
Self {
partial: location.partial,
full: location.full,
guard: None,
}
}

pub fn lock_exclusive(&mut self) -> CargoResult<()> {
assert!(self.guard.is_none());

let partial = open_file(&self.partial)?;
partial.lock()?;

let full = open_file(&self.full)?;
full.lock()?;

self.guard = Some(UnitLockGuard {
partial,
_full: Some(full),
});
Ok(())
}

pub fn lock_shared(&mut self, ty: &SharedLockType) -> CargoResult<()> {
assert!(self.guard.is_none());

let partial = open_file(&self.partial)?;
partial.lock_shared()?;

let full = if matches!(ty, SharedLockType::Full) {
let full_lock = open_file(&self.full)?;
full_lock.lock_shared()?;
Some(full_lock)
} else {
None
};

self.guard = Some(UnitLockGuard {
partial,
_full: full,
});
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike Filessytems locking, this doesn't provide a way to find out what you are blocked on or that we are even blocked.

I doubt we can send a Blocking message to the user within our current progress system though that is something i want to eventually redesign.

Can we at least log a message saying what is being blocked?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I can add logging.

I also looked into providing feedback but:

  1. Was a bit tricky as gctx is not available in the Work units that are executed by the job queue, which I believe is the primary interface to shell output
  2. I wasn't quiet sure what the best way to present this info to the user. I was worried about potentially flooding the screen with messages as units are unlocked and new units get blocked.


pub fn downgrade(&mut self) -> CargoResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we clarify what this means? maybe downgrade_partial?

let guard = self
.guard
.as_ref()
.context("guard was None while calling downgrade")?;

// NOTE:
// > Subsequent flock() calls on an already locked file will convert an existing lock to the new lock mode.
// https://man7.org/linux/man-pages/man2/flock.2.html
//
// However, the `std::file::File::lock/lock_shared` is allowed to change this in the
// future. So its probably up to us if we are okay with using this or if we want to use a
// different interface to flock.
guard.partial.lock_shared()?;

Ok(())
}
}

fn open_file<T: AsRef<Path>>(f: T) -> CargoResult<File> {
Ok(OpenOptions::new()
.read(true)
.create(true)
.write(true)
.append(true)
.open(f)?)
}

fn all_dependency_units<'a>(
build_runner: &'a BuildRunner<'a, '_>,
unit: &Unit,
) -> HashSet<&'a Unit> {
fn inner<'a>(
build_runner: &'a BuildRunner<'a, '_>,
unit: &Unit,
results: &mut HashSet<&'a Unit>,
) {
for dep in build_runner.unit_deps(unit) {
if results.insert(&dep.unit) {
inner(&build_runner, &dep.unit, results);
}
}
}

let mut results = HashSet::new();
inner(build_runner, unit, &mut results);
return results;
}
Loading