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