Skip to content

Commit 2dfbb13

Browse files
committed
feat(locking): Added reference counted file locking
1 parent 0a3680b commit 2dfbb13

File tree

1 file changed

+169
-19
lines changed

1 file changed

+169
-19
lines changed

src/cargo/core/compiler/locking.rs

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

0 commit comments

Comments
 (0)