Skip to content

Commit c1c60f4

Browse files
committed
feat(locking): Added reference counted file locking
1 parent 950dd53 commit c1c60f4

File tree

1 file changed

+170
-22
lines changed

1 file changed

+170
-22
lines changed

src/cargo/core/compiler/locking.rs

Lines changed: 170 additions & 22 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-
partial: File,
126-
_full: Option<File>,
126+
partial: Arc<RcFileLock>,
127+
full: Option<Arc<RcFileLock>>,
128+
}
129+
130+
impl Drop for UnitLockGuard {
131+
fn drop(&mut self) {
132+
self.partial.unlock().unwrap();
133+
if let Some(full) = &self.full {
134+
full.unlock().unwrap();
135+
}
136+
}
127137
}
128138

129139
impl UnitLock {
@@ -138,37 +148,34 @@ impl UnitLock {
138148
pub fn lock_exclusive(&mut self) -> CargoResult<()> {
139149
assert!(self.guard.is_none());
140150

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

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

147157
self.guard = Some(UnitLockGuard {
148158
partial,
149-
_full: Some(full),
159+
full: Some(full),
150160
});
151161
Ok(())
152162
}
153163

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

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

160170
let full = if matches!(ty, SharedLockType::Full) {
161-
let full_lock = open_file(&self.full)?;
171+
let full_lock = FileLockInterner::get_or_create_lock(&self.full)?;
162172
full_lock.lock_shared()?;
163173
Some(full_lock)
164174
} else {
165175
None
166176
};
167177

168-
self.guard = Some(UnitLockGuard {
169-
partial,
170-
_full: full,
171-
});
178+
self.guard = Some(UnitLockGuard { partial, full });
172179
Ok(())
173180
}
174181

@@ -177,15 +184,7 @@ impl UnitLock {
177184
.guard
178185
.as_ref()
179186
.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.partial.lock_shared()?;
187+
guard.partial.downgrade()?;
189188

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

0 commit comments

Comments
 (0)