tollgate_core/lease.rs
1//! Local quota leases: centrally allocated capacity, locally decremented.
2//!
3//! A [`LeaseGrant`] is what an allocator (the store or the quota server)
4//! returns after atomically debiting an account's balance. A [`LocalLease`]
5//! is the instance-side runtime form: one atomic counter by default, or an
6//! explicitly configured set of cache-isolated counters. Requests reserve
7//! units with CAS loops — no lock, no I/O — which is how one
8//! database transaction amortizes across thousands of requests. Central
9//! allocation bounds spend; the grant's lease-scoped capability prevents
10//! release or usage from being attributed to a different lease
11//! (INVARIANTS.md GL-1, GL-4).
12
13use std::mem::ManuallyDrop;
14use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
15use std::sync::{Arc, OnceLock};
16
17use jiff::Timestamp;
18
19use crate::deny::DenyReason;
20use crate::ids::{AccountId, FencingToken, LeaseId};
21use crate::sharding::{LocalSharding, Locality};
22use crate::units::CostUnits;
23
24/// An allocator's record of one lease: `units` were debited from
25/// `account_id`'s balance and belong exclusively to the holder until
26/// `expires_at`, after which the allocator reclaims whatever the holder did
27/// not spend (INVARIANTS.md GL-9).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct LeaseGrant {
31 /// The allocator's identity for this lease record.
32 pub lease_id: LeaseId,
33 /// The account whose balance funded the lease.
34 pub account_id: AccountId,
35 /// Capability token for this lease record, not an account-wide epoch.
36 pub fencing_token: FencingToken,
37 /// Units debited from the account at grant and spendable by the holder.
38 pub units: CostUnits,
39 /// When the allocator's grant ends. The holder stops spending earlier,
40 /// at `expires_at` minus its safety margin (see [`LocalLease::usable_until`]).
41 pub expires_at: Timestamp,
42}
43
44/// An account's unfunded spend on this instance, under
45/// [`EnforcementMode::Elastic`].
46///
47/// Three atomic counters, with the total shaped exactly like [`LocalLease`]'s:
48/// a CAS loop, no lock, no I/O, no clock. A lease counts *down* through units
49/// someone already paid for; `spent` counts *up* through units nobody has.
50/// `committed` distinguishes durable spend from pending reservations that can
51/// still be refunded. `commit_publications` closes the otherwise torn
52/// reservation-phase/occupancy transition, so a cap refusal never advertises
53/// irrevocable units as refundable. The ledger settles the difference by
54/// treating overage as a second funding term, so per-account conservation
55/// still closes exactly (INVARIANTS.md GL-1, GL-3).
56///
57/// **The cap is a parameter, not a field.** It arrives from the snapshot the
58/// request has already read, which means two things: a republished cap takes
59/// effect on the very next request with no reconciliation step, and — the
60/// load-bearing half — the cap comparison happens *inside* the same
61/// compare-exchange that claims the units. Checking a cap and then claiming
62/// against it in two steps would let two cores each observe room for a request
63/// that only one of them can have.
64///
65/// **Its lifetime is the account's, not a lease's.** It hangs off the
66/// per-account lease slot, which is created on first use and held for the
67/// life of the process. That is deliberate and is the difference between this
68/// and the rate-limiter registry: a limiter rebuilt after eviction costs a
69/// full bucket, but an overage counter rebuilt after eviction silently resets
70/// a spend cap.
71///
72/// [`EnforcementMode::Elastic`]: crate::snapshot::EnforcementMode::Elastic
73#[derive(Debug)]
74pub struct AccountOverage {
75 account_id: AccountId,
76 spent: AtomicU64,
77 committed: AtomicU64,
78 commit_publications: AtomicUsize,
79 /// Debit compare-exchanges on `spent` that lost to another writer (GL-139).
80 /// Beside `spent`, so the only thread that writes it is one already
81 /// contending for that line; added once per contended debit.
82 contended: AtomicU64,
83}
84
85/// Evidence that an overage commit publication is visible to cap observers.
86///
87/// The only way to publish committed occupancy is through this guard. Its
88/// lifetime surrounds the reservation's phase CAS and the occupancy update;
89/// dropping it is the release publication that makes the stable counters
90/// observable again.
91struct OverageCommitPublication<'a> {
92 overage: &'a AccountOverage,
93}
94
95impl OverageCommitPublication<'_> {
96 fn publish(self, units: CostUnits) {
97 let prior = self
98 .overage
99 .committed
100 .fetch_add(units.get(), Ordering::AcqRel);
101 debug_assert!(
102 prior
103 .checked_add(units.get())
104 .is_some_and(|committed| committed <= self.overage.spent.load(Ordering::Acquire)),
105 "committed overage exceeds total recorded spend"
106 );
107 }
108}
109
110impl Drop for OverageCommitPublication<'_> {
111 fn drop(&mut self) {
112 let prior = self
113 .overage
114 .commit_publications
115 .fetch_sub(1, Ordering::AcqRel);
116 debug_assert!(prior > 0, "overage commit publication count underflowed");
117 }
118}
119
120impl AccountOverage {
121 /// An empty overage counter for one account on this instance: nothing
122 /// extended, nothing committed.
123 #[must_use]
124 pub fn new(account_id: AccountId) -> Self {
125 AccountOverage {
126 account_id,
127 spent: AtomicU64::new(0),
128 committed: AtomicU64::new(0),
129 commit_publications: AtomicUsize::new(0),
130 contended: AtomicU64::new(0),
131 }
132 }
133
134 /// Overage debits that lost a compare-exchange to another writer, since
135 /// the account's counter was created. A lower bound, for the reasons
136 /// [`LocalLease::contended_debits`] gives. A control-plane read.
137 #[must_use]
138 pub fn contended_debits(&self) -> u64 {
139 self.contended.load(Ordering::Relaxed)
140 }
141
142 /// The account this counter belongs to.
143 #[must_use]
144 pub fn account_id(&self) -> AccountId {
145 self.account_id
146 }
147
148 /// Unfunded units extended on this instance so far.
149 #[must_use]
150 pub fn spent(&self) -> CostUnits {
151 CostUnits(self.spent.load(Ordering::Acquire))
152 }
153
154 /// Units still extendable under `cap`, saturating at zero.
155 ///
156 /// Read by readiness: an elastic account with headroom here is admissible
157 /// even when its lease is empty or absent, which is the whole point of the
158 /// mode (INVARIANTS.md GL-10).
159 #[must_use]
160 pub fn headroom(&self, cap: CostUnits) -> CostUnits {
161 CostUnits(cap.get().saturating_sub(self.spent.load(Ordering::Acquire)))
162 }
163
164 /// Extend `units` of unfunded credit if `cap` has room. Lock-free; the CAS
165 /// loop retries only under concurrent overage on the same account.
166 ///
167 /// Fails closed on both boundaries: a total that exceeds the cap and a
168 /// total that cannot be represented are the same refusal, because a
169 /// wrapped total would read as a tiny spend and reopen the cap
170 /// (INVARIANTS.md GL-11).
171 #[inline]
172 pub(crate) fn try_debit(&self, units: CostUnits, cap: CostUnits) -> Result<(), DenyReason> {
173 let want = units.get();
174 let mut current = self.spent.load(Ordering::Acquire);
175 // Counted in a register and recorded once on every exit.
176 let mut lost = 0u64;
177 let note = |lost: u64| {
178 if lost != 0 {
179 self.contended.fetch_add(lost, Ordering::Relaxed);
180 }
181 };
182 loop {
183 let refused = || {
184 // A zero-delta RMW, rather than a load, places this observer
185 // in the marker's modification order. It therefore either
186 // precedes publication (when the reservation is still
187 // refundable), overlaps it, or acquires the completed
188 // committed update; a stale zero cannot skip an already
189 // linearized publication start.
190 if self.commit_publications.fetch_add(0, Ordering::AcqRel) != 0 {
191 return DenyReason::OverageCommitInProgress {
192 spent: CostUnits(current),
193 overage_cap: cap,
194 };
195 }
196 let committed = self.committed.load(Ordering::Acquire);
197 if committed
198 .checked_add(want)
199 .is_some_and(|next| next <= cap.get())
200 {
201 DenyReason::OverageCapTemporarilyExhausted {
202 spent: CostUnits(current),
203 overage_cap: cap,
204 }
205 } else {
206 DenyReason::OverageCapExhausted {
207 spent: CostUnits(current),
208 overage_cap: cap,
209 }
210 }
211 };
212 let Some(next) = current.checked_add(want) else {
213 note(lost);
214 return Err(refused());
215 };
216 if next > cap.get() {
217 note(lost);
218 return Err(refused());
219 }
220 match self.spent.compare_exchange_weak(
221 current,
222 next,
223 Ordering::AcqRel,
224 Ordering::Acquire,
225 ) {
226 Ok(_) => {
227 note(lost);
228 return Ok(());
229 }
230 Err(observed) => {
231 lost += 1;
232 current = observed;
233 }
234 }
235 }
236 }
237
238 /// Publish the reservation's phase claim and committed occupancy as one
239 /// observer-safe transition.
240 ///
241 /// `claim` owns the single commit-vs-cancel CAS. The publication marker is
242 /// installed before that closure can run and removed only after a winning
243 /// claim has advanced `committed`. A cap observer that overlaps either
244 /// half therefore sees `OverageCommitInProgress`, never a stable claim
245 /// that the already-irrevocable units remain refundable.
246 ///
247 /// **Precondition: the caller already owns `units` in `spent`, and this
248 /// function never credits them back.** Publication is not a debit. There
249 /// are exactly two owners that satisfy that, and both are in this crate: a
250 /// pending overage [`Reservation`], whose cancel/drop refunds, and a
251 /// [`TentativeOverage`] guard, which refunds on drop. A caller that
252 /// publishes without holding one leaves committed occupancy above recorded
253 /// spend, which the `publish` assertion catches in debug and which silently
254 /// reopens the cap in release. Reach it through
255 /// [`TentativeOverage::publish_commit`] unless a pending reservation is
256 /// already the owner.
257 ///
258 /// [`Reservation`]: crate::reservation::Reservation
259 #[inline]
260 pub(crate) fn publish_claim<T, E>(
261 &self,
262 units: CostUnits,
263 claim: impl FnOnce() -> Result<T, E>,
264 ) -> Result<T, E> {
265 // One marker belongs to one live call stack, so exhausting usize would
266 // require more simultaneously executing publications than the process
267 // can address. The fetch is the bounded, lock-free hot-path operation.
268 let prior = self.commit_publications.fetch_add(1, Ordering::AcqRel);
269 debug_assert_ne!(
270 prior,
271 usize::MAX,
272 "live overage commit publications exceed the address space"
273 );
274 let publication = OverageCommitPublication { overage: self };
275 match claim() {
276 Ok(value) => {
277 publication.publish(units);
278 Ok(value)
279 }
280 Err(error) => Err(error),
281 }
282 }
283
284 /// Withdraw previously extended units (release of an uncommitted overage
285 /// reservation). Callers must return only units they extended, exactly
286 /// once — the reservation state machine guarantees this.
287 ///
288 /// Plain `fetch_sub`, and the choice of what happens if that contract were
289 /// ever broken is deliberate rather than accidental. Underflow wraps the
290 /// counter to near `u64::MAX`, which makes every later request exceed the
291 /// cap and deny: wrong, but wrong in the fail-closed direction. Clamping
292 /// to zero would be the fail-*open* direction — it would under-report
293 /// spend and hand the account a fresh cap — so the safer-looking
294 /// arithmetic is the more dangerous one here.
295 #[inline]
296 pub(crate) fn credit(&self, units: CostUnits) {
297 let prior = self.spent.fetch_sub(units.get(), Ordering::AcqRel);
298 debug_assert!(
299 prior >= units.get(),
300 "overage credit of {} exceeds recorded spend {prior}",
301 units.get()
302 );
303 }
304
305 /// Extend `units` as a *revocable* debit, owned by the returned guard.
306 ///
307 /// This is the commit-time half of elastic funding: a leased reservation
308 /// whose window lapsed needs overage capacity *before* it can claim the
309 /// phase, because a won claim with no funding term breaks the ledger
310 /// equation by exactly these units. But it cannot know yet whether it will
311 /// win — a canceller may already be resolving the same reservation — so
312 /// the debit must be revocable until the claim resolves.
313 ///
314 /// The guard is what makes "debited but never resolved" unrepresentable.
315 /// Without it the units sit in `spent` with no owner between the debit and
316 /// the claim; a panic unwinding through that window (or any early return a
317 /// later edit adds) leaves the reservation's own `Drop` refunding the
318 /// *lease* while these units are stranded for the life of the process,
319 /// silently shrinking the account's cap.
320 #[inline]
321 pub(crate) fn debit_tentatively(
322 &self,
323 units: CostUnits,
324 cap: CostUnits,
325 ) -> Result<TentativeOverage<'_>, DenyReason> {
326 self.try_debit(units, cap)?;
327 Ok(TentativeOverage {
328 overage: self,
329 units,
330 })
331 }
332}
333
334/// A revocable overage debit, held between the claim's funding and its
335/// resolution. Dropping it returns the units; [`publish_commit`] is the only
336/// way to make them irrevocable.
337///
338/// [`publish_commit`]: TentativeOverage::publish_commit
339#[derive(Debug)]
340pub(crate) struct TentativeOverage<'a> {
341 overage: &'a AccountOverage,
342 units: CostUnits,
343}
344
345impl TentativeOverage<'_> {
346 /// Publish `claim` and this debit as one observer-safe transition.
347 ///
348 /// Consuming `self` is the ordering guarantee: the debit is already in
349 /// `spent` before the marker is installed, so the publication's occupancy
350 /// assertion holds exactly as it does for a natively admitted overage, and
351 /// a cap observer that overlaps either half sees `OverageCommitInProgress`
352 /// rather than a stable claim that irrevocable units are refundable. A
353 /// caller cannot claim without first holding a debit, because there is no
354 /// other way to reach this function.
355 ///
356 /// A winning claim retains the units; a losing claim credits them back
357 /// before returning, because the winner was a cancellation that refunded
358 /// its own funding source and owes nothing for these.
359 #[inline]
360 pub(crate) fn publish_commit<T, E>(self, claim: impl FnOnce() -> Result<T, E>) -> Result<T, E> {
361 // The refund is this function's responsibility from here, so disarm
362 // the guard rather than letting it double-credit a retained debit.
363 let this = ManuallyDrop::new(self);
364 match this.overage.publish_claim(this.units, claim) {
365 Ok(value) => Ok(value),
366 Err(error) => {
367 this.overage.credit(this.units);
368 Err(error)
369 }
370 }
371 }
372}
373
374impl Drop for TentativeOverage<'_> {
375 fn drop(&mut self) {
376 self.overage.credit(self.units);
377 }
378}
379
380/// Somewhere for a draining lease to say so, without this crate learning what
381/// a task, a runtime, or a waker is.
382///
383/// The refill plane supplies the implementation; the request path only calls
384/// it. That keeps the hot-path crate's dependency policy intact — a trait
385/// declaration is not an async dependency — and leaves the wake mechanism free
386/// to change without touching a line of request-path code.
387///
388/// **Contract:** [`request_refill`](RefillSignal::request_refill) is invoked
389/// from inside a debit, on the request path. It must not block, wait on a
390/// lock, allocate, or perform I/O (INVARIANTS.md GL-5, GL-6). A single-counter
391/// lease calls it at most once. A sharded lease calls it at most once per
392/// shard between aggregate checks; an early shard signal is re-armed if the
393/// aggregate has not reached low water yet.
394pub trait RefillSignal: Send + Sync + core::fmt::Debug {
395 /// This lease has crossed its low-water mark and wants replacing.
396 fn request_refill(&self);
397}
398
399/// What the refill plane should do about a lease, from
400/// [`LocalLease::refill_due_or_rearm`].
401///
402/// The two active verdicts are not degrees of the same thing. `Draining` says
403/// the lease is still serving and should be replaced *before* it refuses
404/// anything; `Refused` says it already refused work the account could fund,
405/// which is a statement about the grant's size rather than its depletion and
406/// needs the holder's unspent units folded back in before the next one is
407/// sized (INVARIANTS.md GL-1, GL-6).
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum RefillVerdict {
410 /// The lease is serving and has asked for nothing.
411 Idle,
412 /// Spending crossed low water. Rotate: acquire the next lease while this
413 /// one keeps serving, then release this one once it quiesces.
414 Draining,
415 /// A debit was refused for want of units. However far this lease is from
416 /// its low-water mark, it is too small for the work being offered, and
417 /// the units it still holds are the ones the next grant needs. Rotate by
418 /// *consolidating*: return them and re-grant against the restored
419 /// balance, atomically, so the exchange cannot shrink the holder or lose
420 /// the units to another instance in between.
421 Refused,
422}
423
424/// One lease shard on its own cache line.
425///
426/// The supported Apple Silicon hosts report 128-byte lines; aligning to 64
427/// would still let adjacent shards invalidate one another there. Over-aligning
428/// on a 64-byte-line target costs memory but does not weaken isolation.
429#[repr(align(128))]
430#[derive(Debug)]
431struct LeaseShard {
432 remaining: AtomicU64,
433 low_water: u64,
434 signalled: AtomicBool,
435 /// Debit compare-exchanges on `remaining` that lost to another writer.
436 ///
437 /// On this shard's own line, in padding it already had: the counter costs
438 /// no memory, and the only thread that writes it is one already contending
439 /// for this line. Added once per contended debit, never per failure, so a
440 /// contended debit adds one write rather than one per lost race.
441 contended: AtomicU64,
442}
443
444impl LeaseShard {
445 fn new(remaining: u64, low_water: u64) -> Self {
446 Self {
447 remaining: AtomicU64::new(remaining),
448 low_water,
449 signalled: AtomicBool::new(false),
450 contended: AtomicU64::new(0),
451 }
452 }
453
454 /// Record `lost` failed exchanges, if any. The branch is the whole cost
455 /// of the signal on an uncontended debit.
456 #[inline]
457 fn note_contention(&self, lost: u64) {
458 if lost != 0 {
459 self.contended.fetch_add(lost, Ordering::Relaxed);
460 }
461 }
462}
463
464#[derive(Debug)]
465enum LeaseBalance {
466 Single(LeaseShard),
467 Sharded(Box<[LeaseShard]>),
468}
469
470impl LeaseBalance {
471 fn as_slice(&self) -> &[LeaseShard] {
472 match self {
473 Self::Single(shard) => std::slice::from_ref(shard),
474 Self::Sharded(shards) => shards,
475 }
476 }
477}
478
479/// Fixed-size evidence for returning a pending debit.
480///
481/// A fragmented debit may draw from several counters, but cancellation may
482/// return the exact aggregate to any one counter: every counter is merely a
483/// partition of the same lease bound. Keeping only the refund destination
484/// avoids allocating a variable-length receipt on the request path.
485#[derive(Debug)]
486pub(crate) struct LeaseDebit {
487 shard: usize,
488 units: u64,
489}
490
491/// Instance-side lease state: the grant plus live remaining-unit counters.
492///
493/// Shared as `Arc<LocalLease>` between the request path (reserve/return) and
494/// the background refill task (`needs_refill`). Never mutated otherwise; a
495/// refill installs a *new* `LocalLease` rather than growing this one, so the
496/// request path never observes a counter that jumps upward mid-reservation.
497///
498/// Every fresh lease begins with clear shard-local refill signals. The refill
499/// plane re-arms an early signal only after checking the aggregate and closes
500/// the clear/debit race with a second aggregate read.
501#[derive(Debug)]
502struct LeaseInner {
503 grant: LeaseGrant,
504 balance: LeaseBalance,
505 /// Whom to tell when spending crosses `low_water`, if anyone. `None` for
506 /// a lease nobody refills — a test fixture, or a caller driving the
507 /// counter directly.
508 refill: OnceLock<Arc<dyn RefillSignal>>,
509 /// Refill trigger: when `remaining` falls to or below this, the holder
510 /// should acquire its next lease — in the background, never inline.
511 low_water: u64,
512 /// Set by a debit this lease could not fund, cleared by the refill plane
513 /// when it reads the verdict.
514 ///
515 /// Lease-wide rather than per-shard, because it reports a fact about the
516 /// *grant* and not about one counter: a refusal already walked every
517 /// shard (see [`LocalLease::try_reserve_at`]), so no sibling is holding
518 /// the units that would have funded it. It doubles as the refusal
519 /// doorbell's once-token, which is why a refusal wakes the plane even
520 /// when a low-water crossing already spent this lease's shard flags.
521 refused: AtomicBool,
522 /// The largest quote this lease refused for want of units: the demand a
523 /// consolidation may grow to (GL-131). Raised before the doorbell's swap,
524 /// which publishes it, and read by the plane only at quiescence. Zero
525 /// until a refusal, and never set by an expiry refusal, which rotates.
526 refused_quote: AtomicU64,
527 /// How much of the shards' contention a [`LocalLease::take_unreported_contention`]
528 /// caller has already been handed. Raised with `fetch_max`, so a lease that
529 /// is taken out of its slot and reinstalled — which a refused consolidation
530 /// does — is never reported twice.
531 contention_reported: AtomicU64,
532 /// Local end of life: `expires_at - safety margin`. Debits and commits
533 /// stop here, *before* the server-stamped expiry, so clock skew between
534 /// allocator and holder plus in-flight request time fit inside the
535 /// margin. Together with the allocator's reclaim grace (which starts
536 /// *after* `expires_at`) this closes the expiry race: the holder stops
537 /// spending strictly before the server starts reclaiming.
538 usable_until: Timestamp,
539}
540
541/// A local view of one lease's shared counters and metadata.
542///
543/// Sharded slots create one outer `Arc<LocalLease>` per locality. Those
544/// independently reference-counted handles all point to this shared inner
545/// state, so acquiring and dropping a routine request handle does not contend
546/// with other localities. The refill plane still observes every live alias
547/// through the inner `Arc` count before releasing a superseded lease.
548#[derive(Debug, Clone)]
549#[repr(align(128))]
550pub struct LocalLease {
551 inner: Arc<LeaseInner>,
552}
553
554fn usable_until(expires_at: Timestamp, margin: jiff::SignedDuration) -> Timestamp {
555 if margin < jiff::SignedDuration::ZERO {
556 // A negative margin would extend local use past allocator expiry and
557 // invert the expiry-safety protocol. Fail closed even if a caller
558 // bypasses validated LeaseManager configuration.
559 Timestamp::MIN
560 } else {
561 expires_at
562 .checked_sub(margin)
563 // A margin longer than the lease's life fails closed: never
564 // usable, settled by refill/reclaim.
565 .unwrap_or(Timestamp::MIN)
566 }
567}
568
569fn partition(total: u64, count: usize, index: usize) -> u64 {
570 let count = u64::try_from(count).expect("local shard count fits u64");
571 let index = u64::try_from(index).expect("local shard index fits u64");
572 total / count + u64::from(index < total % count)
573}
574
575impl LocalLease {
576 /// Wrap a grant for local spending with no safety margin (usable right
577 /// up to the grant's expiry). Prefer [`LocalLease::with_safety_margin`]
578 /// whenever the grant's clock is not the local clock.
579 ///
580 /// `low_water` is where background refill should begin; it must be below
581 /// the grant size to be useful, but any value is accepted (0 disables
582 /// early refill).
583 #[must_use]
584 pub fn new(grant: LeaseGrant, low_water: CostUnits) -> Self {
585 Self::with_safety_margin(grant, low_water, jiff::SignedDuration::ZERO)
586 }
587
588 /// Wrap a grant, refusing debits and commits once within `margin` of the
589 /// grant's expiry. Size the margin to cover worst-case allocator/holder
590 /// clock skew plus the longest request the service executes.
591 #[must_use]
592 pub fn with_safety_margin(
593 grant: LeaseGrant,
594 low_water: CostUnits,
595 margin: jiff::SignedDuration,
596 ) -> Self {
597 let usable_until = usable_until(grant.expires_at, margin);
598 LocalLease {
599 inner: Arc::new(LeaseInner {
600 balance: LeaseBalance::Single(LeaseShard::new(grant.units.get(), low_water.get())),
601 low_water: low_water.get(),
602 refused: AtomicBool::new(false),
603 refused_quote: AtomicU64::new(0),
604 contention_reported: AtomicU64::new(0),
605 usable_until,
606 refill: OnceLock::new(),
607 grant,
608 }),
609 }
610 }
611
612 /// Wrap a grant in an explicitly sharded local layout.
613 ///
614 /// Grant units and the low-water threshold are partitioned exactly across
615 /// `sharding`; their sums remain the original values. A one-shard request
616 /// retains the inline representation used by [`Self::with_safety_margin`].
617 #[must_use]
618 pub fn with_sharding(
619 grant: LeaseGrant,
620 low_water: CostUnits,
621 margin: jiff::SignedDuration,
622 sharding: LocalSharding,
623 ) -> Self {
624 if sharding == LocalSharding::SINGLE {
625 return Self::with_safety_margin(grant, low_water, margin);
626 }
627 let usable_until = usable_until(grant.expires_at, margin);
628 let count = sharding.get();
629 let shards = (0..count)
630 .map(|index| {
631 LeaseShard::new(
632 partition(grant.units.get(), count, index),
633 partition(low_water.get(), count, index),
634 )
635 })
636 .collect::<Vec<_>>()
637 .into_boxed_slice();
638 Self {
639 inner: Arc::new(LeaseInner {
640 grant,
641 balance: LeaseBalance::Sharded(shards),
642 refill: OnceLock::new(),
643 low_water: low_water.get(),
644 refused: AtomicBool::new(false),
645 refused_quote: AtomicU64::new(0),
646 contention_reported: AtomicU64::new(0),
647 usable_until,
648 }),
649 }
650 }
651
652 /// Attach the signal to raise when spending crosses `low_water`.
653 ///
654 /// Without one, a lease still records the crossing in `needs_refill` and
655 /// waits to be polled — which is the behaviour every caller had before
656 /// refill became demand-driven, and remains correct, just later.
657 #[must_use]
658 pub fn with_refill(self, signal: Arc<dyn RefillSignal>) -> Self {
659 // The first attachment owns the doorbell for every local view. A
660 // repeated attachment keeps that established signal; construction
661 // remains safe even if a caller created views before wiring refill.
662 let _already_attached = self.inner.refill.set(signal);
663 self
664 }
665
666 /// The instant this lease stops accepting debits and commits locally.
667 #[must_use]
668 pub fn usable_until(&self) -> Timestamp {
669 self.inner.usable_until
670 }
671
672 /// The allocator's grant this local lease spends.
673 #[must_use]
674 pub fn grant(&self) -> &LeaseGrant {
675 &self.inner.grant
676 }
677
678 /// Whether this is the only independently reference-counted local view of
679 /// the lease. The refill plane combines this with the outer `Arc` count;
680 /// only then can no request still hold any locality's handle.
681 #[must_use]
682 pub fn is_only_local_view(&self) -> bool {
683 Arc::strong_count(&self.inner) == 1
684 }
685
686 /// The largest quote this lease refused for want of units, or zero.
687 ///
688 /// Demand the refill plane has proof of: a consolidation may grow the
689 /// replacement to it when the account can fund it (GL-131). Exact only once
690 /// the lease has quiesced, the same condition [`Self::remaining`] needs.
691 #[must_use]
692 pub fn largest_refused_quote(&self) -> CostUnits {
693 CostUnits(self.inner.refused_quote.load(Ordering::Acquire))
694 }
695
696 /// Debits that lost a compare-exchange on a shard to another writer,
697 /// summed across shards, since this lease was created.
698 ///
699 /// Evidence that the account's funding line is being written from more
700 /// than one core at once, and a **lower bound** on contention rather than
701 /// a measure of its cost: only the debit loops can observe a lost race.
702 /// The rate bucket, reference counts and settlement use read-modify-write
703 /// operations that pay for a contended line without ever failing, and a
704 /// debit whose exchange lands between two rivals' records nothing. On a
705 /// load-linked/store-conditional target a spurious exchange failure also
706 /// counts; x86-64 and aarch64 with LSE atomics have none.
707 ///
708 /// A control-plane read that walks every shard. No decision reads it.
709 #[must_use]
710 pub fn contended_debits(&self) -> u64 {
711 self.inner
712 .balance
713 .as_slice()
714 .iter()
715 .fold(0u64, |total, shard| {
716 total.saturating_add(shard.contended.load(Ordering::Relaxed))
717 })
718 }
719
720 /// The contention recorded since the last call, handed out exactly once.
721 ///
722 /// A slot folds this into its account's running total when the lease
723 /// leaves it. Idempotent across removal and reinstallation of the same
724 /// lease: what was handed out is remembered on the lease itself, and a
725 /// concurrent or repeated call receives only what no earlier call did.
726 #[must_use]
727 pub fn take_unreported_contention(&self) -> u64 {
728 let total = self.contended_debits();
729 let reported = self
730 .inner
731 .contention_reported
732 .fetch_max(total, Ordering::AcqRel);
733 total.saturating_sub(reported)
734 }
735
736 /// The contention a slot has not yet been handed, without handing it out.
737 #[must_use]
738 pub fn unreported_contention(&self) -> u64 {
739 self.contended_debits()
740 .saturating_sub(self.inner.contention_reported.load(Ordering::Acquire))
741 }
742
743 /// Units still spendable, aggregated across every shard.
744 ///
745 /// Exact for a single-counter lease, and for a sharded lease once it has
746 /// quiesced — which is the state every accounting use requires, and the
747 /// one `release_quiesced` establishes before settling a grant.
748 ///
749 /// Under concurrency a sharded read is an *estimate in both directions*,
750 /// and deliberately so: a shard-by-shard walk is not one atomic instant,
751 /// and a failed fragmented reservation returns its whole aggregate to one
752 /// shard rather than to the shards it drew from (see
753 /// `Self::try_reserve_at`). A refund landing on an already-visited
754 /// shard is counted twice; one landing on a shard the walk has passed is
755 /// missed. Hence the clamp: the sum can exceed the grant, so it is
756 /// saturated and bounded rather than asserted, and no reader of a live
757 /// lease may treat the result as an exact balance.
758 #[must_use]
759 pub fn remaining(&self) -> CostUnits {
760 let remaining = self
761 .inner
762 .balance
763 .as_slice()
764 .iter()
765 .fold(0u64, |total, shard| {
766 total.saturating_add(shard.remaining.load(Ordering::Acquire))
767 })
768 .min(self.inner.grant.units.get());
769 CostUnits(remaining)
770 }
771
772 /// True once spending has crossed the low-water mark. Monotonic in
773 /// practice only between refills; the refill task polls or checks after
774 /// each reservation.
775 #[must_use]
776 pub fn needs_refill(&self) -> bool {
777 self.remaining().get() <= self.inner.low_water
778 }
779
780 /// What the refill plane should do about this lease, clearing whatever
781 /// the lease was holding to tell it.
782 ///
783 /// [`RefillVerdict::Refused`] is tested first and outranks a low-water
784 /// crossing, because the two verdicts ask for different actions and only
785 /// one of them is still preventable. A crossing is an *anticipatory*
786 /// signal — the lease can still serve, so the plane acquires alongside it
787 /// and no request is refused between ticks. A refusal is the failure that
788 /// crossing exists to avoid, already happened: there is nothing left to
789 /// preserve, and acquiring alongside a grant that could not fund the work
790 /// would install a *smaller* one beside it (see
791 /// `Self::signal_refusal`).
792 ///
793 /// Re-arming: for the crossing case the second aggregate read closes the
794 /// race with a debit that observed a still-set flag just before this
795 /// method cleared it — that debit is either included in the recheck, or a
796 /// later debit sees the cleared flag and rings the doorbell itself.
797 #[must_use]
798 pub fn refill_due_or_rearm(&self) -> RefillVerdict {
799 // Acquires the refused debit's counter reads before the plane acts on
800 // them, and consumes the doorbell so a plane that answers this
801 // verdict is not woken again for the same refusal.
802 if self.inner.refused.swap(false, Ordering::AcqRel) {
803 return RefillVerdict::Refused;
804 }
805 if self.needs_refill() {
806 return RefillVerdict::Draining;
807 }
808 for shard in self.inner.balance.as_slice() {
809 // Reading `true` acquires the debit published by the signal's
810 // release RMW before clearing its doorbell.
811 shard.signalled.swap(false, Ordering::AcqRel);
812 }
813 if self.needs_refill() {
814 RefillVerdict::Draining
815 } else {
816 RefillVerdict::Idle
817 }
818 }
819
820 /// Debit `units` if the lease is live and has capacity. Lock-free; the
821 /// CAS loop retries only under concurrent reservations on the same lease.
822 ///
823 /// This is the raw counter operation. Request code should prefer
824 /// [`crate::reservation::Reservation::reserve`], which pairs the debit
825 /// with the commit/release state machine.
826 #[inline]
827 pub fn try_debit(&self, units: CostUnits, now: Timestamp) -> Result<(), DenyReason> {
828 self.try_reserve_at(units, now, Locality::current())
829 .map(|_| ())
830 }
831
832 #[inline]
833 pub(crate) fn try_reserve_at(
834 &self,
835 units: CostUnits,
836 now: Timestamp,
837 locality: Locality,
838 ) -> Result<LeaseDebit, DenyReason> {
839 if now >= self.inner.usable_until {
840 // The same silence as the exhaustion exit below, for the same
841 // reason: a lease that can no longer serve must say so rather
842 // than wait to be discovered. Rotation keeps the poll interval as
843 // its backstop for a lease no request touches (INVARIANTS.md GL-6).
844 self.signal_refusal();
845 return Err(DenyReason::LeaseExpired);
846 }
847 let want = units.get();
848 let shards = self.inner.balance.as_slice();
849 let first = locality.index(LocalSharding::new(
850 std::num::NonZeroUsize::new(shards.len()).expect("lease has at least one shard"),
851 ));
852
853 // The routine path: one CAS on the caller's stable shard. Before
854 // assembling a split receipt, try every sibling for the whole debit;
855 // an idle shard can therefore be stolen without allocation.
856 for offset in 0..shards.len() {
857 let index = (first + offset) % shards.len();
858 if let Some(part) = self.try_whole(index, want) {
859 return Ok(part);
860 }
861 }
862
863 // Genuine fragmentation: reserve pieces in a stable circular order.
864 // The receipt remains fixed-size: all pieces belong to one lease, so
865 // cancellation can restore their exact aggregate to one shard without
866 // changing the lease bound. A failed attempt does the same before it
867 // returns, so denial remains zero-charge and no capacity is stranded.
868 let mut needed = want;
869 for offset in 0..shards.len() {
870 let index = (first + offset) % shards.len();
871 needed -= self.take_up_to(index, needed);
872 if needed == 0 {
873 // The first shard was necessarily exhausted (or already
874 // empty), so it is a sufficient shard-local refill doorbell
875 // for this cold fragmented path.
876 let next = shards[first].remaining.load(Ordering::Acquire);
877 self.maybe_signal_refill(first, next);
878 return Ok(LeaseDebit {
879 shard: first,
880 units: want,
881 });
882 }
883 }
884
885 self.credit_to(first, want - needed);
886 // Reported after the rollback, so the plane that answers this refusal
887 // reads the restored aggregate rather than a torn one. The quote is
888 // raised first so the doorbell's release carries it.
889 self.inner.refused_quote.fetch_max(want, Ordering::Relaxed);
890 self.signal_refusal();
891 Err(DenyReason::LeaseExhausted {
892 remaining: self.remaining(),
893 })
894 }
895
896 #[inline]
897 fn try_whole(&self, shard_index: usize, want: u64) -> Option<LeaseDebit> {
898 let shard = &self.inner.balance.as_slice()[shard_index];
899 let mut current = shard.remaining.load(Ordering::Acquire);
900 // Counted in a register and recorded once on every exit, so an
901 // uncontended debit pays one untaken branch.
902 let mut lost = 0u64;
903 loop {
904 let Some(next) = current.checked_sub(want) else {
905 shard.note_contention(lost);
906 return None;
907 };
908 match shard.remaining.compare_exchange_weak(
909 current,
910 next,
911 Ordering::AcqRel,
912 Ordering::Acquire,
913 ) {
914 Ok(_) => {
915 shard.note_contention(lost);
916 self.maybe_signal_refill(shard_index, next);
917 return Some(LeaseDebit {
918 shard: shard_index,
919 units: want,
920 });
921 }
922 Err(observed) => {
923 lost += 1;
924 current = observed;
925 }
926 }
927 }
928 }
929
930 #[cold]
931 fn take_up_to(&self, shard_index: usize, want: u64) -> u64 {
932 let shard = &self.inner.balance.as_slice()[shard_index];
933 let mut current = shard.remaining.load(Ordering::Acquire);
934 let mut lost = 0u64;
935 loop {
936 if current == 0 {
937 shard.note_contention(lost);
938 return 0;
939 }
940 let taken = current.min(want);
941 let next = current - taken;
942 match shard.remaining.compare_exchange_weak(
943 current,
944 next,
945 Ordering::AcqRel,
946 Ordering::Acquire,
947 ) {
948 Ok(_) => {
949 shard.note_contention(lost);
950 return taken;
951 }
952 Err(observed) => {
953 lost += 1;
954 current = observed;
955 }
956 }
957 }
958 }
959
960 #[inline]
961 fn maybe_signal_refill(&self, shard_index: usize, next: u64) {
962 if next <= self.inner.balance.as_slice()[shard_index].low_water {
963 self.signal_refill(shard_index);
964 }
965 }
966
967 /// Report a debit this lease could not fund, and ring the doorbell the
968 /// first time.
969 ///
970 /// A refusal is the one lease event that *proves* the grant can no longer
971 /// serve the work being offered, so it is the one event the refill plane
972 /// most needs and, before GL-109, the only one it was never told about.
973 /// Low water cannot stand in for it: the mark counts units and the
974 /// refusal is about a quote, so a lease holding more units than its mark
975 /// can refuse every request until its TTL while the account has the
976 /// balance to fund them.
977 ///
978 /// Cold, and once per lease: the flag is both the report the plane reads
979 /// and the token that keeps a refusal storm from becoming a wake storm.
980 /// A refusal arriving while an earlier one is still unanswered adds
981 /// nothing — the plane's response does not depend on how many there were.
982 #[cold]
983 #[inline(never)]
984 fn signal_refusal(&self) {
985 // Release publishes the preceding counter reads to the plane that
986 // clears this flag; a swap that observes `true` means an unanswered
987 // report already stands.
988 if self.inner.refused.swap(true, Ordering::AcqRel) {
989 return;
990 }
991 if let Some(signal) = self.inner.refill.get() {
992 signal.request_refill();
993 }
994 }
995
996 /// Raise the refill signal at most once per shard between control-plane
997 /// aggregate checks.
998 ///
999 /// Out of line and `#[cold]`: every debit tests the branch above, but only
1000 /// one debit per lease ever arrives here, so none of this belongs in the
1001 /// hot path's instruction stream.
1002 #[cold]
1003 #[inline(never)]
1004 fn signal_refill(&self, shard_index: usize) {
1005 let Some(signal) = self.inner.refill.get() else {
1006 return;
1007 };
1008 // Release publishes the preceding remaining-counter CAS to the
1009 // control plane when it clears this doorbell. The implementation
1010 // behind `request_refill` remains responsible for its own wake state.
1011 if !self.inner.balance.as_slice()[shard_index]
1012 .signalled
1013 .swap(true, Ordering::Release)
1014 {
1015 signal.request_refill();
1016 }
1017 }
1018
1019 /// Return previously debited units (release of an uncommitted
1020 /// reservation). Callers must return only units they debited, exactly
1021 /// once — the reservation state machine guarantees this.
1022 #[inline]
1023 pub(crate) fn credit(&self, debit: &LeaseDebit) {
1024 self.credit_to(debit.shard, debit.units);
1025 }
1026
1027 fn credit_to(&self, shard: usize, units: u64) {
1028 self.inner.balance.as_slice()[shard]
1029 .remaining
1030 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
1031 current.checked_add(units)
1032 })
1033 .expect("a debit receipt cannot credit beyond its lease grant");
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040 use std::num::NonZeroUsize;
1041
1042 fn t(secs: i64) -> Timestamp {
1043 Timestamp::from_second(secs).unwrap()
1044 }
1045
1046 fn overage() -> AccountOverage {
1047 AccountOverage::new(AccountId(1))
1048 }
1049
1050 /// The tentative debit's whole reason for existing: units taken but never
1051 /// resolved come back on their own. Without the guard they would sit in
1052 /// `spent` with no owner — a panic or an early return between the debit
1053 /// and the claim would strand them for the life of the process, silently
1054 /// shrinking the account's cap.
1055 #[test]
1056 fn dropping_an_unresolved_tentative_debit_returns_the_credit() {
1057 let o = overage();
1058 {
1059 let tentative = o.debit_tentatively(CostUnits(40), CostUnits(100)).unwrap();
1060 assert_eq!(o.spent(), CostUnits(40));
1061 drop(tentative);
1062 }
1063 assert_eq!(o.spent(), CostUnits::ZERO);
1064 assert_eq!(o.headroom(CostUnits(100)), CostUnits(100));
1065 }
1066
1067 /// A losing claim credits the debit back before returning, so the cap is
1068 /// immediately reusable and committed occupancy never moved.
1069 #[test]
1070 fn a_tentative_debit_whose_claim_loses_returns_the_credit() {
1071 let o = overage();
1072 let tentative = o.debit_tentatively(CostUnits(40), CostUnits(100)).unwrap();
1073 assert_eq!(o.spent(), CostUnits(40));
1074
1075 let lost: Result<(), ()> = tentative.publish_commit(|| Err(()));
1076 assert!(lost.is_err());
1077 assert_eq!(o.spent(), CostUnits::ZERO);
1078 assert_eq!(o.headroom(CostUnits(100)), CostUnits(100));
1079 }
1080
1081 /// A winning claim retains the debit and advances committed occupancy,
1082 /// which is what makes those units irrevocable to a cap observer.
1083 #[test]
1084 fn a_tentative_debit_whose_claim_wins_becomes_irrevocable() {
1085 let o = overage();
1086 let tentative = o.debit_tentatively(CostUnits(40), CostUnits(100)).unwrap();
1087 let won: Result<(), ()> = tentative.publish_commit(|| Ok(()));
1088 assert!(won.is_ok());
1089 assert_eq!(o.spent(), CostUnits(40));
1090 // Committed occupancy moved with it, so the remaining cap is stably
1091 // exhausted rather than temporarily so.
1092 assert_eq!(
1093 o.try_debit(CostUnits(61), CostUnits(100)),
1094 Err(DenyReason::OverageCapExhausted {
1095 spent: CostUnits(40),
1096 overage_cap: CostUnits(100),
1097 })
1098 );
1099 }
1100
1101 #[test]
1102 fn committed_overage_accumulates_up_to_the_cap_and_then_refuses() {
1103 let o = overage();
1104 o.try_debit(CostUnits(40), CostUnits(100)).unwrap();
1105 o.publish_claim(CostUnits(40), || Ok::<_, ()>(())).unwrap();
1106 o.try_debit(CostUnits(60), CostUnits(100)).unwrap();
1107 o.publish_claim(CostUnits(60), || Ok::<_, ()>(())).unwrap();
1108 assert_eq!(o.spent(), CostUnits(100));
1109 assert_eq!(o.headroom(CostUnits(100)), CostUnits::ZERO);
1110 assert_eq!(
1111 o.try_debit(CostUnits(1), CostUnits(100)),
1112 Err(DenyReason::OverageCapExhausted {
1113 spent: CostUnits(100),
1114 overage_cap: CostUnits(100),
1115 })
1116 );
1117 assert_eq!(o.spent(), CostUnits(100), "a refusal claims nothing");
1118 }
1119
1120 /// A pending debit changes which local overage state is reported when
1121 /// returning all pending credit would make this request fit. A request
1122 /// larger than the whole cap is stable local saturation, but it remains
1123 /// retryable because a background lease grant can fund it.
1124 #[test]
1125 fn pending_overage_is_transient_only_when_its_refund_would_make_room() {
1126 let o = overage();
1127 o.try_debit(CostUnits(60), CostUnits(100)).unwrap();
1128
1129 let pending_saturation = o.try_debit(CostUnits(50), CostUnits(100)).unwrap_err();
1130 assert_eq!(
1131 pending_saturation,
1132 DenyReason::OverageCapTemporarilyExhausted {
1133 spent: CostUnits(60),
1134 overage_cap: CostUnits(100),
1135 }
1136 );
1137 assert_eq!(pending_saturation.retry(), crate::deny::Retry::Transient);
1138
1139 let request_exceeds_cap = o.try_debit(CostUnits(101), CostUnits(100)).unwrap_err();
1140 assert_eq!(
1141 request_exceeds_cap,
1142 DenyReason::OverageCapExhausted {
1143 spent: CostUnits(60),
1144 overage_cap: CostUnits(100),
1145 }
1146 );
1147 assert_eq!(request_exceeds_cap.retry(), crate::deny::Retry::Transient);
1148 }
1149
1150 /// The cap is a parameter, so lowering it below what an account has
1151 /// already spent refuses immediately rather than waiting for the counter
1152 /// to catch up — and `headroom` saturates instead of underflowing.
1153 #[test]
1154 fn lowering_the_cap_below_current_spend_refuses_at_once() {
1155 let o = overage();
1156 o.try_debit(CostUnits(80), CostUnits(100)).unwrap();
1157 assert_eq!(o.headroom(CostUnits(50)), CostUnits::ZERO);
1158 assert!(o.try_debit(CostUnits(1), CostUnits(50)).is_err());
1159 o.try_debit(CostUnits(1), CostUnits(100)).unwrap();
1160 }
1161
1162 /// A total that cannot be represented is the same refusal as one that
1163 /// exceeds the cap: never a wrap to a small spend, which would reopen the
1164 /// cap (INVARIANTS.md GL-11).
1165 #[test]
1166 fn an_unrepresentable_total_refuses_rather_than_wrapping() {
1167 let o = overage();
1168 o.try_debit(CostUnits(u64::MAX - 1), CostUnits(u64::MAX))
1169 .unwrap();
1170 assert!(o.try_debit(CostUnits(2), CostUnits(u64::MAX)).is_err());
1171 assert_eq!(o.spent(), CostUnits(u64::MAX - 1));
1172 }
1173
1174 #[test]
1175 fn credit_returns_headroom_to_the_cap() {
1176 let o = overage();
1177 o.try_debit(CostUnits(100), CostUnits(100)).unwrap();
1178 o.credit(CostUnits(30));
1179 assert_eq!(o.spent(), CostUnits(70));
1180 assert_eq!(o.headroom(CostUnits(100)), CostUnits(30));
1181 o.try_debit(CostUnits(30), CostUnits(100)).unwrap();
1182 assert!(o.try_debit(CostUnits(1), CostUnits(100)).is_err());
1183 }
1184
1185 /// Concurrent debits that individually fit the cap must not jointly
1186 /// exceed it. The cap comparison lives inside the compare-exchange for
1187 /// exactly this reason; a check-then-claim would let both threads through.
1188 #[test]
1189 fn concurrent_debits_never_exceed_the_cap() {
1190 const THREADS: usize = 8;
1191 const EACH: usize = 500;
1192 const CAP: u64 = 1_000;
1193 let o = Arc::new(overage());
1194 let admitted = Arc::new(AtomicU64::new(0));
1195 std::thread::scope(|scope| {
1196 for _ in 0..THREADS {
1197 let o = Arc::clone(&o);
1198 let admitted = Arc::clone(&admitted);
1199 scope.spawn(move || {
1200 for _ in 0..EACH {
1201 if o.try_debit(CostUnits(1), CostUnits(CAP)).is_ok() {
1202 admitted.fetch_add(1, Ordering::Relaxed);
1203 }
1204 }
1205 });
1206 }
1207 });
1208 assert_eq!(o.spent(), CostUnits(CAP));
1209 assert_eq!(
1210 admitted.load(Ordering::Relaxed),
1211 CAP,
1212 "every admitted unit is one the counter recorded, and vice versa"
1213 );
1214 }
1215
1216 fn lease(units: u64, expires: i64, low_water: u64) -> LocalLease {
1217 LocalLease::new(
1218 LeaseGrant {
1219 lease_id: LeaseId(7),
1220 account_id: AccountId(1),
1221 fencing_token: FencingToken(3),
1222 units: CostUnits(units),
1223 expires_at: t(expires),
1224 },
1225 CostUnits(low_water),
1226 )
1227 }
1228
1229 fn sharded_lease(units: u64, low_water: u64, shards: usize) -> LocalLease {
1230 LocalLease::with_sharding(
1231 LeaseGrant {
1232 lease_id: LeaseId(7),
1233 account_id: AccountId(1),
1234 fencing_token: FencingToken(3),
1235 units: CostUnits(units),
1236 expires_at: t(1_000),
1237 },
1238 CostUnits(low_water),
1239 jiff::SignedDuration::ZERO,
1240 LocalSharding::new(NonZeroUsize::new(shards).unwrap()),
1241 )
1242 }
1243
1244 #[test]
1245 fn sharded_grant_and_low_water_partitions_are_exact() {
1246 assert_eq!(align_of::<LeaseShard>(), 128);
1247 assert_eq!(size_of::<LeaseShard>(), 128);
1248 assert_eq!(align_of::<LocalLease>(), 128);
1249
1250 let l = sharded_lease(10, 3, 4);
1251 let shards = l.inner.balance.as_slice();
1252 assert_eq!(shards.len(), 4);
1253 assert_eq!(
1254 shards
1255 .iter()
1256 .map(|shard| shard.remaining.load(Ordering::Relaxed))
1257 .collect::<Vec<_>>(),
1258 [3, 3, 2, 2]
1259 );
1260 assert_eq!(
1261 shards
1262 .iter()
1263 .map(|shard| shard.low_water)
1264 .collect::<Vec<_>>(),
1265 [1, 1, 1, 0]
1266 );
1267 assert_eq!(l.remaining(), CostUnits(10));
1268 }
1269
1270 #[test]
1271 fn cloned_local_views_are_visible_to_quiescence_detection() {
1272 let lease = sharded_lease(10, 0, 2);
1273 assert!(lease.is_only_local_view());
1274 let sibling = lease.clone();
1275 assert!(!lease.is_only_local_view());
1276 drop(sibling);
1277 assert!(lease.is_only_local_view());
1278 }
1279
1280 #[test]
1281 fn fragmented_reservation_refunds_without_stranding_capacity() {
1282 let l = Arc::new(sharded_lease(10, 0, 4));
1283 assert_eq!(size_of::<LeaseDebit>(), 16, "the receipt is fixed-size");
1284
1285 let reservation = crate::Reservation::reserve(&l, CostUnits(8), t(0)).unwrap();
1286 assert_eq!(l.remaining(), CostUnits(2));
1287 assert_eq!(reservation.cancel(), crate::CancelOutcome::ZeroCharged);
1288 assert_eq!(l.remaining(), CostUnits(10));
1289 }
1290
1291 #[test]
1292 fn failed_fragmented_debit_reports_true_remaining_and_rolls_back() {
1293 let l = sharded_lease(10, 0, 4);
1294
1295 assert_eq!(
1296 l.try_debit(CostUnits(11), t(0)),
1297 Err(DenyReason::LeaseExhausted {
1298 remaining: CostUnits(10)
1299 })
1300 );
1301 assert_eq!(l.remaining(), CostUnits(10));
1302 }
1303
1304 #[test]
1305 fn rebalanced_refunds_are_exact_at_the_u64_boundary() {
1306 let l = Arc::new(sharded_lease(u64::MAX, 0, 2));
1307 let reservation = crate::Reservation::reserve(&l, CostUnits(u64::MAX), t(0)).unwrap();
1308 assert_eq!(l.remaining(), CostUnits::ZERO);
1309 assert_eq!(reservation.cancel(), crate::CancelOutcome::ZeroCharged);
1310 assert_eq!(l.remaining(), CostUnits(u64::MAX));
1311
1312 // The refund is deliberately allowed to concentrate the grant on one
1313 // shard. A second maximum-sized reserve proves that representation is
1314 // spendable and cannot overflow its refund destination.
1315 let reservation = crate::Reservation::reserve(&l, CostUnits(u64::MAX), t(0)).unwrap();
1316 assert_eq!(reservation.cancel(), crate::CancelOutcome::ZeroCharged);
1317 assert_eq!(l.remaining(), CostUnits(u64::MAX));
1318 }
1319
1320 /// No rival, no lost exchange: a debit that won its first compare-exchange
1321 /// records nothing, on the single and the sharded layout alike. Limited to
1322 /// targets whose weak exchange cannot fail spuriously; on a
1323 /// load-linked/store-conditional target a spurious failure also counts.
1324 #[cfg(any(
1325 target_arch = "x86_64",
1326 all(target_arch = "aarch64", target_feature = "lse")
1327 ))]
1328 #[test]
1329 fn an_uncontended_debit_records_no_contention() {
1330 for l in [lease(1_000, 1_000, 0), sharded_lease(1_000, 0, 4)] {
1331 for _ in 0..100 {
1332 let debit = l
1333 .try_reserve_at(CostUnits(3), t(0), Locality::current())
1334 .unwrap();
1335 l.credit(&debit);
1336 }
1337 // Fragmented debits walk `take_up_to` instead of `try_whole`.
1338 let fragmented = l
1339 .try_reserve_at(CostUnits(1_000), t(0), Locality::current())
1340 .unwrap();
1341 l.credit(&fragmented);
1342 assert_eq!(l.contended_debits(), 0);
1343 }
1344 }
1345
1346 /// Many writers on one line lose exchanges, and each loss is recorded on
1347 /// the shard it was lost on without disturbing the balance.
1348 #[test]
1349 fn contended_debits_are_recorded_without_disturbing_the_balance() {
1350 const THREADS: u64 = 8;
1351 const DEBITS: u64 = 20_000;
1352 let l = lease(THREADS * DEBITS, 1_000, 0);
1353 // Retries are a race, so the stress repeats until one is seen. Eight
1354 // threads on one line lose races within the first round on any
1355 // multi-core host; the bound keeps a single-core host from hanging.
1356 for _ in 0..50 {
1357 std::thread::scope(|scope| {
1358 for _ in 0..THREADS {
1359 scope.spawn(|| {
1360 for _ in 0..DEBITS {
1361 let debit = l
1362 .try_reserve_at(CostUnits(1), t(0), Locality::current())
1363 .unwrap();
1364 l.credit(&debit);
1365 }
1366 });
1367 }
1368 });
1369 if l.contended_debits() > 0 {
1370 break;
1371 }
1372 }
1373 assert!(
1374 l.contended_debits() > 0,
1375 "eight writers on one line never lost a race"
1376 );
1377 assert_eq!(
1378 l.remaining(),
1379 CostUnits(THREADS * DEBITS),
1380 "credits restored every debit"
1381 );
1382 }
1383
1384 /// The hand-out is exact and idempotent. A slot that takes the lease out,
1385 /// reinstalls it, and takes it out again must not report the same
1386 /// contention twice.
1387 #[test]
1388 fn unreported_contention_is_handed_out_exactly_once() {
1389 let l = sharded_lease(100, 0, 2);
1390 let shards = l.inner.balance.as_slice();
1391 shards[0].note_contention(3);
1392 shards[1].note_contention(4);
1393 assert_eq!(l.contended_debits(), 7);
1394 assert_eq!(l.unreported_contention(), 7);
1395 assert_eq!(l.take_unreported_contention(), 7);
1396 assert_eq!(
1397 l.take_unreported_contention(),
1398 0,
1399 "reinstalled and taken again"
1400 );
1401 assert_eq!(l.unreported_contention(), 0);
1402 shards[1].note_contention(2);
1403 assert_eq!(l.unreported_contention(), 2);
1404 assert_eq!(l.take_unreported_contention(), 2);
1405 assert_eq!(
1406 l.contended_debits(),
1407 9,
1408 "the lease's own total never resets"
1409 );
1410 // A handle cloned into another locality view shares the same record.
1411 assert_eq!(l.clone().take_unreported_contention(), 0);
1412 shards[0].note_contention(0);
1413 assert_eq!(l.contended_debits(), 9, "recording zero writes nothing");
1414 }
1415
1416 /// The overage counter records lost debit races (GL-139) and nothing when
1417 /// uncontended, without disturbing `spent`.
1418 #[cfg(any(
1419 target_arch = "x86_64",
1420 all(target_arch = "aarch64", target_feature = "lse")
1421 ))]
1422 #[test]
1423 fn an_uncontended_overage_debit_records_no_contention() {
1424 let overage = AccountOverage::new(AccountId(1));
1425 for _ in 0..100 {
1426 overage.try_debit(CostUnits(1), CostUnits(1_000)).unwrap();
1427 }
1428 // Refusals exit the loop on another path.
1429 assert!(
1430 overage
1431 .try_debit(CostUnits(1_000), CostUnits(1_000))
1432 .is_err()
1433 );
1434 assert_eq!(overage.contended_debits(), 0);
1435 }
1436
1437 #[test]
1438 fn contended_overage_debits_are_recorded_without_disturbing_spend() {
1439 const THREADS: u64 = 8;
1440 const DEBITS: u64 = 20_000;
1441 let overage = AccountOverage::new(AccountId(1));
1442 let mut rounds = 0;
1443 while overage.contended_debits() == 0 && rounds < 50 {
1444 rounds += 1;
1445 std::thread::scope(|scope| {
1446 for _ in 0..THREADS {
1447 scope.spawn(|| {
1448 for _ in 0..DEBITS {
1449 overage
1450 .try_debit(CostUnits(1), CostUnits(u64::MAX))
1451 .unwrap();
1452 }
1453 });
1454 }
1455 });
1456 }
1457 assert!(
1458 overage.contended_debits() > 0,
1459 "eight writers on one line never lost a race"
1460 );
1461 assert_eq!(
1462 overage.spent.load(Ordering::Relaxed),
1463 rounds * THREADS * DEBITS,
1464 "every debit counted exactly once"
1465 );
1466 }
1467
1468 #[test]
1469 fn sharded_lease_spends_to_exact_exhaustion_without_stranding() {
1470 let l = sharded_lease(17, 0, 8);
1471 for _ in 0..17 {
1472 l.try_debit(CostUnits(1), t(0)).unwrap();
1473 }
1474 assert_eq!(l.remaining(), CostUnits::ZERO);
1475 assert_eq!(
1476 l.try_debit(CostUnits(1), t(0)),
1477 Err(DenyReason::LeaseExhausted {
1478 remaining: CostUnits::ZERO
1479 })
1480 );
1481 }
1482
1483 #[test]
1484 fn debit_search_starts_at_the_supplied_locality_and_wraps_in_order() {
1485 let l = sharded_lease(16, 0, 4);
1486 let debit = l
1487 .try_reserve_at(CostUnits(1), t(0), Locality::for_test(3))
1488 .unwrap();
1489 let remaining: Vec<_> = l
1490 .inner
1491 .balance
1492 .as_slice()
1493 .iter()
1494 .map(|shard| shard.remaining.load(Ordering::Relaxed))
1495 .collect();
1496 assert_eq!(remaining, [4, 4, 4, 3]);
1497 l.credit(&debit);
1498
1499 let fragmented = l
1500 .try_reserve_at(CostUnits(5), t(0), Locality::for_test(3))
1501 .unwrap();
1502 let remaining: Vec<_> = l
1503 .inner
1504 .balance
1505 .as_slice()
1506 .iter()
1507 .map(|shard| shard.remaining.load(Ordering::Relaxed))
1508 .collect();
1509 assert_eq!(remaining, [3, 4, 4, 0]);
1510 l.credit(&fragmented);
1511 assert_eq!(l.remaining(), CostUnits(16));
1512 }
1513
1514 /// A sibling that can satisfy the whole debit is the routine fallback,
1515 /// not fragmentation. Keeping the debit on one counter is what avoids an
1516 /// O(shards) gather and concentrates its cancellation on the counter that
1517 /// actually paid it.
1518 #[test]
1519 fn a_whole_sibling_is_used_before_fragmenting() {
1520 let l = sharded_lease(16, 0, 4);
1521
1522 // Leave locality 3 with three units while locality 0 still has four.
1523 let first = l
1524 .try_reserve_at(CostUnits(1), t(0), Locality::for_test(3))
1525 .unwrap();
1526 let sibling = l
1527 .try_reserve_at(CostUnits(4), t(0), Locality::for_test(3))
1528 .unwrap();
1529
1530 let remaining: Vec<_> = l
1531 .inner
1532 .balance
1533 .as_slice()
1534 .iter()
1535 .map(|shard| shard.remaining.load(Ordering::Relaxed))
1536 .collect();
1537 assert_eq!(
1538 remaining,
1539 [0, 4, 4, 3],
1540 "the whole sibling pays; the undersized local shard is untouched"
1541 );
1542
1543 l.credit(&sibling);
1544 l.credit(&first);
1545 assert_eq!(l.remaining(), CostUnits(16));
1546 }
1547
1548 #[test]
1549 fn a_torn_aggregate_above_the_grant_reads_as_the_grant() {
1550 let l = sharded_lease(4, 0, 2);
1551 // The state a concurrent walk can observe: the reader counted shard 0
1552 // at two units, then a failed fragmented reservation drained both
1553 // shards and returned their whole aggregate to shard 1 — which the
1554 // reader has not visited yet. Its sum is six against a grant of four.
1555 let LeaseBalance::Sharded(shards) = &l.inner.balance else {
1556 panic!("a two-shard lease is sharded");
1557 };
1558 shards[1].remaining.store(4, Ordering::Release);
1559
1560 assert_eq!(
1561 l.remaining(),
1562 CostUnits(4),
1563 "an over-counted walk is clamped to the grant, never asserted"
1564 );
1565 }
1566
1567 #[test]
1568 fn a_fragmenting_rollback_never_panics_a_concurrent_aggregate_read() {
1569 // The rollback path concentrates a failed reservation's aggregate on
1570 // one shard, so a reader partway through its walk can count the same
1571 // units twice. Before the clamp that tripped `remaining`'s debug
1572 // assertion here, and its checked-add in release.
1573 let l = Arc::new(sharded_lease(6_400, 0, 64));
1574 let stop = Arc::new(AtomicBool::new(false));
1575 let spenders: Vec<_> = (48..52)
1576 .map(|locality| {
1577 let l = Arc::clone(&l);
1578 let stop = Arc::clone(&stop);
1579 std::thread::spawn(move || {
1580 while !stop.load(Ordering::Relaxed) {
1581 // One unit more than the whole grant: every attempt
1582 // drains all sixty-four shards, fails, and rolls the
1583 // aggregate back onto this locality's own shard —
1584 // far enough along the walk to be reached after a
1585 // reader has already counted the shards before it.
1586 drop(l.try_reserve_at(
1587 CostUnits(6_401),
1588 t(0),
1589 Locality::for_test(locality),
1590 ));
1591 }
1592 })
1593 })
1594 .collect();
1595 for _ in 0..200_000 {
1596 assert!(l.remaining() <= CostUnits(6_400));
1597 }
1598 stop.store(true, Ordering::Relaxed);
1599 for spender in spenders {
1600 spender.join().unwrap();
1601 }
1602 assert_eq!(
1603 l.remaining(),
1604 CostUnits(6_400),
1605 "no units were lost or created"
1606 );
1607 }
1608
1609 #[test]
1610 fn an_early_shard_signal_is_rearmed_until_the_aggregate_crosses() {
1611 let signal = Arc::new(CountingSignal::default());
1612 let l = sharded_lease(100, 20, 2).with_refill(signal.clone());
1613
1614 l.try_debit(CostUnits(40), t(0)).unwrap();
1615 assert_eq!(signal.count(), 1, "the first shard crossed its share");
1616 assert_eq!(
1617 l.refill_due_or_rearm(),
1618 RefillVerdict::Idle,
1619 "sixty aggregate units remain"
1620 );
1621
1622 l.try_debit(CostUnits(1), t(0)).unwrap();
1623 assert_eq!(signal.count(), 2, "the control-plane check rearmed it");
1624 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Idle);
1625
1626 l.try_debit(CostUnits(40), t(0)).unwrap();
1627 assert_eq!(l.remaining(), CostUnits(19));
1628 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Draining);
1629 }
1630
1631 #[test]
1632 fn debit_decrements_and_credit_restores() {
1633 let l = lease(100, 1_000, 25);
1634 let debit = l
1635 .try_reserve_at(CostUnits(60), t(0), Locality::current())
1636 .unwrap();
1637 assert_eq!(l.remaining(), CostUnits(40));
1638 l.credit(&debit);
1639 assert_eq!(l.remaining(), CostUnits(100));
1640 }
1641
1642 #[test]
1643 fn exhaustion_denies_with_remaining() {
1644 let l = lease(10, 1_000, 0);
1645 assert_eq!(
1646 l.try_debit(CostUnits(11), t(0)),
1647 Err(DenyReason::LeaseExhausted {
1648 remaining: CostUnits(10)
1649 })
1650 );
1651 // Exact spend-to-zero is allowed.
1652 l.try_debit(CostUnits(10), t(0)).unwrap();
1653 assert_eq!(l.remaining(), CostUnits::ZERO);
1654 }
1655
1656 #[test]
1657 fn expiry_boundary_is_exclusive_of_expires_at() {
1658 let l = lease(10, 500, 0);
1659 assert_eq!(
1660 l.try_debit(CostUnits(1), t(500)),
1661 Err(DenyReason::LeaseExpired)
1662 );
1663 l.try_debit(CostUnits(1), t(499)).unwrap();
1664 }
1665
1666 #[test]
1667 fn low_water_triggers_refill_signal() {
1668 let l = lease(100, 1_000, 25);
1669 assert!(!l.needs_refill());
1670 l.try_debit(CostUnits(75), t(0)).unwrap();
1671 assert!(l.needs_refill());
1672 }
1673
1674 /// Counts calls so the exactly-once contract can be asserted rather than
1675 /// assumed.
1676 #[derive(Debug, Default)]
1677 struct CountingSignal(AtomicU64);
1678
1679 impl RefillSignal for CountingSignal {
1680 fn request_refill(&self) {
1681 self.0.fetch_add(1, Ordering::Relaxed);
1682 }
1683 }
1684
1685 impl CountingSignal {
1686 fn count(&self) -> u64 {
1687 self.0.load(Ordering::Relaxed)
1688 }
1689 }
1690
1691 /// The signal fires on the debit that *crosses* low water — not before,
1692 /// and, because the threshold is "at or below", not one debit late.
1693 #[test]
1694 fn the_crossing_debit_raises_the_signal() {
1695 let signal = Arc::new(CountingSignal::default());
1696 let l = lease(100, 1_000, 25).with_refill(signal.clone());
1697
1698 l.try_debit(CostUnits(74), t(0)).unwrap();
1699 assert_eq!(signal.count(), 0, "26 remaining is above low water");
1700 l.try_debit(CostUnits(1), t(0)).unwrap();
1701 assert_eq!(signal.count(), 1, "landing exactly on low water crosses it");
1702 }
1703
1704 /// GL-109: the refusal is the one lease event that *proves* the grant can
1705 /// no longer serve the work offered, and it was the one event the refill
1706 /// plane was never told about. A low-water crossing cannot stand in for
1707 /// it: this lease is comfortably above its mark and still cannot fund the
1708 /// quote, so nothing crosses, and before this the plane learned nothing.
1709 #[test]
1710 fn a_refused_debit_tells_the_refill_plane_rather_than_waiting_to_be_polled() {
1711 let signal = Arc::new(CountingSignal::default());
1712 let l = lease(49, 1_000, 25).with_refill(signal.clone());
1713
1714 assert!(matches!(
1715 l.try_debit(CostUnits(51), t(0)),
1716 Err(DenyReason::LeaseExhausted { .. })
1717 ));
1718 assert_eq!(signal.count(), 1, "the refusal rang the doorbell");
1719 assert!(
1720 !l.needs_refill(),
1721 "and it rang from above the mark, which is the whole point"
1722 );
1723 assert_eq!(
1724 l.refill_due_or_rearm(),
1725 RefillVerdict::Refused,
1726 "the plane is told the grant is mis-sized, not that it is draining"
1727 );
1728 assert_eq!(
1729 l.refill_due_or_rearm(),
1730 RefillVerdict::Idle,
1731 "reading the verdict consumes it; one refusal is one rotation"
1732 );
1733 assert_eq!(
1734 l.remaining(),
1735 CostUnits(49),
1736 "a refusal is still zero-charge"
1737 );
1738 }
1739
1740 /// GL-131: a consolidation may grow only to demand the lease has proven, so
1741 /// the refusal records its quote, keeps the largest across a storm, and an
1742 /// expiry refusal records nothing because it rotates rather than folds.
1743 #[test]
1744 fn a_refused_debit_records_its_largest_quote() {
1745 for l in [lease(49, 1_000, 25), sharded_lease(49, 25, 4)] {
1746 assert_eq!(l.largest_refused_quote(), CostUnits::ZERO);
1747 for quote in [51, 90, 60] {
1748 assert!(l.try_debit(CostUnits(quote), t(0)).is_err());
1749 }
1750 assert_eq!(l.largest_refused_quote(), CostUnits(90));
1751 l.try_debit(CostUnits(10), t(0)).unwrap();
1752 assert_eq!(
1753 l.largest_refused_quote(),
1754 CostUnits(90),
1755 "a funded debit is not demand the grant failed"
1756 );
1757 }
1758 let expired = lease(49, 1_000, 25);
1759 assert!(matches!(
1760 expired.try_debit(CostUnits(51), t(1_000)),
1761 Err(DenyReason::LeaseExpired)
1762 ));
1763 assert_eq!(expired.largest_refused_quote(), CostUnits::ZERO);
1764 }
1765
1766 /// The doorbell is rung once however hard the caller retries: the plane's
1767 /// response does not depend on how many requests were refused, and a
1768 /// refusal storm must not become a wake storm on the refill task.
1769 #[test]
1770 fn a_refusal_storm_rings_once_and_reports_once() {
1771 let signal = Arc::new(CountingSignal::default());
1772 let l = lease(49, 1_000, 25).with_refill(signal.clone());
1773
1774 for _ in 0..1_000 {
1775 assert!(l.try_debit(CostUnits(51), t(0)).is_err());
1776 }
1777 assert_eq!(signal.count(), 1);
1778 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1779
1780 // Cleared, so a later refusal is a fresh report the plane must act on.
1781 assert!(l.try_debit(CostUnits(51), t(0)).is_err());
1782 assert_eq!(signal.count(), 2);
1783 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1784 }
1785
1786 /// A refusal outranks a crossing because the two ask for different things
1787 /// and only one is still preventable. Rotating *alongside* a lease that
1788 /// already refused work would size the next grant against a balance this
1789 /// lease's unspent units are missing from (GL-109).
1790 #[test]
1791 fn a_refusal_outranks_a_low_water_crossing() {
1792 let l = lease(100, 1_000, 90);
1793
1794 // One debit both crosses low water and leaves too little for the next.
1795 l.try_debit(CostUnits(20), t(0)).unwrap();
1796 assert!(l.needs_refill(), "80 remaining is below the 90 mark");
1797 assert!(l.try_debit(CostUnits(81), t(0)).is_err());
1798
1799 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1800 assert_eq!(
1801 l.refill_due_or_rearm(),
1802 RefillVerdict::Draining,
1803 "the crossing is still there once the refusal has been answered"
1804 );
1805 }
1806
1807 /// The expiry refusal was silent for the same reason the exhaustion one
1808 /// was, and is the same defect. Rollover keeps the poll interval as its
1809 /// backstop for a lease no request touches; a lease requests *are*
1810 /// reaching now says so on the first one (INVARIANTS.md GL-6).
1811 #[test]
1812 fn an_expired_lease_reports_its_refusal_rather_than_waiting_for_the_tick() {
1813 let signal = Arc::new(CountingSignal::default());
1814 let l = lease(100, 1_000, 25).with_refill(signal.clone());
1815
1816 assert_eq!(
1817 l.try_debit(CostUnits(1), t(2_000)),
1818 Err(DenyReason::LeaseExpired)
1819 );
1820 assert_eq!(signal.count(), 1);
1821 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1822 }
1823
1824 /// A lease nobody refills still records the refusal, exactly as it records
1825 /// a crossing: the verdict is lease state, and attaching a doorbell only
1826 /// decides whether the plane hears about it sooner.
1827 #[test]
1828 fn a_lease_with_no_doorbell_still_records_its_refusal() {
1829 let l = lease(49, 1_000, 25);
1830 assert!(l.try_debit(CostUnits(51), t(0)).is_err());
1831 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1832 }
1833
1834 /// A sharded lease refuses only after walking every sibling, so the report
1835 /// is about the grant and not about one counter — which is why the flag is
1836 /// lease-wide and one refusal is one report however many shards it tried.
1837 #[test]
1838 fn a_sharded_refusal_reports_once_for_the_whole_grant() {
1839 let signal = Arc::new(CountingSignal::default());
1840 let l = sharded_lease(40, 4, 4).with_refill(signal.clone());
1841
1842 assert!(matches!(
1843 l.try_debit(CostUnits(41), t(0)),
1844 Err(DenyReason::LeaseExhausted { .. })
1845 ));
1846 assert_eq!(signal.count(), 1);
1847 assert_eq!(l.remaining(), CostUnits(40), "the rollback restored it all");
1848 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1849 }
1850
1851 /// A lease is replaced rather than refilled, so "at most once" needs no
1852 /// reset protocol — but it does need proving, since every later debit on
1853 /// a drained lease still tests the branch.
1854 #[test]
1855 fn a_lease_signals_at_most_once_however_long_it_drains() {
1856 let signal = Arc::new(CountingSignal::default());
1857 let l = lease(100, 1_000, 25).with_refill(signal.clone());
1858
1859 // Eighty single-unit debits against a hundred units: every one is
1860 // admissible, so a failure here would be the test lying, not the
1861 // lease refusing.
1862 for _ in 0..80 {
1863 l.try_debit(CostUnits(1), t(0)).unwrap();
1864 }
1865 assert_eq!(l.remaining(), CostUnits(20));
1866 assert_eq!(
1867 signal.count(),
1868 1,
1869 "one crossing, however many debits followed it"
1870 );
1871
1872 // A fresh lease is a fresh flag: this is the whole reset mechanism.
1873 let next = lease(100, 1_000, 25).with_refill(signal.clone());
1874 next.try_debit(CostUnits(80), t(0)).unwrap();
1875 assert_eq!(signal.count(), 2);
1876 }
1877
1878 /// A refused debit changes no counter, so it must never claim a
1879 /// *crossing* — and it must still report the refusal.
1880 ///
1881 /// This test used to assert the silence outright (`a_refused_debit_never
1882 /// _signals`), on the reasoning that a debit which moved no counter has
1883 /// nothing to announce. The premise held for crossings and hid GL-109: it
1884 /// read the doorbell as "low water was crossed" when what the refill
1885 /// plane needs is "act on this lease", and those differ exactly here.
1886 /// Both facts are now representable at once, so neither has to be given
1887 /// up: the verdict distinguishes them and the shard flags stay clear.
1888 #[test]
1889 fn a_refused_debit_reports_a_refusal_and_never_a_crossing() {
1890 let signal = Arc::new(CountingSignal::default());
1891 let l = lease(100, 1_000, 25).with_refill(signal.clone());
1892
1893 assert!(l.try_debit(CostUnits(500), t(0)).is_err(), "exhausted");
1894 assert_eq!(signal.count(), 1, "the plane is told once");
1895 assert!(
1896 l.try_debit(CostUnits(10), t(10_000)).is_err(),
1897 "past the usability window"
1898 );
1899 assert_eq!(
1900 signal.count(),
1901 1,
1902 "and not again while that report still stands"
1903 );
1904 assert_eq!(l.remaining(), CostUnits(100), "still zero-charge");
1905
1906 assert_eq!(l.refill_due_or_rearm(), RefillVerdict::Refused);
1907 assert_eq!(
1908 l.refill_due_or_rearm(),
1909 RefillVerdict::Idle,
1910 "seventy-five units above the mark: no crossing was ever claimed"
1911 );
1912 }
1913
1914 /// A lease with no signal attached is the pre-GL-10 behaviour: the crossing
1915 /// is still recorded for the poll loop, it simply arrives later.
1916 #[test]
1917 fn a_lease_without_a_signal_still_reports_the_crossing() {
1918 let l = lease(100, 1_000, 25);
1919 l.try_debit(CostUnits(80), t(0)).unwrap();
1920 assert!(l.needs_refill());
1921 }
1922
1923 #[test]
1924 fn negative_safety_margin_fails_closed() {
1925 let grant = LeaseGrant {
1926 lease_id: LeaseId(8),
1927 account_id: AccountId(1),
1928 fencing_token: FencingToken(4),
1929 units: CostUnits(10),
1930 expires_at: t(100),
1931 };
1932 let l = LocalLease::with_safety_margin(
1933 grant,
1934 CostUnits::ZERO,
1935 jiff::SignedDuration::from_secs(-10),
1936 );
1937 assert_eq!(
1938 l.try_debit(CostUnits(1), t(99)),
1939 Err(DenyReason::LeaseExpired)
1940 );
1941 }
1942
1943 #[test]
1944 fn concurrent_debits_never_overspend() {
1945 use std::sync::Arc;
1946 let l = Arc::new(lease(1_000, 1_000, 0));
1947 let mut handles = Vec::new();
1948 for _ in 0..8 {
1949 let l = Arc::clone(&l);
1950 handles.push(std::thread::spawn(move || {
1951 let mut granted = 0u64;
1952 for _ in 0..1_000 {
1953 if l.try_debit(CostUnits(1), t(0)).is_ok() {
1954 granted += 1;
1955 }
1956 }
1957 granted
1958 }));
1959 }
1960 let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
1961 // 8000 attempts against 1000 units: exactly the lease size is granted.
1962 assert_eq!(total, 1_000);
1963 assert_eq!(l.remaining(), CostUnits::ZERO);
1964 }
1965}