Skip to main content

submilli_engine/runtime/
token_ledger.rs

1//! The token accounting both `submilli:llm` and `submilli:embedding` budget
2//! against: a per-execution reservation that also charges a server-wide
3//! aggregate, with held reserve for spend a provider never reported.
4//!
5//! This module owns the arithmetic and the concurrency. It knows nothing about
6//! either capability's error type, flag names, or message text: a refusal comes
7//! back as a [`LedgerRefusal`] naming which ceiling was hit, and the capability
8//! wrapper renders it in its own vocabulary.
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
12
13/// An aggregate token budget several executions reserve against.
14#[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    /// Add `tokens`, or report the total that would have exceeded the cap.
37    ///
38    /// Compare-and-swap so two executions reserving at once cannot both observe
39    /// the same headroom.
40    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    /// Charge `tokens` that were already spent, past the cap if need be.
60    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    /// Return `tokens`, saturating at zero.
77    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/// The two ceilings one execution's ledger enforces. The server-wide cap lives on
95/// the [`SharedTokenBudget`].
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct LedgerLimits {
98    /// Tokens one execution may hold in total.
99    pub per_execution_tokens: u64,
100    /// Tokens one execution may hold as indeterminate reserve before further
101    /// reservation is refused.
102    pub max_held_tokens: u64,
103}
104
105/// Which ceiling refused a reservation, with the numbers the capability's message
106/// needs.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum LedgerRefusal {
109    /// Held reserve is already past [`LedgerLimits::max_held_tokens`].
110    HeldReserve { held: u64, limit: u64 },
111    /// This execution's own ceiling.
112    PerExecution { requested: u64, limit: u64 },
113    /// The shared aggregate's cap.
114    AllExecutions { requested: u64, limit: u64 },
115}
116
117/// What one execution has reserved, and what of that is indeterminate.
118#[derive(Debug, Default)]
119pub(super) struct TokenState {
120    /// Tokens reserved against both this execution and the aggregate.
121    used: u64,
122    /// The part of `used` held for elements the provider reported no usage for.
123    /// Tracked separately because it is bounded separately, and because it is
124    /// the part that must *not* be returned to the aggregate on teardown.
125    held: u64,
126}
127
128/// One execution's reservations against its own ceiling and the shared
129/// aggregate together.
130///
131/// Both reservations are taken together and both unwound if either fails, so a
132/// refused reservation leaves neither counter charged.
133///
134/// Releasing on drop is what makes the aggregate usable for execution lifetime:
135/// an execution ends by having its budget holder dropped, not by reconciling
136/// every reservation first, so without this the reservation of every ended
137/// execution would be held forever and the aggregate would ratchet to its cap.
138///
139/// **Held reserve is the deliberate exception.** It releases from the
140/// per-execution ledger, which is over, but stays charged against the aggregate.
141/// Releasing indeterminate spend from the aggregate would let it escape the
142/// server-wide ceiling entirely: a caller who can reliably induce null usage pays
143/// real money the aggregate forgets at execution end, so N executions each under
144/// the per-execution ceiling could exceed the server-wide one without ever
145/// tripping it.
146#[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    /// Tokens this execution currently holds against its ceiling.
163    pub fn used(&self) -> u64 {
164        self.locked().used
165    }
166
167    /// The part of [`Self::used`] held for unreported usage.
168    pub fn held(&self) -> u64 {
169        self.locked().held
170    }
171
172    fn locked(&self) -> MutexGuard<'_, TokenState> {
173        // A poisoned lock still yields the counters, and a ledger that refused
174        // to account after an unrelated panic would leak the whole reservation.
175        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    /// Reserve `tokens` against this execution and the aggregate.
189    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    /// Reconcile a completed dispatch down from its `reserved` estimate to what
219    /// the provider actually reported.
220    ///
221    /// `reported` is the summed usage of the elements the provider gave counts
222    /// for; `indeterminate` is the reserve belonging to elements it reported
223    /// nothing for. The indeterminate part is **not** released — `None` means
224    /// indeterminate, not free, since a throttled call may still have been
225    /// billed — and is instead carried as held reserve, bounded by
226    /// [`LedgerLimits::max_held_tokens`].
227    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    /// Settle a sub-batch whose `held_estimate` was already moved to held (via
241    /// [`Self::reconcile`] with the whole estimate indeterminate) now that its
242    /// outcome is known.
243    ///
244    /// The estimate leaves held; `reported` stays charged as spend, `indeterminate`
245    /// goes back to held, and the rest is released to this execution and the
246    /// aggregate. A `reported` above the estimate is spend that already happened:
247    /// the excess is charged (saturating, never refused) even if that passes a
248    /// cap, and the next reservation is then refused.
249    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        // `get_mut` on our own `&mut self` cannot contend; a poisoned lock still
269        // yields the state, whose counters are what we owe back.
270        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}