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- 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
129139impl 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