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    /// Historical spend constrains this run but is not a new aggregate reservation.
123    replayed_used: u64,
124    replayed_held: u64,
125    /// The part of `used` held for elements the provider reported no usage for.
126    /// Tracked separately because it is bounded separately, and because it is
127    /// the part that must *not* be returned to the aggregate on teardown.
128    held: u64,
129}
130
131/// One execution's reservations against its own ceiling and the shared
132/// aggregate together.
133///
134/// Both reservations are taken together and both unwound if either fails, so a
135/// refused reservation leaves neither counter charged.
136///
137/// Releasing on drop is what makes the aggregate usable for execution lifetime:
138/// an execution ends by having its budget holder dropped, not by reconciling
139/// every reservation first, so without this the reservation of every ended
140/// execution would be held forever and the aggregate would ratchet to its cap.
141///
142/// **Held reserve is the deliberate exception.** It releases from the
143/// per-execution ledger, which is over, but stays charged against the aggregate.
144/// Releasing indeterminate spend from the aggregate would let it escape the
145/// server-wide ceiling entirely: a caller who can reliably induce null usage pays
146/// real money the aggregate forgets at execution end, so N executions each under
147/// the per-execution ceiling could exceed the server-wide one without ever
148/// tripping it.
149#[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    /// Tokens this execution currently holds against its ceiling.
166    pub fn used(&self) -> u64 {
167        let state = self.locked();
168        state.used.saturating_add(state.replayed_used)
169    }
170
171    /// The part of [`Self::used`] held for unreported usage.
172    pub fn held(&self) -> u64 {
173        let state = self.locked();
174        state.held.saturating_add(state.replayed_held)
175    }
176
177    /// Restore one committed native call's local spend without billing the shared
178    /// aggregate again. Only a fresh attempt consumes a history prefix.
179    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        // A poisoned lock still yields the counters, and a ledger that refused
187        // to account after an unrelated panic would leak the whole reservation.
188        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    /// Reserve `tokens` against this execution and the aggregate.
202    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    /// Reconcile a completed dispatch down from its `reserved` estimate to what
236    /// the provider actually reported.
237    ///
238    /// `reported` is the summed usage of the elements the provider gave counts
239    /// for; `indeterminate` is the reserve belonging to elements it reported
240    /// nothing for. The indeterminate part is **not** released — `None` means
241    /// indeterminate, not free, since a throttled call may still have been
242    /// billed — and is instead carried as held reserve, bounded by
243    /// [`LedgerLimits::max_held_tokens`].
244    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    /// Settle a sub-batch whose `held_estimate` was already moved to held (via
258    /// [`Self::reconcile`] with the whole estimate indeterminate) now that its
259    /// outcome is known.
260    ///
261    /// The estimate leaves held; `reported` stays charged as spend, `indeterminate`
262    /// goes back to held, and the rest is released to this execution and the
263    /// aggregate. A `reported` above the estimate is spend that already happened:
264    /// the excess is charged (saturating, never refused) even if that passes a
265    /// cap, and the next reservation is then refused.
266    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        // `get_mut` on our own `&mut self` cannot contend; a poisoned lock still
286        // yields the state, whose counters are what we owe back.
287        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}