1use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
12
13#[derive(Debug, Clone)]
15pub struct SharedTokenBudget {
16 used: Arc<AtomicU64>,
17 cap: u64,
18}
19
20impl SharedTokenBudget {
21 pub fn new(cap: u64) -> Self {
22 Self {
23 used: Arc::new(AtomicU64::new(0)),
24 cap,
25 }
26 }
27
28 pub fn used(&self) -> u64 {
29 self.used.load(Ordering::Relaxed)
30 }
31
32 pub fn cap(&self) -> u64 {
33 self.cap
34 }
35
36 fn try_add(&self, tokens: u64) -> Result<(), u64> {
41 let mut current = self.used.load(Ordering::Relaxed);
42 loop {
43 let next = current.saturating_add(tokens);
44 if next > self.cap {
45 return Err(next);
46 }
47 match self.used.compare_exchange_weak(
48 current,
49 next,
50 Ordering::Relaxed,
51 Ordering::Relaxed,
52 ) {
53 Ok(_) => return Ok(()),
54 Err(observed) => current = observed,
55 }
56 }
57 }
58
59 fn add_unchecked(&self, tokens: u64) {
61 let mut current = self.used.load(Ordering::Relaxed);
62 loop {
63 let next = current.saturating_add(tokens);
64 match self.used.compare_exchange_weak(
65 current,
66 next,
67 Ordering::Relaxed,
68 Ordering::Relaxed,
69 ) {
70 Ok(_) => return,
71 Err(observed) => current = observed,
72 }
73 }
74 }
75
76 fn sub(&self, tokens: u64) {
78 let mut current = self.used.load(Ordering::Relaxed);
79 loop {
80 let next = current.saturating_sub(tokens);
81 match self.used.compare_exchange_weak(
82 current,
83 next,
84 Ordering::Relaxed,
85 Ordering::Relaxed,
86 ) {
87 Ok(_) => return,
88 Err(observed) => current = observed,
89 }
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct LedgerLimits {
98 pub per_execution_tokens: u64,
100 pub max_held_tokens: u64,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum LedgerRefusal {
109 HeldReserve { held: u64, limit: u64 },
111 PerExecution { requested: u64, limit: u64 },
113 AllExecutions { requested: u64, limit: u64 },
115}
116
117#[derive(Debug, Default)]
119pub(super) struct TokenState {
120 used: u64,
122 held: u64,
126}
127
128#[derive(Debug)]
147pub struct TokenLedger {
148 limits: LedgerLimits,
149 state: Mutex<TokenState>,
150 aggregate: SharedTokenBudget,
151}
152
153impl TokenLedger {
154 pub fn new(limits: LedgerLimits, aggregate: SharedTokenBudget) -> Self {
155 Self {
156 limits,
157 state: Mutex::new(TokenState::default()),
158 aggregate,
159 }
160 }
161
162 pub fn used(&self) -> u64 {
164 self.locked().used
165 }
166
167 pub fn held(&self) -> u64 {
169 self.locked().held
170 }
171
172 fn locked(&self) -> MutexGuard<'_, TokenState> {
173 self.state.lock().unwrap_or_else(PoisonError::into_inner)
176 }
177
178 #[cfg(test)]
179 pub(super) fn lock_state(&self) -> std::sync::LockResult<MutexGuard<'_, TokenState>> {
180 self.state.lock()
181 }
182
183 #[cfg(test)]
184 pub(super) fn state_is_poisoned(&self) -> bool {
185 self.state.is_poisoned()
186 }
187
188 pub fn reserve(&self, tokens: u64) -> Result<(), LedgerRefusal> {
190 let mut state = self.locked();
191
192 if state.held > self.limits.max_held_tokens {
193 return Err(LedgerRefusal::HeldReserve {
194 held: state.held,
195 limit: self.limits.max_held_tokens,
196 });
197 }
198
199 let next_used = state.used.saturating_add(tokens);
200 if next_used > self.limits.per_execution_tokens {
201 return Err(LedgerRefusal::PerExecution {
202 requested: next_used,
203 limit: self.limits.per_execution_tokens,
204 });
205 }
206
207 if let Err(requested) = self.aggregate.try_add(tokens) {
208 return Err(LedgerRefusal::AllExecutions {
209 requested,
210 limit: self.aggregate.cap(),
211 });
212 }
213
214 state.used = next_used;
215 Ok(())
216 }
217
218 pub fn reconcile(&self, reserved: u64, reported: u64, indeterminate: u64) {
228 let mut state = self.locked();
229 let keep = reported.saturating_add(indeterminate);
230 let release = reserved.saturating_sub(keep);
231 state.used = state.used.saturating_sub(release);
232 state.held = state.held.saturating_add(indeterminate);
233 if release > 0 {
234 self.aggregate.sub(release);
235 }
236 }
237}
238
239impl TokenLedger {
240 pub fn settle_held(&self, held_estimate: u64, reported: u64, indeterminate: u64) {
250 let mut state = self.locked();
251 state.held = state.held.saturating_sub(held_estimate);
252 let keep = reported.saturating_add(indeterminate);
253 let release = held_estimate.saturating_sub(keep);
254 let excess = keep.saturating_sub(held_estimate);
255 state.used = state.used.saturating_sub(release).saturating_add(excess);
256 state.held = state.held.saturating_add(indeterminate);
257 if release > 0 {
258 self.aggregate.sub(release);
259 }
260 if excess > 0 {
261 self.aggregate.add_unchecked(excess);
262 }
263 }
264}
265
266impl Drop for TokenLedger {
267 fn drop(&mut self) {
268 let state = self.state.get_mut().unwrap_or_else(PoisonError::into_inner);
271 let returnable = state.used.saturating_sub(state.held);
272 if returnable > 0 {
273 self.aggregate.sub(returnable);
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 fn limits(per_execution_tokens: u64, max_held_tokens: u64) -> LedgerLimits {
283 LedgerLimits {
284 per_execution_tokens,
285 max_held_tokens,
286 }
287 }
288
289 #[test]
290 fn aggregate_cap_binds_across_ledgers_and_refunds_on_drop() {
291 let aggregate = SharedTokenBudget::new(100);
292 let first = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
293 let second = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
294
295 first.reserve(60).expect("first fits");
296 assert_eq!(
297 second.reserve(50),
298 Err(LedgerRefusal::AllExecutions {
299 requested: 110,
300 limit: 100
301 })
302 );
303 assert_eq!(aggregate.used(), 60, "a refusal charges nothing");
304 second.reserve(40).expect("second fills the remainder");
305 assert_eq!(aggregate.used(), 100);
306
307 drop(first);
308 assert_eq!(aggregate.used(), 40, "drop refunds the first ledger's 60");
309 second.reserve(60).expect("the refund is reusable");
310 }
311
312 #[test]
313 fn held_reserve_stays_charged_to_the_aggregate_after_drop() {
314 let aggregate = SharedTokenBudget::new(1_000);
315 {
316 let ledger = TokenLedger::new(limits(1_000, 1_000), aggregate.clone());
317 ledger.reserve(300).expect("fits");
318 ledger.reconcile(300, 100, 50);
319 assert_eq!(ledger.used(), 150);
320 assert_eq!(ledger.held(), 50);
321 assert_eq!(aggregate.used(), 150);
322 }
323 assert_eq!(aggregate.used(), 50);
324 }
325
326 #[test]
327 fn held_reserve_past_the_ceiling_refuses_further_reservation() {
328 let ledger = TokenLedger::new(limits(1_000, 10), SharedTokenBudget::new(1_000));
329 ledger.reserve(100).expect("fits");
330 ledger.reconcile(100, 0, 100);
331 assert_eq!(
332 ledger.reserve(1),
333 Err(LedgerRefusal::HeldReserve {
334 held: 100,
335 limit: 10
336 })
337 );
338 }
339
340 #[test]
341 fn settle_held_charges_reported_usage_above_the_estimate() {
342 let aggregate = SharedTokenBudget::new(5);
343 let ledger = TokenLedger::new(limits(5, 100), aggregate.clone());
344 ledger.reserve(1).expect("fits");
345 ledger.reconcile(1, 0, 1);
346 ledger.settle_held(1, 7, 0);
347 assert_eq!((ledger.used(), ledger.held(), aggregate.used()), (7, 0, 7));
348 assert!(
349 ledger.reserve(1).is_err(),
350 "spend past the caps refuses the next reservation"
351 );
352 }
353
354 #[test]
355 fn a_reservation_beyond_i64_max_is_refused_not_wrapped() {
356 let aggregate = SharedTokenBudget::new(10);
357 let ledger = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
358 assert!(ledger.reserve(u64::MAX).is_err());
359 assert_eq!(aggregate.used(), 0);
360 }
361}