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 replayed_used: u64,
124 replayed_held: u64,
125 held: u64,
129}
130
131#[derive(Debug)]
150pub struct TokenLedger {
151 limits: LedgerLimits,
152 state: Mutex<TokenState>,
153 aggregate: SharedTokenBudget,
154}
155
156impl TokenLedger {
157 pub fn new(limits: LedgerLimits, aggregate: SharedTokenBudget) -> Self {
158 Self {
159 limits,
160 state: Mutex::new(TokenState::default()),
161 aggregate,
162 }
163 }
164
165 pub fn used(&self) -> u64 {
167 let state = self.locked();
168 state.used.saturating_add(state.replayed_used)
169 }
170
171 pub fn held(&self) -> u64 {
173 let state = self.locked();
174 state.held.saturating_add(state.replayed_held)
175 }
176
177 pub fn restore_spend(&self, used: u64, held: u64) {
180 let mut state = self.locked();
181 state.replayed_used = state.replayed_used.saturating_add(used);
182 state.replayed_held = state.replayed_held.saturating_add(held);
183 }
184
185 fn locked(&self) -> MutexGuard<'_, TokenState> {
186 self.state.lock().unwrap_or_else(PoisonError::into_inner)
189 }
190
191 #[cfg(test)]
192 pub(super) fn lock_state(&self) -> std::sync::LockResult<MutexGuard<'_, TokenState>> {
193 self.state.lock()
194 }
195
196 #[cfg(test)]
197 pub(super) fn state_is_poisoned(&self) -> bool {
198 self.state.is_poisoned()
199 }
200
201 pub fn reserve(&self, tokens: u64) -> Result<(), LedgerRefusal> {
203 let mut state = self.locked();
204
205 let held = state.held.saturating_add(state.replayed_held);
206 if held > self.limits.max_held_tokens {
207 return Err(LedgerRefusal::HeldReserve {
208 held,
209 limit: self.limits.max_held_tokens,
210 });
211 }
212
213 let next_used = state
214 .used
215 .saturating_add(state.replayed_used)
216 .saturating_add(tokens);
217 if next_used > self.limits.per_execution_tokens {
218 return Err(LedgerRefusal::PerExecution {
219 requested: next_used,
220 limit: self.limits.per_execution_tokens,
221 });
222 }
223
224 if let Err(requested) = self.aggregate.try_add(tokens) {
225 return Err(LedgerRefusal::AllExecutions {
226 requested,
227 limit: self.aggregate.cap(),
228 });
229 }
230
231 state.used = state.used.saturating_add(tokens);
232 Ok(())
233 }
234
235 pub fn reconcile(&self, reserved: u64, reported: u64, indeterminate: u64) {
245 let mut state = self.locked();
246 let keep = reported.saturating_add(indeterminate);
247 let release = reserved.saturating_sub(keep);
248 state.used = state.used.saturating_sub(release);
249 state.held = state.held.saturating_add(indeterminate);
250 if release > 0 {
251 self.aggregate.sub(release);
252 }
253 }
254}
255
256impl TokenLedger {
257 pub fn settle_held(&self, held_estimate: u64, reported: u64, indeterminate: u64) {
267 let mut state = self.locked();
268 state.held = state.held.saturating_sub(held_estimate);
269 let keep = reported.saturating_add(indeterminate);
270 let release = held_estimate.saturating_sub(keep);
271 let excess = keep.saturating_sub(held_estimate);
272 state.used = state.used.saturating_sub(release).saturating_add(excess);
273 state.held = state.held.saturating_add(indeterminate);
274 if release > 0 {
275 self.aggregate.sub(release);
276 }
277 if excess > 0 {
278 self.aggregate.add_unchecked(excess);
279 }
280 }
281}
282
283impl Drop for TokenLedger {
284 fn drop(&mut self) {
285 let state = self.state.get_mut().unwrap_or_else(PoisonError::into_inner);
288 let returnable = state.used.saturating_sub(state.held);
289 if returnable > 0 {
290 self.aggregate.sub(returnable);
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn limits(per_execution_tokens: u64, max_held_tokens: u64) -> LedgerLimits {
300 LedgerLimits {
301 per_execution_tokens,
302 max_held_tokens,
303 }
304 }
305
306 #[test]
307 fn aggregate_cap_binds_across_ledgers_and_refunds_on_drop() {
308 let aggregate = SharedTokenBudget::new(100);
309 let first = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
310 let second = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
311
312 first.reserve(60).expect("first fits");
313 assert_eq!(
314 second.reserve(50),
315 Err(LedgerRefusal::AllExecutions {
316 requested: 110,
317 limit: 100
318 })
319 );
320 assert_eq!(aggregate.used(), 60, "a refusal charges nothing");
321 second.reserve(40).expect("second fills the remainder");
322 assert_eq!(aggregate.used(), 100);
323
324 drop(first);
325 assert_eq!(aggregate.used(), 40, "drop refunds the first ledger's 60");
326 second.reserve(60).expect("the refund is reusable");
327 }
328
329 #[test]
330 fn held_reserve_stays_charged_to_the_aggregate_after_drop() {
331 let aggregate = SharedTokenBudget::new(1_000);
332 {
333 let ledger = TokenLedger::new(limits(1_000, 1_000), aggregate.clone());
334 ledger.reserve(300).expect("fits");
335 ledger.reconcile(300, 100, 50);
336 assert_eq!(ledger.used(), 150);
337 assert_eq!(ledger.held(), 50);
338 assert_eq!(aggregate.used(), 150);
339 }
340 assert_eq!(aggregate.used(), 50);
341 }
342
343 #[test]
344 fn held_reserve_past_the_ceiling_refuses_further_reservation() {
345 let ledger = TokenLedger::new(limits(1_000, 10), SharedTokenBudget::new(1_000));
346 ledger.reserve(100).expect("fits");
347 ledger.reconcile(100, 0, 100);
348 assert_eq!(
349 ledger.reserve(1),
350 Err(LedgerRefusal::HeldReserve {
351 held: 100,
352 limit: 10
353 })
354 );
355 }
356
357 #[test]
358 fn settle_held_charges_reported_usage_above_the_estimate() {
359 let aggregate = SharedTokenBudget::new(5);
360 let ledger = TokenLedger::new(limits(5, 100), aggregate.clone());
361 ledger.reserve(1).expect("fits");
362 ledger.reconcile(1, 0, 1);
363 ledger.settle_held(1, 7, 0);
364 assert_eq!((ledger.used(), ledger.held(), aggregate.used()), (7, 0, 7));
365 assert!(
366 ledger.reserve(1).is_err(),
367 "spend past the caps refuses the next reservation"
368 );
369 }
370
371 #[test]
372 fn a_reservation_beyond_i64_max_is_refused_not_wrapped() {
373 let aggregate = SharedTokenBudget::new(10);
374 let ledger = TokenLedger::new(limits(u64::MAX, 0), aggregate.clone());
375 assert!(ledger.reserve(u64::MAX).is_err());
376 assert_eq!(aggregate.used(), 0);
377 }
378}
379
380#[cfg(test)]
381mod replay_tests {
382 use super::*;
383 #[test]
384 fn restored_spend_limits_the_run_without_reserving_shared_tokens_twice() {
385 let aggregate = SharedTokenBudget::new(100);
386 let ledger = TokenLedger::new(
387 LedgerLimits {
388 per_execution_tokens: 10,
389 max_held_tokens: 5,
390 },
391 aggregate.clone(),
392 );
393 ledger.restore_spend(8, 2);
394 assert_eq!(ledger.used(), 8);
395 assert_eq!(aggregate.used(), 0);
396 assert!(ledger.reserve(3).is_err());
397 ledger.reserve(2).unwrap();
398 assert_eq!(ledger.used(), 10);
399 assert_eq!(aggregate.used(), 2);
400 ledger.reconcile(2, 1, 0);
401 assert_eq!(ledger.used(), 9);
402 assert_eq!(ledger.held(), 2);
403 assert_eq!(aggregate.used(), 1);
404 drop(ledger);
405 assert_eq!(aggregate.used(), 0);
406 }
407 #[test]
408 fn restored_uncertain_usage_preserves_the_held_ceiling() {
409 let ledger = TokenLedger::new(
410 LedgerLimits {
411 per_execution_tokens: 100,
412 max_held_tokens: 5,
413 },
414 SharedTokenBudget::new(100),
415 );
416 ledger.restore_spend(6, 6);
417 assert!(matches!(
418 ledger.reserve(1),
419 Err(LedgerRefusal::HeldReserve { .. })
420 ));
421 }
422}