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/// Owner-lock records and their liveness.
17use super::owner_lock::{OwnerLock, OwnerLockError, try_acquire_owner_lock};
18use super::owner_lock_record::{
19    OwnerLockRecord, OwnerRecordError, PublishedOwner, owner_lock_holder_is_alive, read_owner_lock_record,
20};
21/// Registry layout and grant order.
22use super::recovery_inspect::{InspectedEntry, OwnerEvidence, oldest_first};
23use super::transaction_owner::OwnerLiveness;
24use super::transaction_registry::{EntryKind, RESERVATION_LOCK_NAME, list_transaction_entries};
25/// Private file writes that survive a crash.
26use super::private_storage::{sync_directory, write_private_file};
27use super::recovery_error::RecoveryError;
28/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
29use std::path::{Path, PathBuf};
30/// Delay between reservation checks.
31use std::time::Duration;
32
33/// Name of the request file inside a transaction directory.
34pub const RESERVATION_REQUEST_FILENAME: &str = "reservation-request";
35
36/// Delay between reservation and holder checks, as the incumbent's `RESERVATION_POLL_MS`.
37pub const RESERVATION_POLL: Duration = Duration::from_millis(20);
38
39/// What:
40///  A reservation failure:
41///       the registry, a lock record, or a request file could not be read or written.
42/// Why:
43///   Replay fails the transaction with the underlying reason instead of landing without the reservation.
44///
45/// In TS you'd write (pseudocode):
46/// ```ts
47/// throw new CommitTransactionGitError(...);
48/// ```
49#[derive(Debug)]
50pub enum ReservationError {
51    /// The transaction registry could not be listed or inspected.
52    Registry(RecoveryError),
53    /// The reservation lock's record could not be read.
54    Record(OwnerRecordError),
55    /// The reservation lock could not be taken or released.
56    Lock(OwnerLockError),
57    /// The holder's process identity could not be probed.
58    Identity(String),
59    /// A request file or its directory could not be written.
60    Write(std::io::Error),
61}
62
63/// What:
64///  The request file bytes:
65///       `{"schemaVersion":2,"state":"reservation-request","lostRaces":<n>}` and a newline.
66/// Why:
67///   The TypeScript writes this object, not the empty marker SPEC names;
68///       both wrappers read the same file, so the bytes match the incumbent.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// `${JSON.stringify({ schemaVersion: 2, state: 'reservation-request', lostRaces })}\n`
73/// ```
74pub fn reservation_request_bytes(lost_races: u64) -> Vec<u8> {
75    return format!(
76        "{{\"schemaVersion\":2,\"state\":\"reservation-request\",\"lostRaces\":{lost_races}}}\n"
77    )
78    .into_bytes();
79}
80
81/// What:
82///  Whether a lost race earns the reservation:
83///       the `reserve_after`-th lost race and every later one.
84/// Why:
85///   `landing.reserveAfterLostRaces` counts races lost so far, from 1; the check runs after each loss.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// lostRaces >= input.reserveAfterLostRaces
90/// ```
91pub fn reservation_earned(lost_races: u64, reserve_after: u64) -> bool {
92    return lost_races >= reserve_after;
93}
94
95/// What:
96///  The transactions with a live request, oldest first; the first one is the next grant.
97/// Why:
98///   Grant order is the oldest live requester, with the registry's creation key breaking ties.
99///
100/// In TS you'd write (pseudocode):
101/// ```ts
102/// await liveRequesters(registryRoot); // transaction IDs, oldest invocation first
103/// ```
104pub fn live_requesters(registry_root: &Path) -> Result<Vec<String>, ReservationError> {
105    let entries = list_transaction_entries(registry_root).map_err(ReservationError::Registry)?;
106    // `mut` lets the requesting entries be gathered from the registry listing.
107    let mut requesting: Vec<InspectedEntry> = Vec::new();
108    for entry in entries {
109        if !matches!(entry.kind, EntryKind::Transaction) || !entry.path.join(RESERVATION_REQUEST_FILENAME).exists() {
110            continue;
111        }
112        requesting.push(super::recovery_inspect::inspect_registry_entry(&entry).map_err(ReservationError::Registry)?);
113    }
114    return Ok(oldest_first(requesting)
115        .into_iter()
116        .filter(|inspected| { return matches!(inspected.owner, OwnerEvidence::Owned(_, OwnerLiveness::Alive)); })
117        .map(|inspected| { return inspected.entry.transaction_id; })
118        .collect());
119}
120
121/// What:
122///  The live holder of the reservation lock:
123///       its record when the lock is published and its owner is alive, otherwise nothing.
124/// Why:
125///   A dead holder's reservation no longer blocks anyone; its lock is retired by the next acquisition.
126///
127/// In TS you'd write (pseudocode):
128/// ```ts
129/// liveHolder(lockDirectory); // OwnerLockRecord | NO_HOLDER
130/// ```
131pub fn live_holder(lock_directory: &Path) -> Result<Option<OwnerLockRecord>, ReservationError> {
132    let published: PublishedOwner = read_owner_lock_record(lock_directory).map_err(ReservationError::Record)?;
133    let record = match published {
134        PublishedOwner::Owner(found) => found,
135        PublishedOwner::Busy => return Ok(None),
136    };
137    let alive: bool = owner_lock_holder_is_alive(&record)
138        .map_err(|error| { return ReservationError::Identity(format!("{error:?}")); })?;
139    return Ok(if alive { Some(record) } else { None });
140}
141
142/// What:
143///  One transaction's view of the reservation:
144///       whether another transaction holds it, waiting for it, requesting it, and releasing it.
145/// Why:
146///   The landing loop takes the reservation after its lost races and must give it back when the loop ends,
147///       landed or failed.
148///
149/// In TS you'd write (pseudocode):
150/// ```ts
151/// type LandingReservation = AsyncDisposable & { awaitSlot; heldByAnother; reserve };
152/// ```
153pub struct LandingReservation {
154    /// Transaction registry.
155    registry_root: PathBuf,
156    /// This transaction's directory, which holds the request file.
157    transaction_directory: PathBuf,
158    /// This transaction's ID.
159    transaction_id: String,
160    /// Whether this invocation lands under an ancestor's landing lock, which neither waits nor takes the reservation.
161    nested: bool,
162    /// The reservation, once granted to this transaction.
163    held: Option<OwnerLock>,
164}
165
166impl LandingReservation {
167    /// What:
168    ///  Opens the reservation view for one landing loop.
169    /// Why:
170    ///   The caller decides `nested` from its inherited landing lease, which this module does not read.
171    ///
172    /// In TS you'd write (pseudocode):
173    /// ```ts
174    /// await openLandingReservation({ registryRoot, transactionDirectory, transactionId });
175    /// ```
176    pub fn open(registry_root: &Path, transaction_directory: &Path, transaction_id: &str, nested: bool) -> Self {
177        return Self {
178            registry_root: registry_root.to_path_buf(),
179            transaction_directory: transaction_directory.to_path_buf(),
180            transaction_id: String::from(transaction_id),
181            nested,
182            held: None,
183        };
184    }
185
186    /// The reservation lock's directory.
187    fn lock_directory(&self) -> PathBuf {
188        return self.registry_root.join(RESERVATION_LOCK_NAME);
189    }
190
191    /// What:
192    ///  Whether another transaction holds a live reservation right now.
193    /// Why:
194    ///   The holder's own token never counts against itself.
195    ///
196    /// In TS you'd write (pseudocode):
197    /// ```ts
198    /// await reservation.heldByAnother();
199    /// ```
200    pub fn held_by_another(&self) -> Result<bool, ReservationError> {
201        if self.nested {
202            return Ok(false);
203        }
204        let holder = live_holder(self.lock_directory().as_path())?;
205        return Ok(match (holder, self.held.as_ref()) {
206            (None, _) => false,
207            (Some(record), Some(lock)) => record.token != lock.token(),
208            (Some(_), None) => true,
209        });
210    }
211
212    /// What:
213    ///  Waits while a live reservation owned by another transaction exists.
214    /// Why:
215    ///   A landing may take the landing lock only after the holder has finished its landing loop.
216    ///
217    /// In TS you'd write (pseudocode):
218    /// ```ts
219    /// await reservation.awaitSlot();
220    /// ```
221    pub fn await_slot(&self) -> Result<(), ReservationError> {
222        while self.held_by_another()? {
223            std::thread::sleep(RESERVATION_POLL);
224        }
225        return Ok(());
226    }
227
228    /// What:
229    ///  Writes the request, then waits until this transaction is the oldest live requester and the lock is free,
230    ///       and takes the reservation.
231    /// Why:
232    ///   The first requester in grant order gets the next slot, so a starving commit cannot be overtaken again.
233    ///       An inherited landing lease neither asks nor waits.
234    ///
235    /// In TS you'd write (pseudocode):
236    /// ```ts
237    /// await reservation.reserve(lostRaces); // false when already held or not applicable
238    /// ```
239    pub fn reserve(&mut self, lost_races: u64) -> Result<bool, ReservationError> {
240        if self.nested || self.held.is_some() {
241            return Ok(false);
242        }
243        write_private_file(
244            self.transaction_directory.join(RESERVATION_REQUEST_FILENAME).as_path(),
245            &reservation_request_bytes(lost_races),
246        )
247        .map_err(ReservationError::Write)?;
248        sync_directory(self.transaction_directory.as_path()).map_err(ReservationError::Write)?;
249        loop {
250            let lock_directory: PathBuf = self.lock_directory();
251            let free: bool = live_holder(lock_directory.as_path())?.is_none();
252            let first: bool = live_requesters(self.registry_root.as_path())?.first() == Some(&self.transaction_id);
253            let granted: Option<OwnerLock> = if free && first {
254                try_acquire_owner_lock(lock_directory.as_path(), Some(self.transaction_id.as_str()))
255                    .map_err(ReservationError::Lock)?
256            } else {
257                None
258            };
259            if let Some(lock) = granted {
260                self.held = Some(lock);
261                return Ok(true);
262            }
263            std::thread::sleep(RESERVATION_POLL);
264        }
265    }
266
267    /// What:
268    ///  Releases the reservation if held and removes the request file.
269    /// Why:
270    ///   The request goes even when the reservation was never granted, so no stale request blocks the next grant.
271    ///
272    /// In TS you'd write (pseudocode):
273    /// ```ts
274    /// await reservation[Symbol.asyncDispose]();
275    /// ```
276    pub fn release(mut self) -> Result<(), ReservationError> {
277        let request: PathBuf = self.transaction_directory.join(RESERVATION_REQUEST_FILENAME);
278        match std::fs::remove_file(&request) {
279            Ok(()) => {}
280            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
281            Err(error) => return Err(ReservationError::Write(error)),
282        }
283        if let Some(lock) = self.held.take() {
284            lock.release().map_err(ReservationError::Lock)?;
285        }
286        return Ok(());
287    }
288}
289
290/// What:
291///  Takes the landing lock once no other transaction's reservation stands in the way,
292///       checking again under the lock, so a reservation granted while this landing waited is honoured.
293/// Why:
294///   The landing lock is acquired by the caller's closure because its module belongs to the landing lane.
295///
296/// In TS you'd write (pseudocode):
297/// ```ts
298/// await acquireReservedLandingLock({ gitPath, cwd, registryRoot, reservation });
299/// ```
300pub fn acquire_reserved_landing_lock<F>(
301    reservation: &LandingReservation,
302    mut acquire_landing: F,
303) -> Result<OwnerLock, ReservationError>
304where
305    F: FnMut() -> Result<OwnerLock, ReservationError>,
306{
307    loop {
308        reservation.await_slot()?;
309        let lock: OwnerLock = acquire_landing()?;
310        if !reservation.held_by_another()? {
311            return Ok(lock);
312        }
313        lock.release().map_err(ReservationError::Lock)?;
314    }
315}
316
317#[cfg(test)]
318#[path = "replay_reservation_tests.rs"]
319mod tests;