Skip to content

Commit adc1228

Browse files
committed
feat(locking): Added reference counted file locking
1 parent 04f8610 commit adc1228

File tree

1 file changed

+170
-19
lines changed

1 file changed

+170
-19
lines changed

src/cargo/core/compiler/locking.rs

Lines changed: 170 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@
3131
//! [`CompilationLock`] is the primary interface for locking.
3232
3333
use std::{
34-
collections::HashSet,
34+
collections::{HashMap, HashSet},
3535
fs::{File, OpenOptions},
3636
path::{Path, PathBuf},
37+
sync::{Arc, Condvar, LazyLock, Mutex},
3738
};
3839

39-
use anyhow::Context;
40+
use anyhow::{Context, anyhow};
4041
use itertools::Itertools;
4142
use tracing::{instrument, trace};
4243

@@ -122,35 +123,44 @@ struct UnitLock {
122123
}
123124

124125
struct UnitLockGuard {
125-
primary: File,
126-
_secondary: Option<File>,
126+
primary: Arc<RcFileLock>,
127+
secondary: Option<Arc<RcFileLock>>,
128+
}
129+
130+
impl Drop for UnitLockGuard {
131+
fn drop(&mut self) {
132+
self.primary.unlock().unwrap();
133+
if let Some(secondary) = &self.secondary {
134+
secondary.unlock().unwrap();
135+
}
136+
}
127137
}
128138

129139
impl UnitLock {
130140
pub fn lock_exclusive(&mut self) -> CargoResult<()> {
131141
assert!(self.guard.is_none());
132142

133-
let primary_lock = open_file(&self.primary)?;
143+
let primary_lock = FileLockInterner::get_or_create_lock(&self.primary)?;
134144
primary_lock.lock()?;
135145

136-
let secondary_lock = open_file(&self.secondary)?;
146+
let secondary_lock = FileLockInterner::get_or_create_lock(&self.secondary)?;
137147
secondary_lock.lock()?;
138148

139149
self.guard = Some(UnitLockGuard {
140150
primary: primary_lock,
141-
_secondary: Some(secondary_lock),
151+
secondary: Some(secondary_lock),
142152
});
143153
Ok(())
144154
}
145155

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

149-
let primary_lock = open_file(&self.primary)?;
159+
let primary_lock = FileLockInterner::get_or_create_lock(&self.primary)?;
150160
primary_lock.lock_shared()?;
151161

152162
let secondary_lock = if matches!(ty, SharedLockType::Full) {
153-
let secondary_lock = open_file(&self.secondary)?;
163+
let secondary_lock = FileLockInterner::get_or_create_lock(&self.secondary)?;
154164
secondary_lock.lock_shared()?;
155165
Some(secondary_lock)
156166
} else {
@@ -159,7 +169,7 @@ impl UnitLock {
159169

160170
self.guard = Some(UnitLockGuard {
161171
primary: primary_lock,
162-
_secondary: secondary_lock,
172+
secondary: secondary_lock,
163173
});
164174
Ok(())
165175
}
@@ -169,15 +179,7 @@ impl UnitLock {
169179
.guard
170180
.as_ref()
171181
.context("guard was None while calling downgrade")?;
172-
173-
// NOTE:
174-
// > Subsequent flock() calls on an already locked file will convert an existing lock to the new lock mode.
175-
// https://man7.org/linux/man-pages/man2/flock.2.html
176-
//
177-
// However, the `std::file::File::lock/lock_shared` is allowed to change this in the
178-
// future. So its probably up to us if we are okay with using this or if we want to use a
179-
// different interface to flock.
180-
guard.primary.lock_shared()?;
182+
guard.primary.downgrade()?;
181183

182184
Ok(())
183185
}
@@ -222,3 +224,152 @@ fn all_dependency_units<'a>(
222224
inner(build_runner, unit, &mut results);
223225
return results;
224226
}
227+
228+
/// An interner to manage [`RcFileLock`]s to make sharing across compilation jobs easier.
229+
pub struct FileLockInterner {
230+
locks: Mutex<HashMap<PathBuf, Arc<RcFileLock>>>,
231+
}
232+
233+
impl FileLockInterner {
234+
pub fn new() -> Self {
235+
Self {
236+
locks: Mutex::new(HashMap::new()),
237+
}
238+
}
239+
240+
pub fn get_or_create_lock(path: &Path) -> CargoResult<Arc<RcFileLock>> {
241+
static GLOBAL: LazyLock<FileLockInterner> = LazyLock::new(FileLockInterner::new);
242+
243+
let mut locks = GLOBAL
244+
.locks
245+
.lock()
246+
.map_err(|_| anyhow!("lock was poisoned"))?;
247+
248+
if let Some(lock) = locks.get(path) {
249+
return Ok(Arc::clone(lock));
250+
}
251+
252+
let file = open_file(&path)?;
253+
254+
let lock = Arc::new(RcFileLock {
255+
inner: Mutex::new(RcFileLockInner {
256+
file,
257+
share_count: 0,
258+
exclusive: false,
259+
}),
260+
condvar: Condvar::new(),
261+
});
262+
263+
locks.insert(path.to_path_buf(), Arc::clone(&lock));
264+
265+
return Ok(lock);
266+
}
267+
}
268+
269+
/// A reference counted file lock.
270+
///
271+
/// This lock is designed to reduce file descriptors by sharing a single file descriptor for a
272+
/// given lock when the lock is shared. The motivation for this is to avoid hitting file descriptor
273+
/// limits when fine grain locking is enabled.
274+
pub struct RcFileLock {
275+
inner: Mutex<RcFileLockInner>,
276+
condvar: Condvar,
277+
}
278+
279+
struct RcFileLockInner {
280+
file: File,
281+
exclusive: bool,
282+
share_count: u32,
283+
}
284+
285+
impl RcFileLock {
286+
pub fn lock(&self) -> CargoResult<()> {
287+
let mut inner = self
288+
.inner
289+
.lock()
290+
.map_err(|_| anyhow!("lock was poisoned"))?;
291+
292+
while inner.exclusive || inner.share_count > 0 {
293+
inner = self
294+
.condvar
295+
.wait(inner)
296+
.map_err(|_| anyhow!("lock was poisoned"))?;
297+
}
298+
299+
inner.file.lock()?;
300+
inner.exclusive = true;
301+
302+
Ok(())
303+
}
304+
305+
pub fn lock_shared(&self) -> CargoResult<()> {
306+
let mut inner = self
307+
.inner
308+
.lock()
309+
.map_err(|_| anyhow!("lock was poisoned"))?;
310+
311+
while inner.exclusive {
312+
inner = self
313+
.condvar
314+
.wait(inner)
315+
.map_err(|_| anyhow!("lock was poisoned"))?;
316+
}
317+
318+
if inner.share_count == 0 {
319+
inner.file.lock_shared()?;
320+
inner.share_count = 1;
321+
} else {
322+
inner.share_count += 1;
323+
}
324+
325+
Ok(())
326+
}
327+
328+
pub fn unlock(&self) -> CargoResult<()> {
329+
let mut inner = self
330+
.inner
331+
.lock()
332+
.map_err(|_| anyhow!("lock was poisoned"))?;
333+
334+
if inner.exclusive {
335+
assert!(inner.share_count == 0);
336+
inner.file.unlock()?;
337+
self.condvar.notify_all();
338+
inner.exclusive = false;
339+
} else {
340+
if inner.share_count > 1 {
341+
inner.share_count -= 1;
342+
} else {
343+
inner.file.unlock()?;
344+
inner.share_count = 0;
345+
self.condvar.notify_all();
346+
}
347+
}
348+
349+
Ok(())
350+
}
351+
352+
pub fn downgrade(&self) -> CargoResult<()> {
353+
let mut inner = self
354+
.inner
355+
.lock()
356+
.map_err(|_| anyhow!("lock was poisoned"))?;
357+
358+
assert!(inner.exclusive);
359+
assert!(inner.share_count == 0);
360+
361+
// NOTE:
362+
// > Subsequent flock() calls on an already locked file will convert an existing lock to the new lock mode.
363+
// https://man7.org/linux/man-pages/man2/flock.2.html
364+
//
365+
// However, the `std::file::File::lock/lock_shared` is allowed to change this in the
366+
// future. So its probably up to us if we are okay with using this or if we want to use a
367+
// different interface to flock.
368+
inner.file.lock_shared()?;
369+
370+
inner.exclusive = false;
371+
inner.share_count = 1;
372+
373+
Ok(())
374+
}
375+
}

0 commit comments

Comments
 (0)