1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! What:
//! The removal of a stale index lock: taken aside by one rename, looked at again there, and removed only if it is still the stale
//! one.
//! Why:
//! A removal by name removes whatever the name holds by then, so a lock another program placed after the marker was read would
//! be lost. The module is split from `service_index_placement.rs` and `service_index_lock.rs`, which re-export its functions.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! reclaimIf(platform, lock, stillStale) // rename to aside; stillStale(aside) ? unlink(aside) : link(aside, lock)
//! ```
use super::service_index_lock::lock_path_of;
use super::service_index_marker::{holder_is_gone, marker_of};
use super::service_index_placement::{Platform, SYSTEM};
use std::path::{Path, PathBuf};
/// What:
/// Removes an index lock left by a service that was killed, when it still carries the service's marker for `seq` and its
/// holder is gone.
/// Why:
/// Only the marker proves the lock is ours; a foreign lock, one a Git holds, or one whose holder still runs is never touched.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// removeStaleLock(index, seq)
/// ```
pub fn remove_stale(index: &Path, seq: u64) {
let lock: PathBuf = lock_path_of(index);
let stale = |path: &Path| -> bool { return marker_of(path).is_some_and(|marked| { return marked.seq == seq && holder_is_gone(marked.holder.as_ref()); }); };
if stale(lock.as_path()) {
crate::service_fault::reach("reclaim-checked");
// The lock is taken aside by one rename and looked at again, so a lock another program placed after the look above is
// put back and never removed: removing by name would remove whatever the name holds by then.
let _removed = reclaim_if(&SYSTEM, lock.as_path(), &stale);
}
}
/// What:
/// Removes the file at `lock` only if `still_stale` says the file taken from that name is the stale one; true when no lock is
/// left in the way.
/// Why:
/// A removal by name removes whatever the name holds by then: between a look at the marker and the removal, the stale lock
/// may be gone and a foreign program's live lock in its place (a second instance serving the same physical index, or Git), and
/// the live lock would be removed. The file is taken aside by one rename, which takes exactly one file, looked at again
/// there, removed if it is the stale one, and otherwise put back by a hard link that fails rather than replace a lock
/// placed meanwhile. A lock that could not be put back stays at the name it was taken to, and the log says where.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// reclaimIf(platform, lock, stillStale) // rename to aside; stillStale(aside) ? unlink(aside) : link(aside, lock)
/// ```
pub fn reclaim_if(platform: &Platform, lock: &Path, still_stale: &dyn Fn(&Path) -> bool) -> bool {
let Some(aside) = aside_of(lock) else {
crate::service_error!("the index lock {} cannot be taken aside: it has no file name", lock.display());
return false;
};
match std::fs::rename(lock, &aside) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return true,
Err(error) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it cannot be taken aside ({error}); \
remove it by hand, and clear its immutable attribute or fix the directory's permissions first if that is why it stays",
lock.display()
);
return false;
}
}
if !still_stale(aside.as_path()) {
put_back(aside.as_path(), lock);
return false;
}
return match (platform.unlink)(&aside) {
Ok(()) => true,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => true,
Err(error) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it cannot be removed ({error}); \
remove it by hand, and clear its immutable attribute or fix the directory's permissions first if that is why it stays",
lock.display()
);
put_back(aside.as_path(), lock);
false
}
};
}
/// A name beside `lock` that no other run uses: the process and the clock.
fn aside_of(lock: &Path) -> Option<std::path::PathBuf> {
let clock: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_nanos();
return Some(lock.with_file_name(format!("{}.aside-{}-{clock:x}", lock.file_name()?.to_string_lossy(), std::process::id())));
}
/// Puts the file taken aside back under the lock's name, without replacing a file that has the name by now.
fn put_back(aside: &Path, lock: &Path) {
match std::fs::hard_link(aside, lock) {
Ok(()) => {
let _removed = std::fs::remove_file(aside);
}
Err(error) => {
crate::service_error!("the index lock {} was taken aside to {} and could not be put back ({error}); the file at the second name is that lock, and it is not removed",
lock.display(),
aside.display()
);
}
}
}