@@ -91,24 +91,42 @@ impl Drop for Lockfile {
9191}
9292
9393pub mod advisory {
94+ use chrono:: { DateTime , Utc } ;
95+ use log;
9496 use std:: {
97+ error:: Error as StdError ,
9598 fmt,
96- fs:: { self , File } ,
97- io,
99+ fs:: File ,
100+ io:: { self , Read , Seek , SeekFrom , Write } ,
98101 path:: { Path , PathBuf } ,
102+ process,
103+ time:: SystemTime ,
99104 } ;
100105
101- use fs2:: FileExt as _;
102- use thiserror:: Error ;
103-
104106 use crate :: create_parent_dir;
107+ use fs2:: FileExt as _;
105108
106- #[ derive( Debug , Error ) ]
107- #[ error( "failed to lock {}" , path. display( ) ) ]
109+ #[ derive( Debug ) ]
108110 pub struct LockError {
109111 pub path : PathBuf ,
110- #[ source]
111112 pub source : io:: Error ,
113+ pub existing_contents : Option < String > ,
114+ }
115+
116+ impl fmt:: Display for LockError {
117+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
118+ write ! ( f, "failed to lock {}" , self . path. display( ) ) ?;
119+ if let Some ( contents) = & self . existing_contents {
120+ write ! ( f, " (existing contents: {:?})" , contents) ?;
121+ }
122+ Ok ( ( ) )
123+ }
124+ }
125+
126+ impl StdError for LockError {
127+ fn source ( & self ) -> Option < & ( dyn StdError + ' static ) > {
128+ Some ( & self . source )
129+ }
112130 }
113131
114132 /// A file locked with an exclusive, filesystem-level lock.
@@ -139,30 +157,96 @@ pub mod advisory {
139157 /// created.
140158 pub fn lock ( path : impl AsRef < Path > ) -> Result < Self , LockError > {
141159 let path = path. as_ref ( ) ;
142- Self :: lock_inner ( path) . map_err ( |source| LockError {
143- path : path. into ( ) ,
144- source,
145- } )
160+ Self :: lock_inner ( path)
146161 }
147162
148- fn lock_inner ( path : & Path ) -> io:: Result < Self > {
149- create_parent_dir ( path) ?;
150- let lock = File :: create ( path) ?;
163+ /// Replace the lock file contents with `metadata` while holding the lock.
164+ pub fn write_metadata ( & mut self , metadata : impl AsRef < [ u8 ] > ) -> io:: Result < ( ) > {
165+ self . lock . set_len ( 0 ) ?;
166+ self . lock . seek ( SeekFrom :: Start ( 0 ) ) ?;
167+ self . lock . write_all ( metadata. as_ref ( ) ) ?;
168+ self . lock . sync_data ( ) ?;
169+ Ok ( ( ) )
170+ }
171+
172+ fn lock_inner ( path : & Path ) -> Result < Self , LockError > {
173+ create_parent_dir ( path) . map_err ( |source| LockError {
174+ path : path. to_path_buf ( ) ,
175+ source,
176+ existing_contents : None ,
177+ } ) ?;
178+ // This will create the file if it doesn't already exist.
179+ let mut lock = File :: options ( )
180+ . read ( true )
181+ . write ( true )
182+ . create ( true )
183+ . truncate ( false )
184+ . open ( path)
185+ . map_err ( |source| LockError {
186+ path : path. to_path_buf ( ) ,
187+ source,
188+ existing_contents : None ,
189+ } ) ?;
151190 // TODO: Use `File::lock` (available since rust 1.89) instead?
152- lock. try_lock_exclusive ( ) ?;
191+ if let Err ( source) = lock. try_lock_exclusive ( ) {
192+ let existing_contents = if source. kind ( ) == io:: ErrorKind :: WouldBlock {
193+ Self :: read_existing_contents ( & mut lock) . ok ( ) . flatten ( )
194+ } else {
195+ None
196+ } ;
197+ return Err ( LockError {
198+ path : path. to_path_buf ( ) ,
199+ source,
200+ existing_contents,
201+ } ) ;
202+ }
203+ log:: debug!( "Acquired lock on {}" , path. display( ) ) ;
204+ // Now that we own the lock, clear any content that may have been written by a previous holder.
205+ lock. set_len ( 0 ) . map_err ( |source| LockError {
206+ path : path. to_path_buf ( ) ,
207+ source,
208+ existing_contents : None ,
209+ } ) ?;
210+ lock. seek ( SeekFrom :: Start ( 0 ) ) . map_err ( |source| LockError {
211+ path : path. to_path_buf ( ) ,
212+ source,
213+ existing_contents : None ,
214+ } ) ?;
153215
154- Ok ( Self {
216+ let mut locked = Self {
155217 path : path. to_path_buf ( ) ,
156218 lock,
157- } )
219+ } ;
220+ // Write the default metadata.
221+ locked
222+ . write_metadata ( Self :: default_metadata ( ) )
223+ . map_err ( |source| LockError {
224+ path : path. to_path_buf ( ) ,
225+ source,
226+ existing_contents : None ,
227+ } ) ?;
228+
229+ Ok ( locked)
158230 }
159231
160- /// Release the lock and optionally remove the locked file.
161- pub fn release ( self , remove : bool ) -> io:: Result < ( ) > {
162- if remove {
163- fs:: remove_file ( & self . path ) ?;
232+ fn read_existing_contents ( lock : & mut File ) -> io:: Result < Option < String > > {
233+ lock. seek ( SeekFrom :: Start ( 0 ) ) ?;
234+ let mut bytes = Vec :: new ( ) ;
235+ lock. read_to_end ( & mut bytes) ?;
236+ if bytes. is_empty ( ) {
237+ return Ok ( None ) ;
164238 }
165- Ok ( ( ) )
239+ Ok ( Some ( String :: from_utf8_lossy ( & bytes) . into_owned ( ) ) )
240+ }
241+
242+ // Default contents of a lockfile, which has the pid and timestamp.
243+ fn default_metadata ( ) -> String {
244+ let timestamp_ms = SystemTime :: now ( )
245+ . duration_since ( SystemTime :: UNIX_EPOCH )
246+ . unwrap_or_default ( )
247+ . as_millis ( ) as i64 ;
248+ let timestamp = DateTime :: < Utc > :: from_timestamp_millis ( timestamp_ms) . unwrap_or ( DateTime :: < Utc > :: UNIX_EPOCH ) ;
249+ format ! ( "pid={};timestamp_utc={}" , process:: id( ) , timestamp. to_rfc3339( ) )
166250 }
167251 }
168252
@@ -171,4 +255,98 @@ pub mod advisory {
171255 f. debug_struct ( "LockedFile" ) . field ( "path" , & self . path ) . finish ( )
172256 }
173257 }
258+
259+ impl Drop for LockedFile {
260+ fn drop ( & mut self ) {
261+ log:: debug!( "Released lock on {}" , self . path. display( ) ) ;
262+ }
263+ }
264+ }
265+
266+ #[ cfg( test) ]
267+ mod tests {
268+ use std:: { fs, io:: ErrorKind } ;
269+
270+ use tempdir:: TempDir ;
271+
272+ use super :: advisory:: LockedFile ;
273+
274+ #[ test]
275+ fn lockedfile_can_create_a_file ( ) {
276+ let tmp = TempDir :: new ( "lockfile_test" ) . unwrap ( ) ;
277+ let path = tmp. path ( ) . join ( "db.lock" ) ;
278+ let _lock1 = LockedFile :: lock ( & path) . unwrap ( ) ;
279+ assert ! ( path. exists( ) ) ;
280+ let contents = fs:: read_to_string ( & path) . unwrap ( ) ;
281+ assert ! ( contents. contains( & format!( "pid={}" , std:: process:: id( ) ) ) ) ;
282+ assert ! ( contents. contains( "timestamp_utc=" ) ) ;
283+ }
284+
285+ #[ test]
286+ fn lockedfile_can_create_a_directory_file ( ) {
287+ let tmp = TempDir :: new ( "lockfile_test" ) . unwrap ( ) ;
288+ let path = tmp. path ( ) . join ( "new_dir" ) . join ( "db.lock" ) ;
289+ let _lock1 = LockedFile :: lock ( & path) . unwrap ( ) ;
290+ assert ! ( path. exists( ) ) ;
291+ }
292+
293+ #[ test]
294+ fn only_one_exclusive_lock_can_be_held ( ) {
295+ let tmp = TempDir :: new ( "lockfile_test" ) . unwrap ( ) ;
296+ let path = tmp. path ( ) . join ( "db.lock" ) ;
297+ let _lock1 = LockedFile :: lock ( & path) . unwrap ( ) ;
298+
299+ assert ! ( LockedFile :: lock( & path) . is_err( ) ) ;
300+ }
301+
302+ #[ test]
303+ fn lockedfile_can_handle_existing_file ( ) {
304+ let tmp = TempDir :: new ( "locked_file_test" ) . unwrap ( ) ;
305+ let path = tmp. path ( ) . join ( "db.lock" ) ;
306+ let original = b"existing lock metadata" ;
307+ fs:: write ( & path, original) . unwrap ( ) ;
308+
309+ let _lock = LockedFile :: lock ( & path) . unwrap ( ) ;
310+
311+ // Previous metadata should be replaced when we acquire the lock.
312+ let contents = fs:: read_to_string ( & path) . unwrap ( ) ;
313+ assert ! ( contents. contains( & format!( "pid={}" , std:: process:: id( ) ) ) ) ;
314+ assert ! ( contents. contains( "timestamp_utc=" ) ) ;
315+ }
316+
317+ #[ test]
318+ fn lockedfile_can_store_metadata ( ) {
319+ let tmp = TempDir :: new ( "locked_file_test" ) . unwrap ( ) ;
320+ let path = tmp. path ( ) . join ( "db.lock" ) ;
321+ let mut lock = LockedFile :: lock ( & path) . unwrap ( ) ;
322+
323+ lock. write_metadata ( "pid=1234" ) . unwrap ( ) ;
324+
325+ assert_eq ! ( fs:: read_to_string( & path) . unwrap( ) , "pid=1234" ) ;
326+ }
327+
328+ #[ test]
329+ fn lock_error_includes_existing_contents_when_already_locked ( ) {
330+ let tmp = TempDir :: new ( "locked_file_test" ) . unwrap ( ) ;
331+ let path = tmp. path ( ) . join ( "db.lock" ) ;
332+ let mut lock = LockedFile :: lock ( & path) . unwrap ( ) ;
333+ lock. write_metadata ( "pid=1234" ) . unwrap ( ) ;
334+
335+ let err = LockedFile :: lock ( & path) . unwrap_err ( ) ;
336+ assert_eq ! ( err. source. kind( ) , ErrorKind :: WouldBlock ) ;
337+ assert_eq ! ( err. existing_contents. as_deref( ) , Some ( "pid=1234" ) ) ;
338+ assert ! ( err. to_string( ) . contains( "pid=1234" ) ) ;
339+ }
340+
341+ #[ test]
342+ fn dropping_unlocks_file ( ) {
343+ let tmp = TempDir :: new ( "locked_file_test" ) . unwrap ( ) ;
344+ let path = tmp. path ( ) . join ( "db.lock" ) ;
345+ let mut lock = LockedFile :: lock ( & path) . unwrap ( ) ;
346+ lock. write_metadata ( "pid=1234" ) . unwrap ( ) ;
347+
348+ drop ( lock) ;
349+
350+ let _lock2 = LockedFile :: lock ( & path) . unwrap ( ) ;
351+ }
174352}
0 commit comments