Skip to main content

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}