git_policy_cli/replay_reservation.rs
1//! What:
2//! The starvation reservation of the next landing slot:
3//! a transaction that lost `landing.reserveAfterLostRaces` races writes `reservation-request`,
4//! takes the reservation lock `reservation.lock` in the registry when it is granted,
5//! and every other transaction waits before it takes the landing lock.
6//! Why:
7//! Without it a commit that keeps losing the race can starve behind a stream of landings.
8//! Grant order is the oldest live requester, so the transaction that waited longest goes first.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await using reservation = await openLandingReservation({ registryRoot, transactionDirectory, transactionId });
13//! await reservation.reserve(lostRaces);
14//! ```
15
16/// The backstop every wait on a live process is bounded by.
17use super::live_lock::{LIVE_WAIT_VARIABLE, live_wait_limit};
18/// Owner-lock records and their liveness.
19use super::owner_lock::{OwnerLock, OwnerLockError, try_acquire_owner_lock};
20use super::owner_lock_record::{
21 OwnerLockRecord, OwnerRecordError, PublishedOwner, owner_lock_holder_is_alive, read_owner_lock_record,
22};
23/// Registry layout and grant order.
24use super::recovery_inspect::{InspectedEntry, OwnerEvidence, oldest_first};
25use super::transaction_owner::OwnerLiveness;
26use super::transaction_registry::{EntryKind, RESERVATION_LOCK_NAME, list_transaction_entries};
27/// Private file writes that survive a crash.
28use super::private_storage::{sync_directory, write_private_file};
29use super::recovery_error::RecoveryError;
30/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
31use std::path::{Path, PathBuf};
32/// Delay between reservation checks, and the clock a wait is measured on.
33use std::time::{Duration, Instant};
34
35/// Name of the request file inside a transaction directory.
36pub const RESERVATION_REQUEST_FILENAME: &str = "reservation-request";
37
38/// Delay between reservation and holder checks, as the incumbent's `RESERVATION_POLL_MS`.
39pub const RESERVATION_POLL: Duration = Duration::from_millis(20);
40
41/// What:
42/// A reservation failure:
43/// the registry, a lock record, or a request file could not be read or written.
44/// Why:
45/// Replay fails the transaction with the underlying reason instead of landing without the reservation.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// throw new CommitTransactionGitError(...);
50/// ```
51#[derive(Debug)]
52pub enum ReservationError {
53 /// The transaction registry could not be listed or inspected.
54 Registry(RecoveryError),
55 /// The reservation lock's record could not be read.
56 Record(OwnerRecordError),
57 /// The reservation lock could not be taken or released.
58 Lock(OwnerLockError),
59 /// The holder's process identity could not be probed.
60 Identity(String),
61 /// A request file or its directory could not be written.
62 Write(std::io::Error),
63 /// A live reservation holder or an older live requester kept the slot until the wait limit ran out.
64 WaitTimedOut {
65 /// How long this transaction waited.
66 waited: Duration,
67 /// The reservation holder's process, when one holds it and its record could be read.
68 holder_pid: Option<i64>,
69 },
70}
71
72/// Text of each failure:
73/// the debug form for the failures that carry another error, a sentence for a refused wait.
74impl std::fmt::Display for ReservationError {
75 /// Writes the diagnostic text of the failure.
76 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 return match self {
78 Self::WaitTimedOut { waited, holder_pid } => {
79 let who: String = match holder_pid {
80 Some(pid) => format!("process {pid} holds the reservation"),
81 None => String::from("an older live landing request holds the next slot"),
82 };
83 write!(
84 formatter,
85 "gave up after waiting {} seconds for the next landing slot because {who}; \
86 {LIVE_WAIT_VARIABLE} sets how long to wait, and nothing was taken from that process",
87 waited.as_secs()
88 )
89 }
90 other => write!(formatter, "{other:?}"),
91 };
92 }
93}
94
95/// What:
96/// The request file bytes:
97/// `{"schemaVersion":2,"state":"reservation-request","lostRaces":<n>}` and a newline.
98/// Why:
99/// The TypeScript writes this object, not the empty marker SPEC names;
100/// both wrappers read the same file, so the bytes match the incumbent.
101///
102/// In TS you'd write (pseudocode):
103/// ```ts
104/// `${JSON.stringify({ schemaVersion: 2, state: 'reservation-request', lostRaces })}\n`
105/// ```
106pub fn reservation_request_bytes(lost_races: u64) -> Vec<u8> {
107 return format!(
108 "{{\"schemaVersion\":2,\"state\":\"reservation-request\",\"lostRaces\":{lost_races}}}\n"
109 )
110 .into_bytes();
111}
112
113/// What:
114/// Whether a lost race earns the reservation:
115/// the `reserve_after`-th lost race and every later one.
116/// Why:
117/// `landing.reserveAfterLostRaces` counts races lost so far, from 1; the check runs after each loss.
118///
119/// In TS you'd write (pseudocode):
120/// ```ts
121/// lostRaces >= input.reserveAfterLostRaces
122/// ```
123pub fn reservation_earned(lost_races: u64, reserve_after: u64) -> bool {
124 return lost_races >= reserve_after;
125}
126
127/// What:
128/// The transactions with a live request, oldest first; the first one is the next grant.
129/// Why:
130/// Grant order is the oldest live requester, with the registry's creation key breaking ties.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// await liveRequesters(registryRoot); // transaction IDs, oldest invocation first
135/// ```
136pub fn live_requesters(registry_root: &Path) -> Result<Vec<String>, ReservationError> {
137 let entries = list_transaction_entries(registry_root).map_err(ReservationError::Registry)?;
138 // `mut` lets the requesting entries be gathered from the registry listing.
139 let mut requesting: Vec<InspectedEntry> = Vec::new();
140 for entry in entries {
141 if !matches!(entry.kind, EntryKind::Transaction) || !entry.path.join(RESERVATION_REQUEST_FILENAME).exists() {
142 continue;
143 }
144 requesting.push(super::recovery_inspect::inspect_registry_entry(&entry).map_err(ReservationError::Registry)?);
145 }
146 return Ok(oldest_first(requesting)
147 .into_iter()
148 .filter(|inspected| { return matches!(inspected.owner, OwnerEvidence::Owned(_, OwnerLiveness::Alive)); })
149 .map(|inspected| { return inspected.entry.transaction_id; })
150 .collect());
151}
152
153/// What:
154/// The live holder of the reservation lock:
155/// its record when the lock is published and its owner is alive, otherwise nothing.
156/// Why:
157/// A dead holder's reservation no longer blocks anyone; its lock is retired by the next acquisition.
158///
159/// In TS you'd write (pseudocode):
160/// ```ts
161/// liveHolder(lockDirectory); // OwnerLockRecord | NO_HOLDER
162/// ```
163pub fn live_holder(lock_directory: &Path) -> Result<Option<OwnerLockRecord>, ReservationError> {
164 let published: PublishedOwner = read_owner_lock_record(lock_directory).map_err(ReservationError::Record)?;
165 let record = match published {
166 PublishedOwner::Owner(found) => found,
167 PublishedOwner::Busy => return Ok(None),
168 };
169 let alive: bool = owner_lock_holder_is_alive(&record)
170 .map_err(|error| { return ReservationError::Identity(format!("{error:?}")); })?;
171 return Ok(if alive { Some(record) } else { None });
172}
173
174/// What:
175/// One transaction's view of the reservation:
176/// whether another transaction holds it, waiting for it, requesting it, and releasing it.
177/// Why:
178/// The landing loop takes the reservation after its lost races and must give it back when the loop ends,
179/// landed or failed.
180///
181/// In TS you'd write (pseudocode):
182/// ```ts
183/// type LandingReservation = AsyncDisposable & { awaitSlot; heldByAnother; reserve };
184/// ```
185pub struct LandingReservation {
186 /// Transaction registry.
187 registry_root: PathBuf,
188 /// This transaction's directory, which holds the request file.
189 transaction_directory: PathBuf,
190 /// This transaction's ID.
191 transaction_id: String,
192 /// Whether this invocation lands under an ancestor's landing lock, which neither waits nor takes the reservation.
193 nested: bool,
194 /// The reservation, once granted to this transaction.
195 held: Option<OwnerLock>,
196}
197
198impl LandingReservation {
199 /// What:
200 /// Opens the reservation view for one landing loop.
201 /// Why:
202 /// The caller decides `nested` from its inherited landing lease, which this module does not read.
203 ///
204 /// In TS you'd write (pseudocode):
205 /// ```ts
206 /// await openLandingReservation({ registryRoot, transactionDirectory, transactionId });
207 /// ```
208 pub fn open(registry_root: &Path, transaction_directory: &Path, transaction_id: &str, nested: bool) -> Self {
209 return Self {
210 registry_root: registry_root.to_path_buf(),
211 transaction_directory: transaction_directory.to_path_buf(),
212 transaction_id: String::from(transaction_id),
213 nested,
214 held: None,
215 };
216 }
217
218 /// The reservation lock's directory.
219 fn lock_directory(&self) -> PathBuf {
220 return self.registry_root.join(RESERVATION_LOCK_NAME);
221 }
222
223 /// What:
224 /// Whether another transaction holds a live reservation right now.
225 /// Why:
226 /// The holder's own token never counts against itself.
227 ///
228 /// In TS you'd write (pseudocode):
229 /// ```ts
230 /// await reservation.heldByAnother();
231 /// ```
232 pub fn held_by_another(&self) -> Result<bool, ReservationError> {
233 if self.nested {
234 return Ok(false);
235 }
236 let holder = live_holder(self.lock_directory().as_path())?;
237 return Ok(match (holder, self.held.as_ref()) {
238 (None, _) => false,
239 (Some(record), Some(lock)) => record.token != lock.token(),
240 (Some(_), None) => true,
241 });
242 }
243
244 /// What:
245 /// Waits while a live reservation owned by another transaction exists.
246 /// Why:
247 /// A landing may take the landing lock only after the holder has finished its landing loop.
248 ///
249 /// In TS you'd write (pseudocode):
250 /// ```ts
251 /// await reservation.awaitSlot();
252 /// ```
253 pub fn await_slot(&self) -> Result<(), ReservationError> {
254 return self.await_slot_for(live_wait_limit());
255 }
256
257 /// What:
258 /// Waits while a live reservation owned by another transaction exists, for at most `limit`.
259 /// Why:
260 /// Tests and the default wait share one loop; only the limit differs.
261 ///
262 /// In TS you'd write (pseudocode):
263 /// ```ts
264 /// await reservation.awaitSlot({ limitMs });
265 /// ```
266 pub fn await_slot_for(&self, limit: Duration) -> Result<(), ReservationError> {
267 let started: Instant = Instant::now();
268 while self.held_by_another()? {
269 self.refuse_after(started, limit)?;
270 std::thread::sleep(RESERVATION_POLL);
271 }
272 return Ok(());
273 }
274
275 /// What:
276 /// Refuse to keep waiting once `limit` has passed since `started`.
277 /// Why:
278 /// A live process that never finishes its landing would otherwise hold every other transaction for good;
279 /// the refusal names it and leaves its lock and request alone.
280 ///
281 /// In TS you'd write (pseudocode):
282 /// ```ts
283 /// if (Date.now() - startedMs >= limitMs) throw new ReservationTimeout(holderPid);
284 /// ```
285 fn refuse_after(&self, started: Instant, limit: Duration) -> Result<(), ReservationError> {
286 let waited: Duration = started.elapsed();
287 if !limit_reached(waited, limit) {
288 return Ok(());
289 }
290 let holder_pid: Option<i64> = live_holder(self.lock_directory().as_path())?.map(|record| {
291 return record.owner_pid;
292 });
293 return Err(ReservationError::WaitTimedOut { waited, holder_pid });
294 }
295
296 /// What:
297 /// Writes the request, then waits until this transaction is the oldest live requester and the lock is free,
298 /// and takes the reservation.
299 /// Why:
300 /// The first requester in grant order gets the next slot, so a starving commit cannot be overtaken again.
301 /// An inherited landing lease neither asks nor waits.
302 ///
303 /// In TS you'd write (pseudocode):
304 /// ```ts
305 /// await reservation.reserve(lostRaces); // false when already held or not applicable
306 /// ```
307 pub fn reserve(&mut self, lost_races: u64) -> Result<bool, ReservationError> {
308 return self.reserve_for(lost_races, live_wait_limit());
309 }
310
311 /// What:
312 /// Writes the request and waits for the grant, for at most `limit`.
313 /// Why:
314 /// Tests and the default wait share one loop; only the limit differs.
315 ///
316 /// In TS you'd write (pseudocode):
317 /// ```ts
318 /// await reservation.reserve(lostRaces, { limitMs });
319 /// ```
320 pub fn reserve_for(&mut self, lost_races: u64, limit: Duration) -> Result<bool, ReservationError> {
321 if self.nested || self.held.is_some() {
322 return Ok(false);
323 }
324 write_private_file(
325 self.transaction_directory.join(RESERVATION_REQUEST_FILENAME).as_path(),
326 &reservation_request_bytes(lost_races),
327 )
328 .map_err(ReservationError::Write)?;
329 sync_directory(self.transaction_directory.as_path()).map_err(ReservationError::Write)?;
330 let started: Instant = Instant::now();
331 loop {
332 let lock_directory: PathBuf = self.lock_directory();
333 let free: bool = live_holder(lock_directory.as_path())?.is_none();
334 let first: bool = live_requesters(self.registry_root.as_path())?.first() == Some(&self.transaction_id);
335 let granted: Option<OwnerLock> = if free && first {
336 try_acquire_owner_lock(lock_directory.as_path(), Some(self.transaction_id.as_str()))
337 .map_err(ReservationError::Lock)?
338 } else {
339 None
340 };
341 if let Some(lock) = granted {
342 self.held = Some(lock);
343 return Ok(true);
344 }
345 self.refuse_after(started, limit)?;
346 std::thread::sleep(RESERVATION_POLL);
347 }
348 }
349
350 /// What:
351 /// Releases the reservation if held and removes the request file.
352 /// Why:
353 /// The request goes even when the reservation was never granted, so no stale request blocks the next grant.
354 ///
355 /// In TS you'd write (pseudocode):
356 /// ```ts
357 /// await reservation[Symbol.asyncDispose]();
358 /// ```
359 pub fn release(mut self) -> Result<(), ReservationError> {
360 let request: PathBuf = self.transaction_directory.join(RESERVATION_REQUEST_FILENAME);
361 match std::fs::remove_file(&request) {
362 Ok(()) => {}
363 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
364 Err(error) => return Err(ReservationError::Write(error)),
365 }
366 if let Some(lock) = self.held.take() {
367 lock.release().map_err(ReservationError::Lock)?;
368 }
369 return Ok(());
370 }
371}
372
373/// What:
374/// Whether a wait has used up its limit; a wait of exactly the limit has.
375/// Why:
376/// The clock cannot be set to land on the limit, so the comparison is a function a test can call with chosen values.
377///
378/// In TS you'd write (pseudocode):
379/// ```ts
380/// const limitReached = (waitedMs: number, limitMs: number): boolean => waitedMs >= limitMs;
381/// ```
382fn limit_reached(waited: Duration, limit: Duration) -> bool {
383 return waited >= limit;
384}
385
386/// What:
387/// Takes the landing lock once no other transaction's reservation stands in the way,
388/// checking again under the lock, so a reservation granted while this landing waited is honoured.
389/// Why:
390/// The landing lock is acquired by the caller's closure because its module belongs to the landing lane.
391///
392/// In TS you'd write (pseudocode):
393/// ```ts
394/// await acquireReservedLandingLock({ gitPath, cwd, registryRoot, reservation });
395/// ```
396pub fn acquire_reserved_landing_lock<F>(
397 reservation: &LandingReservation,
398 mut acquire_landing: F,
399) -> Result<OwnerLock, ReservationError>
400where
401 F: FnMut() -> Result<OwnerLock, ReservationError>,
402{
403 loop {
404 reservation.await_slot()?;
405 let lock: OwnerLock = acquire_landing()?;
406 if !reservation.held_by_another()? {
407 return Ok(lock);
408 }
409 lock.release().map_err(ReservationError::Lock)?;
410 }
411}
412
413#[cfg(test)]
414#[path = "replay_reservation_tests.rs"]
415mod tests;