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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
//! What:
//! How the service puts its marked file in place as `index.lock`:
//! the system calls it makes,
//! the atomic placement itself,
//! and the removal of a stale lock.
//! Why:
//! The lock must never exist without its marker,
//! so the marker is written to a file that is new and then made the lock in one step (a rename that does not replace,
//! else a hard link).
//! The calls are one `Platform` value so a control can refuse or count them for one acquisition alone.
//! The module is split from `service_index_lock.rs`,
//! which re-exports the public items here under their old names;
//! the staging files are in `service_index_staging.rs`.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await placeMarked(platform, staged, lock) // renameNoReplace ?? link ?? refuse
//! ```
use super::fs_mount_options::read_mount_facts;
use super::process_identity::ProcessIdentityError;
use super::service_index_marker::own_identity;
use super::service_index_ownership::{Ownership, inspect_ownership};
use super::service_index_watch::Watch;
use std::path::Path;
/// What:
/// The system calls the lock makes that a control must be able to refuse,
/// count or observe:
/// the rename that does not replace,
/// the hard link,
/// the removal of a stale lock,
/// the process's own birth identity,
/// the setting of a watch on the lock's directory,
/// the flush of the staged marker to storage,
/// the question whether a lock is still the holder's,
/// and the removal of a lock the holder owns.
/// Why:
/// A filesystem with neither atomic placement,
/// a stale lock that cannot be unlinked,
/// a process with no identity,
/// a process out of descriptors and a watch quota that is used up
/// are environments no fixture can be made to have without changing the whole test process,
/// and a count kept in a process-wide static is read by whichever test runs beside it;
/// each acquisition takes the calls it makes from one value,
/// so a control substitutes them for that acquisition alone.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type Platform = { rename; link; unlink; identity; watch; sync; inspect };
/// ```
pub struct Platform<'a> {
/// A rename that fails when the target exists (`renameat2` with `RENAME_NOREPLACE`).
pub rename: &'a dyn Fn(&Path, &Path) -> std::io::Result<()>,
/// A hard link that fails when the target exists.
pub link: &'a dyn Fn(&Path, &Path) -> std::io::Result<()>,
/// The removal of a lock whose holder is gone.
pub unlink: &'a dyn Fn(&Path) -> std::io::Result<()>,
/// The birth identity of this process,
/// which a marker names.
pub identity: &'a dyn Fn() -> Result<String, ProcessIdentityError>,
/// A watch on the directory of the lock.
pub watch: &'a dyn Fn(&Path) -> Option<Watch>,
/// The flush of the staged marker to storage,
/// before it becomes the lock.
pub sync: &'a dyn Fn(&std::fs::File) -> std::io::Result<()>,
/// Whether the lock at a path is still the file the holder placed.
pub inspect: fn(&Path, &std::fs::File, &str) -> Ownership,
/// The removal of the lock the holder owns,
/// at its release or when it goes out of scope.
pub release: fn(&Path) -> std::io::Result<()>,
}
/// The operating system's own calls.
pub const SYSTEM: Platform<'static> = Platform {
rename: &rename_noreplace,
link: &link_file,
unlink: &unlink_file,
identity: &own_identity,
watch: &Watch::on,
sync: &sync_file,
inspect: inspect_ownership,
release: unlink_file,
};
pub fn sync_file(file: &std::fs::File) -> std::io::Result<()> {
return file.sync_all();
}
fn link_file(from: &Path, to: &Path) -> std::io::Result<()> {
return std::fs::hard_link(from, to);
}
fn unlink_file(path: &Path) -> std::io::Result<()> {
return std::fs::remove_file(path);
}
/// Renames `from` to `to` unless `to` exists:
/// one step that is as exclusive as `create_new` and leaves no second name.
#[cfg(target_os = "linux")]
pub fn rename_noreplace(from: &Path, to: &Path) -> std::io::Result<()> {
use std::os::unix::ffi::OsStrExt;
let source = std::ffi::CString::new(from.as_os_str().as_bytes()).map_err(std::io::Error::other)?;
let target = std::ffi::CString::new(to.as_os_str().as_bytes()).map_err(std::io::Error::other)?;
// SAFETY: both strings are valid NUL-terminated paths for the call.
let done: i32 = unsafe { libc::renameat2(libc::AT_FDCWD, source.as_ptr(), libc::AT_FDCWD, target.as_ptr(), libc::RENAME_NOREPLACE) };
return if done == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) };
}
#[cfg(not(target_os = "linux"))]
pub fn rename_noreplace(_from: &Path, _to: &Path) -> std::io::Result<()> {
return Err(std::io::Error::from(std::io::ErrorKind::Unsupported));
}
/// Whether a failed rename without replacing means the filesystem cannot do one (renameat2(2):
/// `EINVAL`),
/// not that the lock exists or storage failed.
pub fn rename_unsupported(error: &std::io::Error) -> bool {
if error.kind() == std::io::ErrorKind::Unsupported {
return true;
}
#[cfg(unix)]
{
return matches!(error.raw_os_error(), Some(code) if code == libc::EINVAL || code == libc::ENOSYS || code == libc::EOPNOTSUPP);
}
#[cfg(not(unix))]
{
return false;
}
}
/// Whether a failed hard link means the filesystem makes none (link(2):
/// `EPERM`),
/// not that the lock exists or storage failed.
pub fn links_unsupported(error: &std::io::Error) -> bool {
if error.kind() == std::io::ErrorKind::Unsupported {
return true;
}
#[cfg(unix)]
{
return matches!(error.raw_os_error(), Some(code) if code == libc::EPERM || code == libc::EOPNOTSUPP);
}
#[cfg(not(unix))]
{
return false;
}
}
/// What:
/// Makes the marked `staged` file the lock at `lock`;
/// `AlreadyExists` when any process holds it,
/// and `staged` then stays for the next attempt.
/// Why:
/// Git's lock is taken by exclusive creation,
/// and a restart tells the service's own stale lock from a foreign one only by its marker,
/// so the lock must never exist without it.
/// A rename that does not replace is exclusive and atomic and leaves no second name;
/// where the filesystem has none,
/// a hard link is exclusive too and its staging name is removed after.
/// Where it has neither,
/// nothing is created:
/// exclusive creation followed by a write would leave an empty lock for as long as a kill between the two lasts,
/// and an empty lock may be a Git mid-write,
/// so no restart could take it over.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await renameNoReplace(staged, lock) ?? await link(staged, lock) ?? refuse()
/// ```
///
/// # Errors
///
/// `AlreadyExists` when the lock exists,
/// `Unsupported` (naming both refusals) when the filesystem can place neither way,
/// and the storage error otherwise.
pub fn place_marked(platform: &Platform, staged: &Path, lock: &Path) -> std::io::Result<()> {
let refused_rename: std::io::Error = match (platform.rename)(staged, lock) {
Ok(()) => return Ok(()),
Err(error) if !rename_unsupported(&error) => return Err(error),
Err(error) => error,
};
match (platform.link)(staged, lock) {
Ok(()) => {
let _removed = std::fs::remove_file(staged);
return Ok(());
}
Err(error) if !links_unsupported(&error) => return Err(error),
Err(refused_link) => {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!("renaming without replacing failed ({refused_rename}) and hard links failed ({refused_link})"),
));
}
}
}
/// What:
/// Says in the service's log why a lock was not placed:
/// the lock,
/// the cause,
/// and for a filesystem that can place a lock in neither atomic way,
/// the filesystem and what to do.
/// Why:
/// The clients are answered with a code only;
/// the person who reads the log must find the filesystem and the remedy there.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// console.error(`cli-git-service: could not place ${lock}: ${error}`)
/// ```
pub fn log_placement_failure(lock: &Path, error: &std::io::Error) {
if let Some(text) = placement_failure_text(lock, error) {
crate::service_error!("{text}");
}
}
/// What:
/// The log line for a failure to place `lock`,
/// or nothing when the failure is that the lock exists.
/// Why:
/// A lock that is held is the ordinary case of a wait and is no failure to place;
/// every other error says what happened,
/// and one that means the filesystem can place a lock neither way names the filesystem and what to do.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// placementFailureText(lock, error) // string | undefined
/// ```
pub fn placement_failure_text(lock: &Path, error: &std::io::Error) -> Option<String> {
if error.kind() == std::io::ErrorKind::AlreadyExists {
return None;
}
if error.kind() != std::io::ErrorKind::Unsupported {
return Some(format!("cannot take the index lock {}: {error}", lock.display()));
}
let directory: &Path = lock.parent().unwrap_or(Path::new("."));
let filesystem: String = match read_mount_facts(directory) {
Ok(Some(facts)) => format!("the {} filesystem mounted at {}", facts.filesystem_type, facts.mount_point.display()),
Ok(None) => format!("the filesystem of {}", directory.display()),
Err(reason) => format!("the filesystem of {} ({reason})", directory.display()),
};
return Some(format!(
"cannot take the index lock {}: {filesystem} can neither rename a file without replacing another nor make a hard link ({error}), \
so the lock cannot be created with its marker in one step, and the service takes no lock that a kill could leave empty. \
Keep this repository's Git directory on a filesystem that supports one of the two (ext4, xfs, btrfs, tmpfs and overlayfs do), or update the kernel.",
lock.display()
));
}
/// Removes the stale `lock`,
/// whose holder is gone.
/// False,
/// after saying why in the log,
/// when it is still there;
/// a lock that is already gone counts as removed.
pub fn reclaim(platform: &Platform, lock: &Path) -> bool {
return match (platform.unlink)(lock) {
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()
);
false
}
};
}