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,35 +123,44 @@ 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 {
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