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
107
108
109
110
111
112
113
114
115
//! What:
//! The marker the service writes into the `index.lock` it holds,
//! and the questions asked of a marker found there.
//! Why:
//! A restart tells its own stale lock from a foreign one,
//! and a live holder from a dead one,
//! by the marker alone:
//! the batch,
//! the holding process and that process's birth identity.
//! The module is split from `service_index_lock.rs`,
//! which re-exports the public items here under their old names.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const marker = markerText(seq); const reclaimable = isReclaimable(lockPath);
//! ```
use super::process_identity::{ProcessIdentityError, process_birth_identity};
use std::io::Read;
use std::path::Path;
/// The first bytes of a lock the service holds,
/// followed by the batch number,
/// the holding process and its birth identity.
pub const LOCK_MARKER: &str = "cli-git-service ";
/// The longest marker this reader looks at.
const MARKER_LIMIT: u64 = 512;
/// What a service marker names.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Marked {
/// The batch the lock was taken for.
pub seq: u64,
/// The holding process and its birth identity;
/// none in a marker a release before this one wrote.
pub holder: Option<(i64, String)>,
}
/// What:
/// The marker this process writes for batch `seq`:
/// the batch,
/// the process and its birth identity,
/// one line.
/// Why:
/// A restart reclaims a marked lock only when that process is gone,
/// so a slow but live holder keeps its lock.
/// A marker without a birth identity reads as a dead instance's,
/// so a process that cannot name its own identity writes none:
/// it must not take the lock.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `cli-git-service ${seq} ${process.pid} ${birth}\n`
/// ```
///
/// # Errors
///
/// The identity error when this process's birth identity cannot be read.
pub fn marker_text(seq: u64, identity: &dyn Fn() -> Result<String, ProcessIdentityError>) -> Result<String, ProcessIdentityError> {
let birth: String = identity()?;
return Ok(format!("{LOCK_MARKER}{seq} {} {birth}\n", std::process::id()));
}
/// What:
/// The batch and holder a lock's text names,
/// or nothing when it is not a service marker.
/// Why:
/// A marker without a holder is one a release before this one wrote;
/// one with a malformed holder is not the service's.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseMarker(text) // { seq, holder?: [pid, birth] } | undefined
/// ```
pub fn parse_marker(text: &str) -> Option<Marked> {
let rest: &str = text.strip_prefix(LOCK_MARKER)?;
let line: &str = rest.strip_suffix('\n').unwrap_or(rest);
let mut parts = line.splitn(3, ' ');
let seq: u64 = parts.next()?.parse::<u64>().ok()?;
let holder: Option<(i64, String)> = match (parts.next(), parts.next()) {
(None, None) => None,
(Some(pid), Some(birth)) if !birth.is_empty() => Some((pid.parse::<i64>().ok()?, birth.to_string())),
_ => return None,
};
return Some(Marked { seq, holder });
}
/// Whether the process a marker names is gone:
/// no process under that number,
/// or another one born since.
/// A question that cannot be answered keeps the lock,
/// which is the safe side.
pub fn holder_is_gone(holder: Option<&(i64, String)>) -> bool {
let Some((pid, birth)) = holder else { return true };
return match process_birth_identity(*pid) {
Ok(None) => true,
Ok(Some(now)) => now != *birth,
Err(_) => false,
};
}
/// The marker of the file at `path`,
/// when it holds one.
pub fn marker_of(path: &Path) -> Option<Marked> {
let mut text: String = String::new();
std::fs::File::open(path).ok()?.take(MARKER_LIMIT).read_to_string(&mut text).ok()?;
return parse_marker(text.as_str());
}
/// Whether the lock at `lock` is the service's and its holder is gone,
/// so it may be removed.
pub fn is_reclaimable(lock: &Path) -> bool {
return marker_of(lock).is_some_and(|marked| { return holder_is_gone(marked.holder.as_ref()); });
}