@@ -796,6 +796,51 @@ fn definitions_lock_owned_by(lock_path: &Path, owner_token: &str) -> bool {
796796 . is_some_and ( |token| token == owner_token)
797797}
798798
799+ #[ derive( Debug , PartialEq , Eq ) ]
800+ struct DefinitionsLockSnapshot {
801+ owner_token : String ,
802+ modified : SystemTime ,
803+ len : u64 ,
804+ }
805+
806+ fn definitions_lock_snapshot ( lock_path : & Path ) -> io:: Result < DefinitionsLockSnapshot > {
807+ let content = fs:: read_to_string ( lock_path) ?;
808+ let metadata = fs:: metadata ( lock_path) ?;
809+ let owner_token = content. lines ( ) . next ( ) . unwrap_or_default ( ) . to_string ( ) ;
810+ Ok ( DefinitionsLockSnapshot {
811+ owner_token,
812+ modified : metadata. modified ( ) ?,
813+ len : metadata. len ( ) ,
814+ } )
815+ }
816+
817+ fn stale_definitions_lock_snapshot ( lock_path : & Path ) -> Option < DefinitionsLockSnapshot > {
818+ let snapshot = definitions_lock_snapshot ( lock_path) . ok ( ) ?;
819+ SystemTime :: now ( )
820+ . duration_since ( snapshot. modified )
821+ . ok ( )
822+ . filter ( |age| * age > DEFINITIONS_STALE_LOCK_AFTER )
823+ . map ( |_| snapshot)
824+ }
825+
826+ fn remove_stale_definitions_lock (
827+ lock_path : & Path ,
828+ stale_snapshot : & DefinitionsLockSnapshot ,
829+ ) -> io:: Result < bool > {
830+ match definitions_lock_snapshot ( lock_path) {
831+ Ok ( current_snapshot) if current_snapshot == * stale_snapshot => {
832+ match fs:: remove_file ( lock_path) {
833+ Ok ( ( ) ) => Ok ( true ) ,
834+ Err ( error) if error. kind ( ) == io:: ErrorKind :: NotFound => Ok ( false ) ,
835+ Err ( error) => Err ( error) ,
836+ }
837+ }
838+ Ok ( _) => Ok ( false ) ,
839+ Err ( error) if error. kind ( ) == io:: ErrorKind :: NotFound => Ok ( false ) ,
840+ Err ( error) => Err ( error) ,
841+ }
842+ }
843+
799844fn lock_definitions_file ( path : & Path ) -> io:: Result < DefinitionsFileLock > {
800845 let lock_path = definitions_lock_path ( path) ;
801846 let deadline = Instant :: now ( ) + DEFINITIONS_LOCK_TIMEOUT ;
@@ -817,8 +862,10 @@ fn lock_definitions_file(path: &Path) -> io::Result<DefinitionsFileLock> {
817862 } ) ;
818863 }
819864 Err ( error) if error. kind ( ) == io:: ErrorKind :: AlreadyExists => {
820- if is_stale_definitions_lock ( & lock_path) {
821- let _ = fs:: remove_file ( & lock_path) ;
865+ if let Some ( stale_snapshot) = stale_definitions_lock_snapshot ( & lock_path) {
866+ if remove_stale_definitions_lock ( & lock_path, & stale_snapshot) ? {
867+ continue ;
868+ }
822869 continue ;
823870 }
824871 if Instant :: now ( ) >= deadline {
@@ -834,14 +881,6 @@ fn lock_definitions_file(path: &Path) -> io::Result<DefinitionsFileLock> {
834881 }
835882}
836883
837- fn is_stale_definitions_lock ( lock_path : & Path ) -> bool {
838- fs:: metadata ( lock_path)
839- . and_then ( |metadata| metadata. modified ( ) )
840- . ok ( )
841- . and_then ( |modified| SystemTime :: now ( ) . duration_since ( modified) . ok ( ) )
842- . is_some_and ( |age| age > DEFINITIONS_STALE_LOCK_AFTER )
843- }
844-
845884#[ cfg( not( windows) ) ]
846885fn replace_file_atomic ( temp_path : & Path , path : & Path ) -> io:: Result < ( ) > {
847886 fs:: rename ( temp_path, path)
@@ -2501,6 +2540,32 @@ models:
25012540 let _ = fs:: remove_file ( definitions_path) ;
25022541 }
25032542
2543+ #[ test]
2544+ fn test_stale_lock_cleanup_does_not_remove_recreated_lock ( ) {
2545+ let _guard = test_lock ( ) ;
2546+
2547+ let db_path = unique_db_path ( "stale_lock_recreated" ) ;
2548+ let db_path = CString :: new ( db_path. to_string_lossy ( ) . to_string ( ) ) . unwrap ( ) ;
2549+ let definitions_path = get_definitions_path ( db_path. as_ptr ( ) ) . unwrap ( ) ;
2550+ let lock_path = definitions_lock_path ( & definitions_path) ;
2551+
2552+ let stale_token = "pid=stale;time=1;seq=1" ;
2553+ fs:: write ( & lock_path, format ! ( "{stale_token}\n " ) ) . unwrap ( ) ;
2554+ let stale_snapshot = definitions_lock_snapshot ( & lock_path) . unwrap ( ) ;
2555+
2556+ let recreated_token = "pid=recreated;time=2;seq=2" ;
2557+ fs:: write ( & lock_path, format ! ( "{recreated_token}\n " ) ) . unwrap ( ) ;
2558+
2559+ assert ! ( !remove_stale_definitions_lock( & lock_path, & stale_snapshot) . unwrap( ) ) ;
2560+ assert_eq ! (
2561+ fs:: read_to_string( & lock_path) . unwrap( ) ,
2562+ format!( "{recreated_token}\n " )
2563+ ) ;
2564+
2565+ let _ = fs:: remove_file ( lock_path) ;
2566+ let _ = fs:: remove_file ( definitions_path) ;
2567+ }
2568+
25042569 #[ test]
25052570 fn test_remove_model_from_file_handles_multiline_models ( ) {
25062571 let _guard = test_lock ( ) ;
0 commit comments