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:: { debug, instrument} ;
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,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