3131//! [`CompilationLock`] is the primary interface for locking.
3232
3333use 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 } ;
4041use itertools:: Itertools ;
4142use tracing:: { instrument, trace} ;
4243
@@ -122,8 +123,17 @@ struct UnitLock {
122123}
123124
124125struct 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
129139impl 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