Skip to main content

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;