Skip to content

Commit 0fd228f

Browse files
committed
Verify stale definition locks before cleanup
1 parent 496315e commit 0fd228f

1 file changed

Lines changed: 75 additions & 10 deletions

File tree

sidemantic-rs/src/ffi.rs

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
799844
fn 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))]
846885
fn 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

Comments
 (0)