llm_kernel/tokens/
budget.rs1use core::sync::atomic::{AtomicU32, Ordering};
7
8pub struct TokenBudget {
13 total: u32,
15 used: AtomicU32,
17}
18
19impl TokenBudget {
20 pub fn new(total: u32) -> Self {
24 Self {
25 total,
26 used: AtomicU32::new(0),
27 }
28 }
29
30 pub fn total(&self) -> u32 {
32 self.total
33 }
34
35 pub fn remaining(&self) -> u32 {
37 self.total.saturating_sub(self.used.load(Ordering::Relaxed))
38 }
39
40 pub fn used(&self) -> u32 {
42 self.used.load(Ordering::Relaxed)
43 }
44
45 pub fn try_reserve(&self, n: u32) -> bool {
50 loop {
51 let current = self.used.load(Ordering::Relaxed);
52 let new_used = current.saturating_add(n);
53 if new_used > self.total {
54 return false;
55 }
56 match self.used.compare_exchange_weak(
57 current,
58 new_used,
59 Ordering::Relaxed,
60 Ordering::Relaxed,
61 ) {
62 Ok(_) => return true,
63 Err(_) => continue,
64 }
65 }
66 }
67
68 pub fn release(&self, n: u32) {
72 loop {
73 let current = self.used.load(Ordering::Relaxed);
74 let new_used = current.saturating_sub(n);
75 match self.used.compare_exchange_weak(
76 current,
77 new_used,
78 Ordering::Relaxed,
79 Ordering::Relaxed,
80 ) {
81 Ok(_) => return,
82 Err(_) => continue,
83 }
84 }
85 }
86
87 pub fn reset(&self) {
89 self.used.store(0, Ordering::Relaxed);
90 }
91}
92
93impl std::fmt::Debug for TokenBudget {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("TokenBudget")
96 .field("total", &self.total)
97 .field("used", &self.used())
98 .field("remaining", &self.remaining())
99 .finish()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn new_budget_starts_empty() {
109 let b = TokenBudget::new(1000);
110 assert_eq!(b.total(), 1000);
111 assert_eq!(b.used(), 0);
112 assert_eq!(b.remaining(), 1000);
113 }
114
115 #[test]
116 fn reserve_success() {
117 let b = TokenBudget::new(1000);
118 assert!(b.try_reserve(600));
119 assert_eq!(b.remaining(), 400);
120 assert_eq!(b.used(), 600);
121 }
122
123 #[test]
124 fn reserve_insufficient() {
125 let b = TokenBudget::new(1000);
126 assert!(b.try_reserve(600));
127 assert!(!b.try_reserve(500));
128 assert_eq!(b.remaining(), 400);
129 }
130
131 #[test]
132 fn release_tokens() {
133 let b = TokenBudget::new(1000);
134 b.try_reserve(600);
135 b.release(200);
136 assert_eq!(b.remaining(), 600);
137 assert_eq!(b.used(), 400);
138 }
139
140 #[test]
141 fn release_clamps_to_zero() {
142 let b = TokenBudget::new(1000);
143 b.try_reserve(100);
144 b.release(200);
145 assert_eq!(b.used(), 0);
146 assert_eq!(b.remaining(), 1000);
147 }
148
149 #[test]
150 fn reset_clears_usage() {
151 let b = TokenBudget::new(1000);
152 b.try_reserve(500);
153 b.reset();
154 assert_eq!(b.used(), 0);
155 assert_eq!(b.remaining(), 1000);
156 }
157
158 #[test]
159 fn debug_format() {
160 let b = TokenBudget::new(1000);
161 b.try_reserve(300);
162 let debug = format!("{:?}", b);
163 assert!(debug.contains("total: 1000"));
164 assert!(debug.contains("used: 300"));
165 assert!(debug.contains("remaining: 700"));
166 }
167
168 #[test]
169 fn reserve_exact_total() {
170 let b = TokenBudget::new(100);
171 assert!(b.try_reserve(100));
172 assert_eq!(b.remaining(), 0);
173 }
174
175 #[test]
176 fn reserve_zero_is_noop() {
177 let b = TokenBudget::new(100);
178 assert!(b.try_reserve(0));
179 assert_eq!(b.used(), 0);
180 }
181}