tollgate_core/reservation.rs
1//! The reservation state machine: `pending → committed-at-execution-start`
2//! or `pending → released`.
3//!
4//! The charging rules this encodes (INVARIANTS.md states them as contract):
5//!
6//! - Admission debits the lease immediately, but the charge is only *pending*.
7//! - Execution start commits the full quoted charge — for success, domain
8//! failure, or timeout alike.
9//! - Anything that ends the request before execution (validation failure,
10//! client cancellation, shedding, drop) releases the units for zero charge.
11//! - Commit and cancel race on one atomic compare-exchange: exactly one wins
12//! (INVARIANTS.md GL-3 here). A canceller that loses learns the committed
13//! charge; a committer that loses must not execute.
14//! - Under `Elastic`, a lease whose usability window lapsed between admission
15//! and execution start settles against overage instead of refusing — still
16//! one compare-exchange, so the canceller and the committer still race for
17//! a single phase rather than for two separate reservations.
18
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
21
22use jiff::Timestamp;
23
24use crate::deny::DenyReason;
25use crate::ids::{AccountId, PolicyRevision, RequestId};
26use crate::lease::{AccountOverage, LeaseDebit, LocalLease};
27use crate::sharding::Locality;
28use crate::snapshot::EnforcementMode;
29use crate::units::CostUnits;
30use crate::usage::{UsageEvent, UsageSource};
31
32// The phase word is the single authority for how a reservation *resolved*,
33// and it names the funding that resolution settled against. `ChargeSource`
34// stays the immutable funding *receipt*: it routes refunds, owns the lease
35// window and capability, and supplies the initial phase value — but it is not
36// the billing statement, because a leased reservation whose window lapses at
37// execution start can settle against overage instead (INVARIANTS.md GL-3).
38//
39// PENDING_LEASE ─┬→ COMMITTED_LEASE
40// ├→ COMMITTED_OVERAGE (Elastic commit-time fallback)
41// └→ RELEASED
42// PENDING_OVERAGE ─→ COMMITTED_OVERAGE | RELEASED
43const PENDING_LEASE: u8 = 0;
44const PENDING_OVERAGE: u8 = 1;
45const COMMITTED_LEASE: u8 = 2;
46const COMMITTED_OVERAGE: u8 = 3;
47const RELEASED: u8 = 4;
48
49/// Outcome of [`Reservation::cancel`].
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CancelOutcome {
52 /// Cancellation won (or the reservation was already released): zero units
53 /// charged, units returned to the lease.
54 ZeroCharged,
55 /// Execution had already started; the full charge stands.
56 AlreadyCommitted { units: CostUnits },
57}
58
59/// Error from [`Reservation::commit_at_execution_start`].
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum CommitError {
62 /// Cancellation won before execution started.
63 Cancelled,
64 /// Funding could not remain valid through execution start. The
65 /// reservation has been released (units returned, zero charged) and the
66 /// caller must not execute the work.
67 ///
68 /// `DenyReason::FundingExpiredAtStart` is the lease's usability window
69 /// lapsing between reserve and commit: past that point the allocator may
70 /// reclaim and re-grant the capacity, so committing would perform work
71 /// that can never be billed. Under
72 /// [`EnforcementMode::Elastic`]
73 /// the same lapse first attempts the overage fallback, and any of
74 /// `OverageCapExhausted`, `OverageCapTemporarilyExhausted`, or
75 /// `OverageCommitInProgress` may be reported instead. Each carries its own
76 /// [`Retry`](crate::deny::Retry) classification and none may be collapsed
77 /// into another: telling a caller to retry immediately against units that
78 /// are already irrevocable is precisely the lie the classifier exists to
79 /// prevent.
80 Denied(DenyReason),
81 /// Cancellation won the race. The caller must not execute the work; the
82 /// request reports zero units.
83 AlreadyReleased,
84 /// The reservation was committed before — committing twice is a caller
85 /// bug, surfaced rather than silently absorbed.
86 AlreadyCommitted,
87}
88
89/// What funding the reservation may settle against at execution start.
90///
91/// Built from the request's *pinned* snapshot at the point of commit rather
92/// than stored in the [`Reservation`]. The counter is shared by every
93/// principal, lease, and locality of the account, so its refcount is one cache
94/// line written by every core serving that account; cloning an `Arc` to it per
95/// admission would put a contended atomic on the request path for a capability
96/// most requests never use, and grow every staged type by its width. Passing
97/// it at commit costs nothing, because the whole fallback lives inside the
98/// already-cold "window lapsed" branch and folds away entirely at a
99/// [`LeaseOnly`](CommitFunding::LeaseOnly) call site.
100///
101/// Reading the mode at commit is not a staleness risk: the caller holds the
102/// same immutable snapshot for the request's whole life, so this is the
103/// generation admission itself read.
104#[derive(Debug, Clone, Copy)]
105pub enum CommitFunding<'a> {
106 /// [`EnforcementMode::Strict`]:
107 /// a lapsed lease releases for zero and the kernel must not run.
108 LeaseOnly,
109 /// [`EnforcementMode::Elastic`]:
110 /// a lapsed lease may settle against overage instead, bounded by `cap`.
111 OverageFallback {
112 overage: &'a AccountOverage,
113 cap: CostUnits,
114 },
115}
116
117impl<'a> CommitFunding<'a> {
118 /// The one place enforcement mode chooses a commit-time funding rule.
119 ///
120 /// Taking the counter alongside the mode keeps them from being sourced
121 /// separately: `overage` must be the account's own counter — the one
122 /// reached through the same lease slot that funded the reservation — and
123 /// commit debug-asserts that it names the same account.
124 #[must_use]
125 #[inline]
126 pub fn from_mode(mode: EnforcementMode, overage: &'a AccountOverage) -> Self {
127 match mode.overage_cap() {
128 None => Self::LeaseOnly,
129 Some(cap) => Self::OverageFallback { overage, cap },
130 }
131 }
132}
133
134/// What a reservation debited, and therefore what a release must refund.
135///
136/// A reservation cannot hold a plain `Arc<LocalLease>` any more, because the
137/// case elastic mode exists to serve includes *having no lease at all* — a
138/// cold start, or an instance whose lease lapsed before refill replaced it.
139/// The discriminant is what lets a release find its way back to the counter it
140/// came from without either counter having to know about the other.
141#[derive(Debug)]
142enum ChargeSource {
143 /// Units debited from a lease the allocator granted, with the evidence
144 /// naming the shard counter they came from: a release must return them to
145 /// that counter, not merely to the lease.
146 Lease {
147 lease: Arc<LocalLease>,
148 debit: LeaseDebit,
149 },
150 /// Unfunded units extended under [`EnforcementMode::Elastic`].
151 ///
152 /// [`EnforcementMode::Elastic`]: crate::snapshot::EnforcementMode::Elastic
153 Overage(Arc<AccountOverage>),
154}
155
156impl ChargeSource {
157 /// The phase this receipt opens in.
158 ///
159 /// Deriving it from the receipt, rather than storing a second `pending`
160 /// field, is what keeps construction, `cancel`, and `Drop` from ever
161 /// disagreeing about the value to compare-exchange *from*. A stored copy
162 /// that drifted would make `Drop` fail its CAS and silently skip the
163 /// refund — stranding capacity with no diagnostic.
164 const fn pending_phase(&self) -> u8 {
165 match self {
166 Self::Lease { .. } => PENDING_LEASE,
167 Self::Overage(_) => PENDING_OVERAGE,
168 }
169 }
170
171 /// The phase a commit against this receipt's own funding settles in.
172 ///
173 /// The commit-time fallback is the one transition that does *not* use
174 /// this: it names `COMMITTED_OVERAGE` explicitly, inside the `Lease` arm
175 /// that holds the receipt it refunds. Because that is the only other place
176 /// the constant appears, no expression in this module pairs "compare from
177 /// `PENDING_OVERAGE`" with "credit a lease", so `PENDING_OVERAGE ->
178 /// COMMITTED_LEASE` is unrepresentable rather than merely untested.
179 const fn committed_phase(&self) -> u8 {
180 match self {
181 Self::Lease { .. } => COMMITTED_LEASE,
182 Self::Overage(_) => COMMITTED_OVERAGE,
183 }
184 }
185}
186
187/// One request's debited-but-not-yet-committed units.
188///
189/// Created by [`Reservation::reserve`] or
190/// [`Reservation::reserve_overage`]; resolved by exactly one of
191/// [`commit_at_execution_start`](Reservation::commit_at_execution_start),
192/// [`cancel`](Reservation::cancel), or drop (which releases a pending
193/// reservation — INVARIANTS.md GL-2).
194///
195/// The charging rules are identical whichever funded it: zero charge before
196/// execution, full charge from execution start, and one compare-exchange
197/// deciding the commit/cancel race. Elastic mode changes *whether* a request
198/// is admitted, never how the units it consumes are accounted for.
199#[derive(Debug)]
200pub struct Reservation {
201 source: ChargeSource,
202 units: CostUnits,
203 phase: AtomicU8,
204}
205
206impl Reservation {
207 /// Debit `units` from `lease` and open a pending reservation.
208 ///
209 /// This is the quota step of the admission pipeline; it fails closed on
210 /// lease expiry or exhaustion and performs no I/O.
211 #[inline]
212 pub fn reserve(
213 lease: &Arc<LocalLease>,
214 units: CostUnits,
215 now: Timestamp,
216 ) -> Result<Reservation, DenyReason> {
217 Self::reserve_at_locality(Arc::clone(lease), units, now, Locality::current())
218 }
219
220 /// Reserve using locality already resolved by the enclosing admission
221 /// pipeline, avoiding repeated thread-local lookups between stages.
222 ///
223 /// Takes the handle **by value** because its caller already owns one:
224 /// `LeaseSlot::load_at` hands back an owned `Arc`, and borrowing it here
225 /// only to clone it again would put two refcount operations on the shared
226 /// lease back onto every admission — in the shipped `LocalSharding::SINGLE`
227 /// layout, on the one cache line every thread serving the account touches
228 /// (GL-79). This is the owned half of the same split `request_entry_from`
229 /// and `request_entry_at` draw one layer down, in the snapshot map.
230 ///
231 /// [`reserve`](Reservation::reserve) keeps the borrowing signature, and
232 /// pays the clone at its own call site rather than inside this one, so the
233 /// count is unchanged for every caller that holds only a borrow.
234 #[doc(hidden)]
235 #[inline]
236 pub fn reserve_at_locality(
237 lease: Arc<LocalLease>,
238 units: CostUnits,
239 now: Timestamp,
240 locality: Locality,
241 ) -> Result<Reservation, DenyReason> {
242 let debit = lease.try_reserve_at(units, now, locality)?;
243 Ok(Reservation {
244 source: ChargeSource::Lease { lease, debit },
245 units,
246 phase: AtomicU8::new(PENDING_LEASE),
247 })
248 }
249
250 /// Extend `units` of unfunded credit against `cap` and open a pending
251 /// reservation, for an account whose lease could not fund the quote.
252 ///
253 /// Takes no `now`, and the absence is the design rather than an omission.
254 /// A lease has a usability window because the allocator will reclaim and
255 /// re-grant its units, so work committed outside that window could never
256 /// be billed. Overage was never granted and is never reclaimed: there is
257 /// no window to race. Staleness and status are still enforced — by the
258 /// snapshot checks that run before this step, under every mode.
259 #[inline]
260 pub fn reserve_overage(
261 overage: &Arc<AccountOverage>,
262 units: CostUnits,
263 cap: CostUnits,
264 ) -> Result<Reservation, DenyReason> {
265 overage.try_debit(units, cap)?;
266 Ok(Reservation {
267 source: ChargeSource::Overage(Arc::clone(overage)),
268 units,
269 phase: AtomicU8::new(PENDING_OVERAGE),
270 })
271 }
272
273 #[must_use]
274 pub fn units(&self) -> CostUnits {
275 self.units
276 }
277
278 /// True when **admission** found no lease to fund these units.
279 ///
280 /// Reserve-time truth, and deliberately so: it decides the
281 /// `admitted_overage` qualifier at the stage that admitted the request
282 /// (INVARIANTS.md GL-20). It is *not* the billing statement — a leased
283 /// admission whose window lapsed at execution start settles against
284 /// overage without ever having been an overage admission. The billing
285 /// statement is [`UsageEvent::source`], which reads the terminal phase.
286 #[must_use]
287 pub fn admitted_as_overage(&self) -> bool {
288 matches!(self.source, ChargeSource::Overage(_))
289 }
290
291 fn account_id(&self) -> AccountId {
292 match &self.source {
293 ChargeSource::Lease { lease, .. } => lease.grant().account_id,
294 ChargeSource::Overage(overage) => overage.account_id(),
295 }
296 }
297
298 /// Whether the funding source's local usability window has lapsed.
299 ///
300 /// Only a lease has one. See [`reserve_overage`](Self::reserve_overage).
301 fn window_lapsed(&self, now: Timestamp) -> bool {
302 match &self.source {
303 ChargeSource::Lease { lease, .. } => now >= lease.usable_until(),
304 ChargeSource::Overage(_) => false,
305 }
306 }
307
308 /// Return the units to whichever counter they came from.
309 fn refund(&self) {
310 match &self.source {
311 ChargeSource::Lease { lease, debit } => lease.credit(debit),
312 ChargeSource::Overage(overage) => overage.credit(self.units),
313 }
314 }
315
316 /// Release for zero and report `reason`, or report whoever resolved the
317 /// reservation first.
318 #[cold]
319 fn release_for_zero(&self, reason: DenyReason) -> Result<CostUnits, CommitError> {
320 match self.phase.compare_exchange(
321 self.source.pending_phase(),
322 RELEASED,
323 Ordering::AcqRel,
324 Ordering::Acquire,
325 ) {
326 Ok(_) => {
327 self.refund();
328 Err(CommitError::Denied(reason))
329 }
330 // Someone else already resolved it; report that outcome.
331 Err(RELEASED) => Err(CommitError::AlreadyReleased),
332 Err(_) => Err(CommitError::AlreadyCommitted),
333 }
334 }
335
336 /// Commit the charge because execution is starting. From this point the
337 /// full quote stands regardless of how execution ends.
338 ///
339 /// Rechecks the lease's local usability window: a reservation opened just
340 /// before the window closed must not commit against that lease after it —
341 /// the allocator's reclaim grace only protects work committed *inside* the
342 /// window, and past it the capacity may be reclaimed and re-granted, so
343 /// the charge could never be billed.
344 ///
345 /// What a lapse then means is `funding`'s decision.
346 /// [`CommitFunding::LeaseOnly`] releases the units and reports
347 /// `FundingExpiredAtStart`; the caller must not execute.
348 /// [`CommitFunding::OverageFallback`] instead settles the same reservation
349 /// against overage — **one** transition `PENDING_LEASE ->
350 /// COMMITTED_OVERAGE`, never a release followed by a second reservation,
351 /// which would give a canceller one phase to win while the worker
352 /// committed another. If the overage debit itself is refused, the
353 /// reservation releases for zero and reports that refusal verbatim.
354 #[inline]
355 pub fn commit_at_execution_start(
356 &self,
357 now: Timestamp,
358 funding: CommitFunding<'_>,
359 ) -> Result<CostUnits, CommitError> {
360 if self.window_lapsed(now) {
361 return self.commit_after_lapse(funding);
362 }
363 let claim = || {
364 self.phase.compare_exchange(
365 self.source.pending_phase(),
366 self.source.committed_phase(),
367 Ordering::AcqRel,
368 Ordering::Acquire,
369 )
370 };
371 let transition = match &self.source {
372 // The pending overage units are already in `spent` and owned by
373 // this reservation, so publication must not credit them; cancel
374 // and drop remain their refund.
375 ChargeSource::Overage(overage) => overage.publish_claim(self.units, claim),
376 ChargeSource::Lease { .. } => claim(),
377 };
378 match transition {
379 Ok(_) => Ok(self.units),
380 Err(RELEASED) => Err(CommitError::AlreadyReleased),
381 Err(_) => Err(CommitError::AlreadyCommitted),
382 }
383 }
384
385 /// The lapsed-window branch: cold, and the only place a reservation's
386 /// funding source may change.
387 #[cold]
388 fn commit_after_lapse(&self, funding: CommitFunding<'_>) -> Result<CostUnits, CommitError> {
389 let (CommitFunding::OverageFallback { overage, cap }, ChargeSource::Lease { lease, debit }) =
390 (funding, &self.source)
391 else {
392 return self.release_for_zero(DenyReason::FundingExpiredAtStart);
393 };
394 debug_assert_eq!(
395 overage.account_id(),
396 self.account_id(),
397 "commit-time overage fallback must use the reservation's own account counter"
398 );
399 // A cheap probe that narrows the window in which a doomed reservation
400 // takes a debit it will immediately return. It does not close the
401 // race — a canceller can still win after this load — which is why the
402 // guard below, not this branch, is what guarantees the credit.
403 if self.phase.load(Ordering::Acquire) != PENDING_LEASE {
404 return self.release_for_zero(DenyReason::FundingExpiredAtStart);
405 }
406 // A refused debit claims nothing, so the reservation is simply
407 // released and the refusal reported with its own retry class.
408 let tentative = match overage.debit_tentatively(self.units, cap) {
409 Ok(tentative) => tentative,
410 Err(refused) => return self.release_for_zero(refused),
411 };
412 // The debit is funded *before* the claim, and consuming the guard is
413 // what enforces that order: a won claim can never name overage the
414 // account never recorded, which would break the ledger equation by
415 // exactly these units.
416 match tentative.publish_commit(|| {
417 self.phase.compare_exchange(
418 PENDING_LEASE,
419 COMMITTED_OVERAGE,
420 Ordering::AcqRel,
421 Ordering::Acquire,
422 )
423 }) {
424 // Won. The lease receipt is refunded only here, strictly after the
425 // claim: crediting it before would double-refund granted capacity
426 // alongside a canceller who won the race and refunded it too.
427 Ok(_) => {
428 lease.credit(debit);
429 Ok(self.units)
430 }
431 // Lost. The guard already credited the tentative overage on its
432 // way out and the winning canceller refunded the lease, so nothing
433 // is owed here.
434 Err(RELEASED) => Err(CommitError::AlreadyReleased),
435 Err(_) => Err(CommitError::AlreadyCommitted),
436 }
437 }
438
439 /// Cancel before execution if possible. Idempotent: cancelling an already
440 /// released reservation reports [`CancelOutcome::ZeroCharged`] without
441 /// crediting the lease a second time (the compare-exchange transitions at
442 /// most once).
443 #[inline]
444 pub fn cancel(&self) -> CancelOutcome {
445 match self.phase.compare_exchange(
446 self.source.pending_phase(),
447 RELEASED,
448 Ordering::AcqRel,
449 Ordering::Acquire,
450 ) {
451 Ok(_) => {
452 self.refund();
453 CancelOutcome::ZeroCharged
454 }
455 // Either committed phase means execution started. A leased
456 // reservation that settled against overage still charges its full
457 // quote — the fallback changed which counter funds it, not whether
458 // the work is billed.
459 Err(COMMITTED_LEASE | COMMITTED_OVERAGE) => {
460 CancelOutcome::AlreadyCommitted { units: self.units }
461 }
462 Err(_) => CancelOutcome::ZeroCharged,
463 }
464 }
465
466 /// The billing record, available only once committed. `request_id` is the
467 /// idempotency key (INVARIANTS.md GL-7); emitting the same event twice is
468 /// therefore harmless downstream.
469 /// The billing statement reads the *phase*, not the receipt, and that is
470 /// load-bearing rather than stylistic. A commit-time fallback happens
471 /// precisely because the lease's window lapsed, so by the time the event
472 /// flushes the allocator may already have reclaimed that lease — and the
473 /// sink rejects a `Leased` event naming a reclaimed lease, because the
474 /// reclaim credited its full remainder and the units would double-count.
475 /// Billing the fallback against its receipt would therefore silently drop
476 /// the charge for work that ran, on the exact path elastic mode exists to
477 /// serve. The terminal phase is the funding statement.
478 ///
479 /// `policy_revision` is the consuming application's identity for the
480 /// policy that priced this request (GL-94), taken from the pinned snapshot.
481 /// It is a parameter rather than reservation state on purpose: the
482 /// reservation is a request-path value and would carry 32 bytes for a
483 /// field only the commit reads, and passing it here keeps the event built
484 /// complete in one place instead of assembled and then patched.
485 /// `key_id` follows the same rule: the pinned snapshot's optional
486 /// credential identity reaches only committed events, without enlarging
487 /// the pending reservation. The store owns attribution and replay checks.
488 #[must_use]
489 pub fn usage_event(
490 &self,
491 request_id: RequestId,
492 now: Timestamp,
493 policy_revision: PolicyRevision,
494 key_id: Option<crate::KeyId>,
495 ) -> Option<UsageEvent> {
496 let source = match self.phase.load(Ordering::Acquire) {
497 // Both a natively admitted overage and a commit-time fallback.
498 COMMITTED_OVERAGE => UsageSource::Overage,
499 COMMITTED_LEASE => match &self.source {
500 ChargeSource::Lease { lease, .. } => {
501 let grant = lease.grant();
502 UsageSource::Leased {
503 lease_id: grant.lease_id,
504 fencing_token: grant.fencing_token,
505 }
506 }
507 // Unreachable: `COMMITTED_LEASE` is only ever written by
508 // `committed_phase()` on a `Lease` receipt. Billing against no
509 // capability at least preserves the charge; dropping the event
510 // would lose it outright.
511 ChargeSource::Overage(_) => {
512 debug_assert!(false, "a committed-lease phase requires a lease receipt");
513 UsageSource::Overage
514 }
515 },
516 _ => return None,
517 };
518 Some(UsageEvent::new(
519 request_id,
520 self.account_id(),
521 source,
522 self.units,
523 now,
524 policy_revision,
525 key_id,
526 ))
527 }
528}
529
530impl Drop for Reservation {
531 fn drop(&mut self) {
532 // An abandoned pending reservation charges zero: same transition as
533 // cancel(), so a reservation resolved earlier is untouched.
534 if self
535 .phase
536 .compare_exchange(
537 self.source.pending_phase(),
538 RELEASED,
539 Ordering::AcqRel,
540 Ordering::Acquire,
541 )
542 .is_ok()
543 {
544 self.refund();
545 }
546 }
547}
548
549/// A reservation shared between the thread that will execute the work and an
550/// asynchronous waiter that may cancel it first.
551///
552/// This is the one object GL-93 adds, and it exists because the consumer topology
553/// requires it: an executor races its timeout/disconnect path (on the async
554/// side) against its worker's actual execution start (on a pool thread), and
555/// both need to reach the same charge state. It replaces the
556/// `Cancellation + Arc<Mutex<ChargePhase>>` pairing an embedder would otherwise
557/// build, with no mutex to poison and nothing to lock during a panic.
558///
559/// The reservation's own compare-exchange remains the single authority for
560/// whether the request charged. The extra flag records only that cancellation
561/// was *requested*, which is a different question: once commit wins, the charge
562/// stands in full, and the worker may still want to know that nobody is waiting
563/// for the answer.
564#[derive(Debug)]
565pub struct SharedCharge {
566 reservation: Reservation,
567 cancel_requested: AtomicBool,
568}
569
570impl SharedCharge {
571 /// The reservation itself. Every resolution goes through this, so the
572 /// worker side and the handle side transition the same phase word.
573 #[inline]
574 #[must_use]
575 pub fn reservation(&self) -> &Reservation {
576 &self.reservation
577 }
578
579 /// Whether anyone has asked for this request to stop.
580 ///
581 /// Independent of whether the charge stands: a cancellation that arrives
582 /// after commit reports [`CancelOutcome::AlreadyCommitted`] and changes no
583 /// funding, but still sets this so a running kernel can choose to stop
584 /// computing work nobody is waiting for.
585 #[inline]
586 #[must_use]
587 pub fn cancel_requested(&self) -> bool {
588 self.cancel_requested.load(Ordering::Acquire)
589 }
590
591 /// Another handle to this same charge.
592 ///
593 /// Handles are interchangeable: each can cancel, and the first to win the
594 /// phase decides the outcome for all of them.
595 #[must_use]
596 pub fn cancel_handle(self: &Arc<Self>) -> CancelHandle {
597 CancelHandle(Arc::clone(self))
598 }
599}
600
601/// The asynchronous waiter's half of a [`SharedCharge`].
602///
603/// Holds no ability to commit — only to cancel and to observe. A cancel handle
604/// cannot start execution, and it deliberately cannot move the usage slot,
605/// concurrency guard, or capacity permit out of the worker's value: those are
606/// released exactly once, by the side that owns them, when it observes the
607/// cancellation and drops.
608#[derive(Debug, Clone)]
609pub struct CancelHandle(Arc<SharedCharge>);
610
611impl CancelHandle {
612 /// Ask for the request to stop, and learn whether it had already started.
613 ///
614 /// The request flag is set *before* the phase transition is attempted, so a
615 /// worker that wins the race and reads
616 /// [`cancel_requested`](SharedCharge::cancel_requested) still sees that a
617 /// cancellation happened. Ordering it the other way would let a committed
618 /// worker observe `false` for a cancellation that had already returned
619 /// `AlreadyCommitted` to its caller.
620 #[inline]
621 pub fn cancel(&self) -> CancelOutcome {
622 self.0.cancel_requested.store(true, Ordering::Release);
623 self.0.reservation.cancel()
624 }
625
626 /// Whether cancellation has been requested through this or any other
627 /// handle to the same charge.
628 #[inline]
629 #[must_use]
630 pub fn is_cancelled(&self) -> bool {
631 self.0.cancel_requested()
632 }
633}
634
635impl Reservation {
636 /// Move this reservation into a shared object and hand back a cancel
637 /// handle for the asynchronous side.
638 ///
639 /// This is the single allocation GL-93 permits, and it is opt-in: an inline
640 /// executor never calls it and its admission path stays allocation-free. A
641 /// consumer that needs a timeout/worker race pays one `Arc` for it; one
642 /// that moves an unsplit value to its worker has deliberately chosen to
643 /// have no external cancel race.
644 #[must_use]
645 pub fn split(self) -> (Arc<SharedCharge>, CancelHandle) {
646 let shared = Arc::new(SharedCharge {
647 reservation: self,
648 cancel_requested: AtomicBool::new(false),
649 });
650 let handle = CancelHandle(Arc::clone(&shared));
651 (shared, handle)
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::ids::{AccountId, FencingToken, LeaseId};
659 use crate::lease::LeaseGrant;
660 use crate::usage::UsageSource;
661
662 fn t(secs: i64) -> Timestamp {
663 Timestamp::from_second(secs).unwrap()
664 }
665
666 fn overage() -> Arc<AccountOverage> {
667 Arc::new(AccountOverage::new(AccountId(1)))
668 }
669
670 /// An overage charge bills like any other, and says so on the wire: the
671 /// event names the account and carries no lease capability, because there
672 /// is no lease to name.
673 #[test]
674 fn a_committed_overage_bills_against_no_lease() {
675 let o = overage();
676 let r = Reservation::reserve_overage(&o, CostUnits(30), CostUnits(100)).unwrap();
677 assert!(r.admitted_as_overage());
678 assert_eq!(o.spent(), CostUnits(30));
679 assert_eq!(
680 r.commit_at_execution_start(t(1), CommitFunding::LeaseOnly)
681 .unwrap(),
682 CostUnits(30)
683 );
684 let event = r
685 .usage_event(RequestId(7), t(1), PolicyRevision::UNSTATED, None)
686 .unwrap();
687 assert_eq!(event.account_id, AccountId(1));
688 assert_eq!(event.units, CostUnits(30));
689 assert_eq!(event.source, UsageSource::Overage);
690 assert_eq!(event.source.lease_id(), None);
691 assert_eq!(event.source.fencing_token(), None);
692 }
693
694 /// Zero charge before execution applies identically to credit: the units
695 /// go back to the counter they came from, not to some lease.
696 #[test]
697 fn cancelling_an_overage_returns_the_credit() {
698 let o = overage();
699 let r = Reservation::reserve_overage(&o, CostUnits(30), CostUnits(100)).unwrap();
700 assert_eq!(r.cancel(), CancelOutcome::ZeroCharged);
701 assert_eq!(o.spent(), CostUnits::ZERO);
702 assert_eq!(
703 r.usage_event(RequestId(7), t(1), PolicyRevision::UNSTATED, None),
704 None
705 );
706 }
707
708 #[test]
709 fn dropping_a_pending_overage_returns_the_credit() {
710 let o = overage();
711 drop(Reservation::reserve_overage(&o, CostUnits(30), CostUnits(100)).unwrap());
712 assert_eq!(o.spent(), CostUnits::ZERO);
713 }
714
715 /// A lease reservation cannot commit past its usability window because the
716 /// allocator may reclaim and re-grant those units. Overage was never
717 /// granted and is never reclaimed, so there is no window to race — and a
718 /// commit arbitrarily far past the timestamp it was reserved at still
719 /// stands.
720 #[test]
721 fn overage_has_no_usability_window_to_lapse() {
722 let leased = Reservation::reserve(&lease(100), CostUnits(30), t(0)).unwrap();
723 assert_eq!(
724 leased.commit_at_execution_start(t(1_000), CommitFunding::LeaseOnly),
725 Err(CommitError::Denied(DenyReason::FundingExpiredAtStart))
726 );
727
728 let o = overage();
729 let r = Reservation::reserve_overage(&o, CostUnits(30), CostUnits(100)).unwrap();
730 assert_eq!(
731 r.commit_at_execution_start(t(1_000_000), CommitFunding::LeaseOnly)
732 .unwrap(),
733 CostUnits(30)
734 );
735 assert_eq!(o.spent(), CostUnits(30), "a commit keeps the credit spent");
736 }
737
738 #[test]
739 fn an_overage_beyond_the_cap_is_refused_and_claims_nothing() {
740 let o = overage();
741 assert_eq!(
742 Reservation::reserve_overage(&o, CostUnits(101), CostUnits(100)).unwrap_err(),
743 DenyReason::OverageCapExhausted {
744 spent: CostUnits::ZERO,
745 overage_cap: CostUnits(100),
746 }
747 );
748 assert_eq!(o.spent(), CostUnits::ZERO);
749 }
750
751 fn lease(units: u64) -> Arc<LocalLease> {
752 Arc::new(LocalLease::new(
753 LeaseGrant {
754 lease_id: LeaseId(7),
755 account_id: AccountId(1),
756 fencing_token: FencingToken(3),
757 units: CostUnits(units),
758 expires_at: t(1_000),
759 },
760 CostUnits::ZERO,
761 ))
762 }
763
764 #[test]
765 fn commit_charges_and_keeps_units_spent() {
766 let l = lease(100);
767 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
768 assert_eq!(
769 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly),
770 Ok(CostUnits(30))
771 );
772 assert_eq!(l.remaining(), CostUnits(70));
773 assert!(
774 r.usage_event(RequestId(9), t(1), PolicyRevision::UNSTATED, None)
775 .is_some()
776 );
777 drop(r);
778 // Drop of a committed reservation must not refund.
779 assert_eq!(l.remaining(), CostUnits(70));
780 }
781
782 /// `units()` is how a caller learns what a pending reservation will charge
783 /// before deciding to commit it, and no test called it — it could report
784 /// zero for any reservation with the suite green (GL-43). A caller checking
785 /// the quote before execution would have been told everything is free.
786 ///
787 /// Asserted against what the reservation actually does with those units,
788 /// not just against the constructor argument: the debit taken at reserve,
789 /// the charge returned by commit, and the units billed on the usage event
790 /// must all be the number `units()` advertises.
791 #[test]
792 fn a_reservation_reports_the_units_it_will_charge() {
793 let l = lease(100);
794 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
795 assert_eq!(r.units(), CostUnits(30));
796 assert_eq!(
797 l.remaining(),
798 CostUnits(70),
799 "the pending debit is the advertised amount"
800 );
801
802 assert_eq!(
803 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly),
804 Ok(r.units())
805 );
806 assert_eq!(
807 r.usage_event(RequestId(1), t(1), PolicyRevision::UNSTATED, None)
808 .unwrap()
809 .units,
810 r.units(),
811 "and the billing event carries it too"
812 );
813 }
814
815 #[test]
816 fn cancel_charges_zero_and_refunds() {
817 let l = lease(100);
818 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
819 assert_eq!(r.cancel(), CancelOutcome::ZeroCharged);
820 assert_eq!(l.remaining(), CostUnits(100));
821 assert_eq!(
822 r.usage_event(RequestId(9), t(1), PolicyRevision::UNSTATED, None),
823 None
824 );
825 // Idempotent, and no double credit.
826 assert_eq!(r.cancel(), CancelOutcome::ZeroCharged);
827 assert_eq!(l.remaining(), CostUnits(100));
828 }
829
830 #[test]
831 fn drop_releases_pending() {
832 let l = lease(100);
833 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
834 assert_eq!(l.remaining(), CostUnits(70));
835 drop(r);
836 assert_eq!(l.remaining(), CostUnits(100));
837 }
838
839 #[test]
840 fn cancel_after_commit_reports_full_charge() {
841 let l = lease(100);
842 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
843 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
844 .unwrap();
845 assert_eq!(
846 r.cancel(),
847 CancelOutcome::AlreadyCommitted {
848 units: CostUnits(30)
849 }
850 );
851 assert_eq!(l.remaining(), CostUnits(70));
852 }
853
854 #[test]
855 fn commit_after_cancel_is_refused() {
856 let l = lease(100);
857 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
858 assert_eq!(r.cancel(), CancelOutcome::ZeroCharged);
859 assert_eq!(
860 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly),
861 Err(CommitError::AlreadyReleased)
862 );
863 assert_eq!(l.remaining(), CostUnits(100));
864 }
865
866 #[test]
867 fn double_commit_is_a_surfaced_error() {
868 let l = lease(100);
869 let r = Reservation::reserve(&l, CostUnits(30), t(0)).unwrap();
870 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
871 .unwrap();
872 assert_eq!(
873 r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly),
874 Err(CommitError::AlreadyCommitted)
875 );
876 }
877
878 /// Review finding GL-1 regression: a reservation opened inside the
879 /// usability window cannot commit after the window closes — it releases
880 /// for zero charge instead, so reclaimed-and-re-granted capacity can
881 /// never be double-worked.
882 #[test]
883 fn commit_after_window_closes_releases_for_zero() {
884 let l = lease(100); // usable until t(1_000) (no margin)
885 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
886 assert_eq!(l.remaining(), CostUnits(70));
887 // The window lapses between reserve and commit.
888 assert_eq!(
889 r.commit_at_execution_start(t(1_000), CommitFunding::LeaseOnly),
890 Err(CommitError::Denied(DenyReason::FundingExpiredAtStart))
891 );
892 // Units returned; no usage event can exist; later commit is refused.
893 assert_eq!(l.remaining(), CostUnits(100));
894 assert_eq!(
895 r.usage_event(RequestId(1), t(1_001), PolicyRevision::UNSTATED, None),
896 None
897 );
898 assert_eq!(
899 r.commit_at_execution_start(t(999), CommitFunding::LeaseOnly),
900 Err(CommitError::AlreadyReleased)
901 );
902 }
903
904 /// The commit-time elastic fallback, end to end: a lease that lapsed
905 /// between admission and execution start settles against overage instead
906 /// of refusing, and the resulting bill names *no lease capability*.
907 ///
908 /// The missing capability is the point, not a detail. The sink rejects a
909 /// leased event whose lease has been reclaimed — and this lease lapsed, so
910 /// reclaim is exactly what happens next. Billing against the receipt would
911 /// silently drop the charge for work that ran.
912 #[test]
913 fn an_elastic_lapse_at_execution_start_bills_as_overage_with_no_lease_capability() {
914 let l = lease(100);
915 let o = overage();
916 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
917 assert!(
918 !r.admitted_as_overage(),
919 "admission was funded by the lease"
920 );
921 assert_eq!(l.remaining(), CostUnits(70));
922
923 let funding = CommitFunding::OverageFallback {
924 overage: &o,
925 cap: CostUnits(100),
926 };
927 assert_eq!(
928 r.commit_at_execution_start(t(1_000), funding),
929 Ok(CostUnits(30))
930 );
931
932 // One funding term, not two: the lease receipt came back whole and the
933 // overage counter holds the charge as irrevocable committed occupancy.
934 assert_eq!(l.remaining(), CostUnits(100));
935 assert_eq!(o.spent(), CostUnits(30));
936
937 let event = r
938 .usage_event(RequestId(7), t(1_000), PolicyRevision::UNSTATED, None)
939 .unwrap();
940 assert_eq!(event.units, CostUnits(30));
941 assert_eq!(event.account_id, AccountId(1));
942 assert_eq!(event.source, UsageSource::Overage);
943 assert_eq!(event.source.lease_id(), None);
944 assert_eq!(event.source.fencing_token(), None);
945
946 // Reserve-time truth is unchanged by how the request settled.
947 assert!(!r.admitted_as_overage());
948 }
949
950 /// The same lapse under `Strict` charges zero and forbids execution. This
951 /// is the pair to the test above: one enforcement mode, one outcome.
952 #[test]
953 fn a_strict_lapse_at_execution_start_releases_for_zero_and_yields_no_event() {
954 let l = lease(100);
955 let o = overage();
956 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
957
958 assert_eq!(
959 r.commit_at_execution_start(t(1_000), CommitFunding::LeaseOnly),
960 Err(CommitError::Denied(DenyReason::FundingExpiredAtStart))
961 );
962 assert_eq!(l.remaining(), CostUnits(100));
963 assert_eq!(
964 r.usage_event(RequestId(7), t(1_000), PolicyRevision::UNSTATED, None),
965 None
966 );
967 // Strict took no overage: the counter was never touched.
968 assert_eq!(o.spent(), CostUnits::ZERO);
969 }
970
971 /// A fallback that cannot fit inside the cap releases the lease for zero
972 /// and reports the refusal *verbatim*, keeping its own retry class.
973 #[test]
974 fn an_elastic_lapse_with_no_committed_headroom_releases_the_lease_for_zero() {
975 let l = lease(100);
976 let o = overage();
977 // Spend the whole cap irrevocably, so no refund could make room.
978 let held = Reservation::reserve_overage(&o, CostUnits(100), CostUnits(100)).unwrap();
979 held.commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
980 .unwrap();
981
982 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
983 let funding = CommitFunding::OverageFallback {
984 overage: &o,
985 cap: CostUnits(100),
986 };
987 assert_eq!(
988 r.commit_at_execution_start(t(1_000), funding),
989 Err(CommitError::Denied(DenyReason::OverageCapExhausted {
990 spent: CostUnits(100),
991 overage_cap: CostUnits(100),
992 }))
993 );
994 // Released for zero, and the refused debit claimed nothing.
995 assert_eq!(l.remaining(), CostUnits(100));
996 assert_eq!(o.spent(), CostUnits(100));
997 assert_eq!(
998 r.usage_event(RequestId(7), t(1_000), PolicyRevision::UNSTATED, None),
999 None
1000 );
1001 }
1002
1003 /// When the cap is occupied by a *refundable* sibling, the refusal is the
1004 /// transient one: a later cancel really can make this request fit.
1005 #[test]
1006 fn an_elastic_lapse_blocked_by_refundable_occupancy_is_temporarily_exhausted() {
1007 let l = lease(100);
1008 let o = overage();
1009 // Pending, therefore still refundable.
1010 let _sibling = Reservation::reserve_overage(&o, CostUnits(100), CostUnits(100)).unwrap();
1011
1012 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
1013 let funding = CommitFunding::OverageFallback {
1014 overage: &o,
1015 cap: CostUnits(100),
1016 };
1017 let denied = r.commit_at_execution_start(t(1_000), funding).unwrap_err();
1018 assert_eq!(
1019 denied,
1020 CommitError::Denied(DenyReason::OverageCapTemporarilyExhausted {
1021 spent: CostUnits(100),
1022 overage_cap: CostUnits(100),
1023 })
1024 );
1025 let CommitError::Denied(reason) = denied else {
1026 unreachable!("the fallback reports a classified denial")
1027 };
1028 assert_eq!(reason.retry(), crate::deny::Retry::Transient);
1029 assert_eq!(l.remaining(), CostUnits(100));
1030 }
1031
1032 /// The third refusal `try_debit` can produce, which the fallback must
1033 /// report rather than collapse into one of the other two: those are
1034 /// `Transient`, this is `AfterInFlight`, and telling a caller to retry
1035 /// immediately against units that are already irrevocable is exactly the
1036 /// lie the classifier exists to prevent.
1037 #[test]
1038 fn an_elastic_lapse_overlapping_a_sibling_publication_reports_an_in_flight_commit() {
1039 let l = lease(100);
1040 let o = overage();
1041 // The sibling holds the whole cap, so the fallback's own debit cannot
1042 // fit; overlapping its publication is what makes the refusal
1043 // `AfterInFlight` rather than one of the stable-occupancy reasons.
1044 let sibling = Reservation::reserve_overage(&o, CostUnits(100), CostUnits(100)).unwrap();
1045 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
1046
1047 // Drive a sibling publication and attempt the fallback from inside it,
1048 // exactly as `overage_commit_publication_never_looks_refundable` does.
1049 o.publish_claim(sibling.units, || {
1050 let claimed = sibling.phase.compare_exchange(
1051 PENDING_OVERAGE,
1052 COMMITTED_OVERAGE,
1053 Ordering::AcqRel,
1054 Ordering::Acquire,
1055 );
1056 assert!(claimed.is_ok(), "the sibling wins its own phase claim");
1057
1058 let funding = CommitFunding::OverageFallback {
1059 overage: &o,
1060 cap: CostUnits(100),
1061 };
1062 let denied = r.commit_at_execution_start(t(1_000), funding).unwrap_err();
1063 let CommitError::Denied(reason) = denied else {
1064 unreachable!("the fallback reports a classified denial")
1065 };
1066 assert!(
1067 matches!(reason, DenyReason::OverageCommitInProgress { .. }),
1068 "expected an in-flight publication refusal, got {reason:?}"
1069 );
1070 assert_eq!(reason.retry(), crate::deny::Retry::AfterInFlight);
1071 Ok::<_, ()>(())
1072 })
1073 .unwrap();
1074
1075 // The refused fallback released its lease for zero and claimed nothing.
1076 assert_eq!(l.remaining(), CostUnits(100));
1077 assert_eq!(o.spent(), CostUnits(100));
1078 }
1079
1080 /// The lease receipt is refunded exactly once by a winning fallback. A
1081 /// late cancel and the eventual drop must both find nothing left to do.
1082 #[test]
1083 fn a_fallback_commit_refunds_its_lease_exactly_once() {
1084 let l = lease(100);
1085 let o = overage();
1086 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
1087 let funding = CommitFunding::OverageFallback {
1088 overage: &o,
1089 cap: CostUnits(100),
1090 };
1091 r.commit_at_execution_start(t(1_000), funding).unwrap();
1092 assert_eq!(l.remaining(), CostUnits(100));
1093
1094 assert_eq!(
1095 r.cancel(),
1096 CancelOutcome::AlreadyCommitted {
1097 units: CostUnits(30)
1098 }
1099 );
1100 drop(r);
1101 assert_eq!(l.remaining(), CostUnits(100));
1102 assert_eq!(o.spent(), CostUnits(30));
1103 }
1104
1105 /// A fallback that loses the race to a canceller must strand no overage
1106 /// capacity: the tentative debit is revocable until the claim resolves,
1107 /// and the full cap must be reusable afterwards.
1108 #[test]
1109 fn a_fallback_that_loses_to_cancel_strands_no_overage_capacity() {
1110 let l = lease(100);
1111 let o = overage();
1112 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
1113
1114 // The canceller wins first, deterministically.
1115 assert_eq!(r.cancel(), CancelOutcome::ZeroCharged);
1116 assert_eq!(l.remaining(), CostUnits(100));
1117
1118 let funding = CommitFunding::OverageFallback {
1119 overage: &o,
1120 cap: CostUnits(100),
1121 };
1122 assert_eq!(
1123 r.commit_at_execution_start(t(1_000), funding),
1124 Err(CommitError::AlreadyReleased)
1125 );
1126 // Nothing stranded: the whole cap is still available.
1127 assert_eq!(o.spent(), CostUnits::ZERO);
1128 let fresh = Reservation::reserve_overage(&o, CostUnits(100), CostUnits(100));
1129 assert!(fresh.is_ok(), "the full cap must remain reusable");
1130 // And the lease was credited once, by the canceller alone.
1131 assert_eq!(l.remaining(), CostUnits(100));
1132 }
1133
1134 /// Overage has no usability window, so a natively admitted overage
1135 /// reservation never reaches the fallback and never takes a second debit.
1136 #[test]
1137 fn a_native_overage_reservation_ignores_a_commit_time_fallback() {
1138 let o = overage();
1139 let r = Reservation::reserve_overage(&o, CostUnits(30), CostUnits(100)).unwrap();
1140 assert_eq!(o.spent(), CostUnits(30));
1141
1142 let funding = CommitFunding::OverageFallback {
1143 overage: &o,
1144 cap: CostUnits(100),
1145 };
1146 assert_eq!(
1147 r.commit_at_execution_start(t(9_999), funding),
1148 Ok(CostUnits(30))
1149 );
1150 // One debit, taken at admission — not a second one at commit.
1151 assert_eq!(o.spent(), CostUnits(30));
1152 assert_eq!(
1153 r.usage_event(RequestId(1), t(9_999), PolicyRevision::UNSTATED, None)
1154 .unwrap()
1155 .source,
1156 UsageSource::Overage
1157 );
1158 }
1159
1160 /// Committing twice remains a surfaced programming error after a fallback,
1161 /// not a second charge.
1162 #[test]
1163 fn a_second_commit_after_a_fallback_is_a_surfaced_error() {
1164 let l = lease(100);
1165 let o = overage();
1166 let r = Reservation::reserve(&l, CostUnits(30), t(999)).unwrap();
1167 let funding = CommitFunding::OverageFallback {
1168 overage: &o,
1169 cap: CostUnits(100),
1170 };
1171 r.commit_at_execution_start(t(1_000), funding).unwrap();
1172 assert_eq!(
1173 r.commit_at_execution_start(t(1_000), funding),
1174 Err(CommitError::AlreadyCommitted)
1175 );
1176 assert_eq!(o.spent(), CostUnits(30));
1177 assert_eq!(l.remaining(), CostUnits(100));
1178 }
1179
1180 /// The safety margin closes the window early: commits stop at
1181 /// `expires_at - margin`, not at `expires_at`.
1182 #[test]
1183 fn safety_margin_closes_window_before_expiry() {
1184 let l = Arc::new(LocalLease::with_safety_margin(
1185 LeaseGrant {
1186 lease_id: LeaseId(7),
1187 account_id: AccountId(1),
1188 fencing_token: FencingToken(3),
1189 units: CostUnits(100),
1190 expires_at: t(1_000),
1191 },
1192 CostUnits::ZERO,
1193 jiff::SignedDuration::from_secs(10),
1194 ));
1195 assert_eq!(l.usable_until(), t(990));
1196 // Debits stop at the margin boundary too.
1197 assert_eq!(
1198 Reservation::reserve(&l, CostUnits(1), t(990)).unwrap_err(),
1199 DenyReason::LeaseExpired
1200 );
1201 let r = Reservation::reserve(&l, CostUnits(1), t(989)).unwrap();
1202 assert_eq!(
1203 r.commit_at_execution_start(t(990), CommitFunding::LeaseOnly),
1204 Err(CommitError::Denied(DenyReason::FundingExpiredAtStart))
1205 );
1206 }
1207
1208 /// INVARIANTS.md GL-3: commit and cancel race — exactly one wins, and the
1209 /// lease balance reflects the winner.
1210 #[test]
1211 fn commit_cancel_race_one_winner() {
1212 for _ in 0..500 {
1213 let l = lease(100);
1214 let r = Arc::new(Reservation::reserve(&l, CostUnits(10), t(0)).unwrap());
1215 let rc = Arc::clone(&r);
1216 let committer = std::thread::spawn(move || {
1217 rc.commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
1218 });
1219 let canceller = std::thread::spawn({
1220 let rc = Arc::clone(&r);
1221 move || rc.cancel()
1222 });
1223 let commit = committer.join().unwrap();
1224 let cancel = canceller.join().unwrap();
1225 match (commit, cancel) {
1226 (Ok(units), CancelOutcome::AlreadyCommitted { units: seen }) => {
1227 assert_eq!(units, seen);
1228 assert_eq!(l.remaining(), CostUnits(90));
1229 }
1230 (Err(CommitError::AlreadyReleased), CancelOutcome::ZeroCharged) => {
1231 assert_eq!(l.remaining(), CostUnits(100));
1232 }
1233 other => panic!("impossible race outcome: {other:?}"),
1234 }
1235 }
1236 }
1237
1238 /// The fallback races a canceller for the same single phase, so exactly
1239 /// one funding term survives. This is the property the "one transition,
1240 /// not a second reservation" rule exists for: releasing and re-reserving
1241 /// would give the canceller one phase to win while the worker committed
1242 /// another, and both could report success.
1243 #[test]
1244 fn fallback_commit_and_cancel_leave_exactly_one_funding_term() {
1245 for _ in 0..500 {
1246 let l = lease(100);
1247 let o = overage();
1248 let r = Arc::new(Reservation::reserve(&l, CostUnits(10), t(999)).unwrap());
1249 let committer = std::thread::spawn({
1250 let rc = Arc::clone(&r);
1251 let oc = Arc::clone(&o);
1252 move || {
1253 rc.commit_at_execution_start(
1254 t(1_000),
1255 CommitFunding::OverageFallback {
1256 overage: &oc,
1257 cap: CostUnits(100),
1258 },
1259 )
1260 }
1261 });
1262 let canceller = std::thread::spawn({
1263 let rc = Arc::clone(&r);
1264 move || rc.cancel()
1265 });
1266 let commit = committer.join().unwrap();
1267 let cancel = canceller.join().unwrap();
1268 match (commit, cancel) {
1269 // The worker won: the charge is funded by overage alone, and
1270 // the lease receipt came back whole.
1271 (Ok(units), CancelOutcome::AlreadyCommitted { units: seen }) => {
1272 assert_eq!(units, seen);
1273 assert_eq!(l.remaining(), CostUnits(100));
1274 assert_eq!(o.spent(), CostUnits(10));
1275 assert_eq!(
1276 r.usage_event(RequestId(1), t(1_000), PolicyRevision::UNSTATED, None)
1277 .unwrap()
1278 .source,
1279 UsageSource::Overage
1280 );
1281 }
1282 // The canceller won: zero charge, and — the part the guard
1283 // buys — the tentative debit left nothing behind.
1284 (Err(CommitError::AlreadyReleased), CancelOutcome::ZeroCharged) => {
1285 assert_eq!(l.remaining(), CostUnits(100));
1286 assert_eq!(o.spent(), CostUnits::ZERO);
1287 assert_eq!(
1288 r.usage_event(RequestId(1), t(1_000), PolicyRevision::UNSTATED, None),
1289 None
1290 );
1291 }
1292 other => panic!("impossible race outcome: {other:?}"),
1293 }
1294 }
1295 }
1296
1297 /// Concurrent fallbacks are still bounded by the cap, because the debit
1298 /// that funds each one goes through the same compare-exchange every other
1299 /// overage claim uses.
1300 #[test]
1301 fn concurrent_fallbacks_never_exceed_the_overage_cap() {
1302 let o = overage();
1303 // Ten workers, ten units each, but only room for six.
1304 let cap = CostUnits(60);
1305 let reservations: Vec<_> = (0..10)
1306 .map(|_| {
1307 let l = lease(100);
1308 let r = Reservation::reserve(&l, CostUnits(10), t(999)).unwrap();
1309 (l, r)
1310 })
1311 .collect();
1312
1313 let committed = std::thread::scope(|scope| {
1314 let handles: Vec<_> = reservations
1315 .iter()
1316 .map(|(_, r)| {
1317 let oc = Arc::clone(&o);
1318 scope.spawn(move || {
1319 r.commit_at_execution_start(
1320 t(1_000),
1321 CommitFunding::OverageFallback { overage: &oc, cap },
1322 )
1323 .is_ok()
1324 })
1325 })
1326 .collect();
1327 handles
1328 .into_iter()
1329 .map(|handle| handle.join().expect("no worker panics"))
1330 .filter(|won| *won)
1331 .count()
1332 });
1333
1334 assert!(o.spent() <= cap, "overage spend exceeded its cap");
1335 assert_eq!(o.spent(), CostUnits(committed as u64 * 10));
1336 assert!(committed <= 6, "at most six ten-unit charges fit in sixty");
1337 // Every loser released its lease for zero; every winner returned it.
1338 for (l, _) in &reservations {
1339 assert_eq!(l.remaining(), CostUnits(100));
1340 }
1341 }
1342
1343 /// The shared-charge race, from the consumer's angle: an asynchronous
1344 /// waiter cancels while a worker thread commits. One phase, one winner,
1345 /// and the funding follows the winner.
1346 #[test]
1347 fn split_commit_and_handle_cancel_have_exactly_one_winner() {
1348 for _ in 0..500 {
1349 let l = lease(100);
1350 let (shared, handle) = Reservation::reserve(&l, CostUnits(10), t(0))
1351 .unwrap()
1352 .split();
1353 let worker = std::thread::spawn({
1354 let shared = Arc::clone(&shared);
1355 move || {
1356 shared
1357 .reservation()
1358 .commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
1359 }
1360 });
1361 let waiter = std::thread::spawn(move || handle.cancel());
1362 let commit = worker.join().unwrap();
1363 let cancel = waiter.join().unwrap();
1364 match (commit, cancel) {
1365 (Ok(units), CancelOutcome::AlreadyCommitted { units: seen }) => {
1366 assert_eq!(units, seen);
1367 assert_eq!(l.remaining(), CostUnits(90));
1368 }
1369 (Err(CommitError::AlreadyReleased), CancelOutcome::ZeroCharged) => {
1370 assert_eq!(l.remaining(), CostUnits(100));
1371 }
1372 other => panic!("impossible race outcome: {other:?}"),
1373 }
1374 // Either way the cancellation was *requested*, which is a separate
1375 // fact from whether it changed the funding.
1376 assert!(shared.cancel_requested());
1377 }
1378 }
1379
1380 /// A cancellation that arrives before the worker starts charges zero, and
1381 /// the units go back to the lease immediately.
1382 #[test]
1383 fn a_cancel_handle_before_worker_start_charges_zero() {
1384 let l = lease(100);
1385 let (shared, handle) = Reservation::reserve(&l, CostUnits(30), t(0))
1386 .unwrap()
1387 .split();
1388 assert!(!handle.is_cancelled());
1389 assert_eq!(l.remaining(), CostUnits(70));
1390
1391 assert_eq!(handle.cancel(), CancelOutcome::ZeroCharged);
1392 assert!(handle.is_cancelled());
1393 assert_eq!(l.remaining(), CostUnits(100));
1394 assert_eq!(
1395 shared
1396 .reservation()
1397 .commit_at_execution_start(t(0), CommitFunding::LeaseOnly),
1398 Err(CommitError::AlreadyReleased),
1399 "the worker must not execute after a winning cancellation"
1400 );
1401 assert_eq!(
1402 shared
1403 .reservation()
1404 .usage_event(RequestId(1), t(0), PolicyRevision::UNSTATED, None),
1405 None
1406 );
1407 }
1408
1409 /// Once the worker has started, a late cancellation reports the full
1410 /// charge and refunds nothing — but the *request* is still recorded, so a
1411 /// running kernel can see that nobody is waiting for its answer.
1412 #[test]
1413 fn a_late_cancel_reports_the_full_charge_and_still_records_the_request() {
1414 let l = lease(100);
1415 let (shared, handle) = Reservation::reserve(&l, CostUnits(30), t(0))
1416 .unwrap()
1417 .split();
1418 shared
1419 .reservation()
1420 .commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
1421 .unwrap();
1422
1423 assert_eq!(
1424 handle.cancel(),
1425 CancelOutcome::AlreadyCommitted {
1426 units: CostUnits(30)
1427 }
1428 );
1429 assert_eq!(l.remaining(), CostUnits(70), "the charge stands in full");
1430 assert!(
1431 shared.cancel_requested(),
1432 "a kernel may still learn its caller has gone"
1433 );
1434 assert!(
1435 shared
1436 .reservation()
1437 .usage_event(RequestId(1), t(0), PolicyRevision::UNSTATED, None)
1438 .is_some()
1439 );
1440 }
1441
1442 /// Every handle to one charge resolves the same phase, so a second handle
1443 /// cannot produce a second outcome.
1444 #[test]
1445 fn a_second_cancel_handle_resolves_the_same_charge() {
1446 let l = lease(100);
1447 let (shared, first) = Reservation::reserve(&l, CostUnits(30), t(0))
1448 .unwrap()
1449 .split();
1450 let second = shared.cancel_handle();
1451
1452 assert_eq!(first.cancel(), CancelOutcome::ZeroCharged);
1453 assert_eq!(second.cancel(), CancelOutcome::ZeroCharged);
1454 assert!(second.is_cancelled());
1455 // One refund, not two.
1456 assert_eq!(l.remaining(), CostUnits(100));
1457 }
1458
1459 /// The commit-time elastic fallback composes with the shared cancel: the
1460 /// two race for the same phase, and the guard's revocable debit means a
1461 /// losing fallback strands nothing.
1462 #[test]
1463 fn a_shared_fallback_and_a_handle_cancel_leave_one_funding_term() {
1464 for _ in 0..200 {
1465 let l = lease(100);
1466 let o = overage();
1467 let (shared, handle) = Reservation::reserve(&l, CostUnits(10), t(999))
1468 .unwrap()
1469 .split();
1470 let worker = std::thread::spawn({
1471 let shared = Arc::clone(&shared);
1472 let o = Arc::clone(&o);
1473 move || {
1474 shared.reservation().commit_at_execution_start(
1475 t(1_000),
1476 CommitFunding::OverageFallback {
1477 overage: &o,
1478 cap: CostUnits(100),
1479 },
1480 )
1481 }
1482 });
1483 let waiter = std::thread::spawn(move || handle.cancel());
1484 let commit = worker.join().unwrap();
1485 let cancel = waiter.join().unwrap();
1486 match (commit, cancel) {
1487 (Ok(_), CancelOutcome::AlreadyCommitted { .. }) => {
1488 assert_eq!(l.remaining(), CostUnits(100));
1489 assert_eq!(o.spent(), CostUnits(10));
1490 }
1491 (Err(CommitError::AlreadyReleased), CancelOutcome::ZeroCharged) => {
1492 assert_eq!(l.remaining(), CostUnits(100));
1493 assert_eq!(o.spent(), CostUnits::ZERO);
1494 }
1495 other => panic!("impossible race outcome: {other:?}"),
1496 }
1497 }
1498 }
1499
1500 /// The overage counter has a second transition to publish after the
1501 /// reservation CAS: committed occupancy. Once both racers return, the
1502 /// winning phase and the local occupancy reason must agree exactly —
1503 /// cancellation restores headroom, while commit leaves stable saturation.
1504 #[test]
1505 fn overage_commit_cancel_race_preserves_retry_classification() {
1506 for _ in 0..500 {
1507 let o = overage();
1508 let r =
1509 Arc::new(Reservation::reserve_overage(&o, CostUnits(10), CostUnits(10)).unwrap());
1510 let committer = std::thread::spawn({
1511 let r = Arc::clone(&r);
1512 move || r.commit_at_execution_start(t(0), CommitFunding::LeaseOnly)
1513 });
1514 let canceller = std::thread::spawn({
1515 let r = Arc::clone(&r);
1516 move || r.cancel()
1517 });
1518
1519 match (committer.join().unwrap(), canceller.join().unwrap()) {
1520 (Ok(units), CancelOutcome::AlreadyCommitted { units: seen }) => {
1521 assert_eq!(units, seen);
1522 assert_eq!(o.spent(), CostUnits(10));
1523 let denied =
1524 Reservation::reserve_overage(&o, CostUnits(1), CostUnits(10)).unwrap_err();
1525 assert!(matches!(denied, DenyReason::OverageCapExhausted { .. }));
1526 assert_eq!(denied.retry(), crate::deny::Retry::Transient);
1527 }
1528 (Err(CommitError::AlreadyReleased), CancelOutcome::ZeroCharged) => {
1529 assert_eq!(o.spent(), CostUnits::ZERO);
1530 drop(
1531 Reservation::reserve_overage(&o, CostUnits(10), CostUnits(10))
1532 .expect("cancelled credit is immediately reusable"),
1533 );
1534 }
1535 other => panic!("impossible overage race outcome: {other:?}"),
1536 }
1537 }
1538 }
1539
1540 /// A commit has become irrevocable once it wins the phase transition. A
1541 /// cap observer must never describe those units as refundable while the
1542 /// committed-occupancy publication is still catching up.
1543 #[test]
1544 fn overage_commit_publication_never_looks_refundable() {
1545 let overage = overage();
1546 let reservation =
1547 Reservation::reserve_overage(&overage, CostUnits(100), CostUnits(100)).unwrap();
1548
1549 overage
1550 .publish_claim(reservation.units, || {
1551 let claimed = reservation.phase.compare_exchange(
1552 PENDING_OVERAGE,
1553 COMMITTED_OVERAGE,
1554 Ordering::AcqRel,
1555 Ordering::Acquire,
1556 );
1557 assert!(claimed.is_ok(), "this commit wins the phase claim");
1558 assert_eq!(
1559 {
1560 let denied =
1561 Reservation::reserve_overage(&overage, CostUnits(1), CostUnits(100))
1562 .unwrap_err();
1563 assert_eq!(denied.retry(), crate::deny::Retry::AfterInFlight);
1564 denied
1565 },
1566 DenyReason::OverageCommitInProgress {
1567 spent: CostUnits(100),
1568 overage_cap: CostUnits(100),
1569 },
1570 "an irrevocable commit is identified as an in-flight publication"
1571 );
1572 claimed
1573 })
1574 .unwrap();
1575
1576 assert!(matches!(
1577 Reservation::reserve_overage(&overage, CostUnits(1), CostUnits(100)).unwrap_err(),
1578 DenyReason::OverageCapExhausted { .. }
1579 ));
1580 }
1581}