Skip to content

Commit 253a416

Browse files
committed
feat(locking): Added reference counted file locking
1 parent f8a1a04 commit 253a416

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,8 +123,17 @@ 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 {
@@ -138,27 +148,27 @@ impl UnitLock {
138148
pub fn lock_exclusive(&mut self) -> CargoResult<()> {
139149
assert!(self.guard.is_none());
140150

141-
let primary_lock = open_file(&self.primary)?;
151+
let primary_lock = FileLockInterner::get_or_create_lock(&self.primary)?;
142152
primary_lock.lock()?;
143153

144-
let secondary_lock = open_file(&self.secondary)?;
154+
let secondary_lock = FileLockInterner::get_or_create_lock(&self.secondary)?;
145155
secondary_lock.lock()?;
146156

147157
self.guard = Some(UnitLockGuard {
148158
primary: primary_lock,
149-
_secondary: Some(secondary_lock),
159+
secondary: Some(secondary_lock),
150160
});
151161
Ok(())
152162
}
153163

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

157-
let primary_lock = open_file(&self.primary)?;
167+
let primary_lock = FileLockInterner::get_or_create_lock(&self.primary)?;
158168
primary_lock.lock_shared()?;
159169

160170
let secondary_lock = if matches!(ty, SharedLockType::Full) {
161-
let secondary_lock = open_file(&self.secondary)?;
171+
let secondary_lock = FileLockInterner::get_or_create_lock(&self.secondary)?;
162172
secondary_lock.lock_shared()?;
163173
Some(secondary_lock)
164174
} else {
@@ -167,7 +177,7 @@ impl UnitLock {
167177

168178
self.guard = Some(UnitLockGuard {
169179
primary: primary_lock,
170-
_secondary: secondary_lock,
180+
secondary: secondary_lock,
171181
});
172182
Ok(())
173183
}
@@ -177,15 +187,7 @@ impl UnitLock {
177187
.guard
178188
.as_ref()
179189
.context("guard was None while calling downgrade")?;
180-
181-
// NOTE:
182-
// > Subsequent flock() calls on an already locked file will convert an existing lock to the new lock mode.
183-
// https://man7.org/linux/man-pages/man2/flock.2.html
184-
//
185-
// However, the `std::file::File::lock/lock_shared` is allowed to change this in the
186-
// future. So its probably up to us if we are okay with using this or if we want to use a
187-
// different interface to flock.
188-
guard.primary.lock_shared()?;
190+
guard.primary.downgrade()?;
189191

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

0 commit comments

Comments
 (0)