Skip to content

Commit 5217280

Browse files
committed
Make registry locking more coarse
This commit updates the locking strategy in Cargo handle the recent addition of creating a cache of the on-disk index also on disk. The goal here is reduce the overhead of locking both cognitively when reading but also performance wise by requiring fewer locks. Previously Cargo had a bunch of fine-grained locks throughout the index and git repositories, but after this commit there's just one global "package cache" lock. This global lock now serves to basically synchronize the entire crate graph resolution step. This shouldn't really take that long unless it's downloading, in which case there's not a ton of benefit to running in parallel anyway. The other intention of this single global lock is to make it much easier on the sources to not worry so much about lock ordering or when to acquire locks, but rather they just assert in their various operations that they're locked. Cargo now has a few coarse-grained locations where locks are held (for example during resolution and during package downloading). These locks are a bit sprinkled about but they have in-code asserts which assert that they're held, so we'll find bugs quickly if any lock isn't held (before a race condition is hit that is)
1 parent 6babe72 commit 5217280

File tree

15 files changed

+189
-139
lines changed

15 files changed

+189
-139
lines changed

src/cargo/core/package.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::ops;
2525
use crate::util::errors::{CargoResult, CargoResultExt, HttpNot200};
2626
use crate::util::network::Retry;
2727
use crate::util::{self, internal, lev_distance, Config, Progress, ProgressStyle};
28+
use crate::util::config::PackageCacheLock;
2829

2930
/// Information about a package that is available somewhere in the file system.
3031
///
@@ -339,6 +340,9 @@ pub struct Downloads<'a, 'cfg: 'a> {
339340
/// trigger a timeout; reset `next_speed_check` and set this back to the
340341
/// configured threshold.
341342
next_speed_check_bytes_threshold: Cell<u64>,
343+
/// Global filesystem lock to ensure only one Cargo is downloading at a
344+
/// time.
345+
_lock: PackageCacheLock<'cfg>,
342346
}
343347

344348
struct Download<'cfg> {
@@ -437,6 +441,7 @@ impl<'cfg> PackageSet<'cfg> {
437441
timeout,
438442
next_speed_check: Cell::new(Instant::now()),
439443
next_speed_check_bytes_threshold: Cell::new(0),
444+
_lock: self.config.acquire_package_cache_lock()?,
440445
})
441446
}
442447

src/cargo/ops/cargo_generate_lockfile.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
4040
failure::bail!("you can't update in the offline mode");
4141
}
4242

43+
// Updates often require a lot of modifications to the registry, so ensure
44+
// that we're synchronized against other Cargos.
45+
let _lock = ws.config().acquire_package_cache_lock()?;
46+
4347
let previous_resolve = match ops::load_pkg_lockfile(ws)? {
4448
Some(resolve) => resolve,
4549
None => return generate_lockfile(ws),
@@ -73,6 +77,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
7377
});
7478
}
7579
}
80+
7681
registry.add_sources(sources)?;
7782
}
7883

src/cargo/ops/cargo_install.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,11 @@ fn check_yanked_install(ws: &Workspace<'_>) -> CargoResult<()> {
488488
// wouldn't be available for `compile_ws`.
489489
let (pkg_set, resolve) = ops::resolve_ws_with_method(ws, Method::Everything, &specs)?;
490490
let mut sources = pkg_set.sources_mut();
491+
492+
// Checking the yanked status invovles taking a look at the registry and
493+
// maybe updating files, so be sure to lock it here.
494+
let _lock = ws.config().acquire_package_cache_lock()?;
495+
491496
for pkg_id in resolve.iter() {
492497
if let Some(source) = sources.get_mut(pkg_id.source_id()) {
493498
if source.is_yanked(pkg_id)? {

src/cargo/ops/cargo_package.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,10 @@ fn compare_resolve(
548548
}
549549

550550
fn check_yanked(config: &Config, pkg_set: &PackageSet<'_>, resolve: &Resolve) -> CargoResult<()> {
551+
// Checking the yanked status invovles taking a look at the registry and
552+
// maybe updating files, so be sure to lock it here.
553+
let _lock = config.acquire_package_cache_lock()?;
554+
551555
let mut sources = pkg_set.sources_mut();
552556
for pkg_id in resolve.iter() {
553557
if let Some(source) = sources.get_mut(pkg_id.source_id()) {

src/cargo/ops/common_for_install_and_uninstall.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,11 @@ pub fn select_pkg<'a, T>(
564564
where
565565
T: Source + 'a,
566566
{
567+
// This operation may involve updating some sources or making a few queries
568+
// which may involve frobbing caches, as a result make sure we synchronize
569+
// with other global Cargos
570+
let _lock = config.acquire_package_cache_lock()?;
571+
567572
if needs_update {
568573
source.update()?;
569574
}

src/cargo/ops/registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ fn registry(
353353
let token = token.or(token_config);
354354
let sid = get_source_id(config, index_config.or(index), registry)?;
355355
let api_host = {
356+
let _lock = config.acquire_package_cache_lock()?;
356357
let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
357358
// Only update the index if the config is not available or `force` is set.
358359
let cfg = src.config();

src/cargo/ops/resolve.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ pub fn resolve_with_previous<'cfg>(
146146
specs: &[PackageIdSpec],
147147
register_patches: bool,
148148
) -> CargoResult<Resolve> {
149+
// We only want one Cargo at a time resolving a crate graph since this can
150+
// involve a lot of frobbing of the global caches.
151+
let _lock = ws.config().acquire_package_cache_lock()?;
152+
149153
// Here we place an artificial limitation that all non-registry sources
150154
// cannot be locked at more than one revision. This means that if a Git
151155
// repository provides more than one package, they must all be updated in

src/cargo/sources/git/source.rs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,9 @@ impl<'cfg> Source for GitSource<'cfg> {
154154
}
155155

156156
fn update(&mut self) -> CargoResult<()> {
157-
let lock =
158-
self.config
159-
.git_path()
160-
.open_rw(".cargo-lock-git", self.config, "the git checkouts")?;
161-
162-
let db_path = lock.parent().join("db").join(&self.ident);
157+
let git_path = self.config.git_path();
158+
let git_path = self.config.assert_package_cache_locked(&git_path);
159+
let db_path = git_path.join("db").join(&self.ident);
163160

164161
if self.config.cli_unstable().offline && !db_path.exists() {
165162
failure::bail!(
@@ -189,21 +186,17 @@ impl<'cfg> Source for GitSource<'cfg> {
189186
(self.remote.db_at(&db_path)?, actual_rev.unwrap())
190187
};
191188

192-
// Don’t use the full hash, in order to contribute less to reaching the path length limit
193-
// on Windows. See <https://github.com/servo/servo/pull/14397>.
189+
// Don’t use the full hash, in order to contribute less to reaching the
190+
// path length limit on Windows. See
191+
// <https://github.com/servo/servo/pull/14397>.
194192
let short_id = db.to_short_id(&actual_rev).unwrap();
195193

196-
let checkout_path = lock
197-
.parent()
194+
let checkout_path = git_path
198195
.join("checkouts")
199196
.join(&self.ident)
200197
.join(short_id.as_str());
201198

202-
// Copy the database to the checkout location. After this we could drop
203-
// the lock on the database as we no longer needed it, but we leave it
204-
// in scope so the destructors here won't tamper with too much.
205-
// Checkout is immutable, so we don't need to protect it with a lock once
206-
// it is created.
199+
// Copy the database to the checkout location.
207200
db.copy_to(actual_rev.clone(), &checkout_path, self.config)?;
208201

209202
let source_id = self.source_id.with_precise(Some(actual_rev.to_string()));

src/cargo/sources/registry/index.rs

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ use semver::{Version, VersionReq};
7878

7979
use crate::core::dependency::Dependency;
8080
use crate::core::{InternedString, PackageId, SourceId, Summary};
81-
use crate::sources::registry::RegistryData;
82-
use crate::sources::registry::{RegistryPackage, INDEX_LOCK};
81+
use crate::sources::registry::{RegistryData, RegistryPackage};
8382
use crate::util::{internal, CargoResult, Config, Filesystem, ToSemver};
8483

8584
/// Crates.io treats hyphen and underscores as interchangeable, but the index and old Cargo do not.
@@ -172,7 +171,6 @@ pub struct RegistryIndex<'cfg> {
172171
path: Filesystem,
173172
summaries_cache: HashMap<InternedString, Summaries>,
174173
config: &'cfg Config,
175-
locked: bool,
176174
}
177175

178176
/// An internal cache of summaries for a particular package.
@@ -237,14 +235,12 @@ impl<'cfg> RegistryIndex<'cfg> {
237235
source_id: SourceId,
238236
path: &Filesystem,
239237
config: &'cfg Config,
240-
locked: bool,
241238
) -> RegistryIndex<'cfg> {
242239
RegistryIndex {
243240
source_id,
244241
path: path.clone(),
245242
summaries_cache: HashMap::new(),
246243
config,
247-
locked,
248244
}
249245
}
250246

@@ -314,35 +310,19 @@ impl<'cfg> RegistryIndex<'cfg> {
314310
}
315311

316312
// Prepare the `RegistryData` which will lazily initialize internal data
317-
// structures. Note that this is also importantly needed to initialize
318-
// to avoid deadlocks where we acquire a lock below but the `load`
319-
// function inside *also* wants to acquire a lock. See an instance of
320-
// this on #5551.
313+
// structures.
321314
load.prepare()?;
322315

323-
// Synchronize access to the index. For remote indices we want to make
324-
// sure that while we're reading the index no one is trying to update
325-
// it.
326-
let (root, lock) = if self.locked {
327-
let lock = self
328-
.path
329-
.open_ro(Path::new(INDEX_LOCK), self.config, "the registry index");
330-
match lock {
331-
Ok(lock) => (lock.path().parent().unwrap().to_path_buf(), Some(lock)),
332-
Err(_) => {
333-
self.summaries_cache.insert(name, Summaries::default());
334-
return Ok(self.summaries_cache.get_mut(&name).unwrap());
335-
}
336-
}
337-
} else {
338-
(self.path.clone().into_path_unlocked(), None)
339-
};
316+
// let root = self.config.assert_package_cache_locked(&self.path);
317+
let root = load.assert_index_locked(&self.path);
340318
let cache_root = root.join(".cache");
319+
341320
// TODO: comment
342-
let lock_mtime = lock
343-
.as_ref()
344-
.and_then(|l| l.file().metadata().ok())
345-
.map(|t| FileTime::from_last_modification_time(&t));
321+
let lock_mtime = None;
322+
// let lock_mtime = lock
323+
// .as_ref()
324+
// .and_then(|l| l.file().metadata().ok())
325+
// .map(|t| FileTime::from_last_modification_time(&t));
346326

347327

348328
// See module comment in `registry/mod.rs` for why this is structured
@@ -371,6 +351,7 @@ impl<'cfg> RegistryIndex<'cfg> {
371351
path.as_ref(),
372352
self.source_id,
373353
load,
354+
self.config,
374355
)?;
375356
if let Some(summaries) = summaries {
376357
self.summaries_cache.insert(name, summaries);
@@ -503,6 +484,7 @@ impl Summaries {
503484
relative: &Path,
504485
source_id: SourceId,
505486
load: &mut dyn RegistryData,
487+
config: &Config,
506488
) -> CargoResult<Option<Summaries>> {
507489
// First up, attempt to load the cache. This could fail for all manner
508490
// of reasons, but consider all of them non-fatal and just log their
@@ -579,7 +561,8 @@ impl Summaries {
579561
// This is opportunistic so we ignore failure here but are sure to log
580562
// something in case of error.
581563
if fs::create_dir_all(cache_path.parent().unwrap()).is_ok() {
582-
// TODO: somehow need to globally synchronize this
564+
let path = Filesystem::new(cache_path.clone());
565+
config.assert_package_cache_locked(&path);
583566
if let Err(e) = fs::write(cache_path, cache_bytes) {
584567
log::info!("failed to write cache: {}", e);
585568
}

src/cargo/sources/registry/local.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use std::io::prelude::*;
1+
use std::fs::File;
22
use std::io::SeekFrom;
3+
use std::io::prelude::*;
34
use std::path::Path;
45

56
use crate::core::PackageId;
67
use crate::sources::registry::{MaybeLock, RegistryConfig, RegistryData};
78
use crate::util::errors::{CargoResult, CargoResultExt};
89
use crate::util::paths;
9-
use crate::util::{Config, FileLock, Filesystem, Sha256};
10+
use crate::util::{Config, Filesystem, Sha256};
1011
use hex;
1112

1213
pub struct LocalRegistry<'cfg> {
@@ -36,6 +37,12 @@ impl<'cfg> RegistryData for LocalRegistry<'cfg> {
3637
&self.index_path
3738
}
3839

40+
fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
41+
// Note that the `*_unlocked` variant is used here since we're not
42+
// modifying the index and it's required to be externally synchronized.
43+
path.as_path_unlocked()
44+
}
45+
3946
fn load(
4047
&self,
4148
root: &Path,
@@ -71,7 +78,12 @@ impl<'cfg> RegistryData for LocalRegistry<'cfg> {
7178

7279
fn download(&mut self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
7380
let crate_file = format!("{}-{}.crate", pkg.name(), pkg.version());
74-
let mut crate_file = self.root.open_ro(&crate_file, self.config, "crate file")?;
81+
82+
// Note that the usage of `into_path_unlocked` here is because the local
83+
// crate files here never change in that we're not the one writing them,
84+
// so it's not our responsibility to synchronize access to them.
85+
let path = self.root.join(&crate_file).into_path_unlocked();
86+
let mut crate_file = File::open(&path)?;
7587

7688
// If we've already got an unpacked version of this crate, then skip the
7789
// checksum below as it is in theory already verified.
@@ -89,7 +101,7 @@ impl<'cfg> RegistryData for LocalRegistry<'cfg> {
89101
loop {
90102
let n = crate_file
91103
.read(&mut buf)
92-
.chain_err(|| format!("failed to read `{}`", crate_file.path().display()))?;
104+
.chain_err(|| format!("failed to read `{}`", path.display()))?;
93105
if n == 0 {
94106
break;
95107
}
@@ -109,7 +121,7 @@ impl<'cfg> RegistryData for LocalRegistry<'cfg> {
109121
_pkg: PackageId,
110122
_checksum: &str,
111123
_data: &[u8],
112-
) -> CargoResult<FileLock> {
124+
) -> CargoResult<File> {
113125
panic!("this source doesn't download")
114126
}
115127
}

0 commit comments

Comments
 (0)