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;