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.
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;