Skip to main content

cove_runtime/
budget.rs

1//! Runtime resource control.
2//!
3//! ADR 0001 makes termination and CPU usage runtime concerns rather than
4//! properties the type system proves: "Totality, determinism, and
5//! absence of loops are explicitly not MVP guarantees." This module is where
6//! that decision becomes code. A [`Budget`] tracks one run against the
7//! [`Limits`] a host chose, and the interpreter consults it at safepoints —
8//! loop back edges, calls, and `await` — rather than at arbitrary points, so
9//! the cost of enforcement is bounded and predictable.
10
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::error::RuntimeError;
16use crate::trace::RunOutcome;
17use crate::wallclock::Instant;
18
19/// The rule this module implements, quoted for every error it raises.
20///
21/// Visible to the crate because the limits are not all in one place: the
22/// linear-memory backend's reserved stack region bounds how many tasks may
23/// run at once as well, and a second wording of the same rule beside it would
24/// be a second answer that could drift.
25pub(crate) const RULE: &str =
26    "ADR 0001: CPU, time, concurrency, and host-call limits are runtime controls, not termination proofs.";
27
28/// How many [`Budget::safepoint`] calls pass between checks of the wall clock
29/// when a deadline is set.
30///
31/// `Instant::now()` reads a monotonic clock, which on most platforms is a
32/// system call or vDSO trap — several orders of magnitude slower than
33/// decrementing an integer. Checking it at every safepoint would tax
34/// fuel-heavy loops for a bound that fuel usually enforces anyway. Every
35/// [`DEADLINE_CHECK_INTERVAL`]th call keeps the wasted overrun bounded to a
36/// small, fixed number of safepoints without paying the clock's cost on every
37/// one. When no fuel limit is set, nothing else bounds the run, so the clock
38/// is consulted on every call regardless of this constant.
39pub const DEADLINE_CHECK_INTERVAL: u64 = 64;
40
41/// Limits a host imposes on one run.
42///
43/// A `None` field imposes nothing: `Limits::default()` never stops a run.
44#[derive(Clone, Debug, Default)]
45pub struct Limits {
46    /// The total fuel a run may spend before it is stopped.
47    pub fuel: Option<u64>,
48    /// The wall-clock duration a run may take before it is stopped.
49    pub deadline: Option<Duration>,
50    /// The total number of host calls a run may make before it is stopped.
51    pub max_host_calls: Option<u64>,
52    /// The deepest a call may nest before it is stopped.
53    pub max_call_depth: Option<usize>,
54    /// The tasks a run may have alive at once before it is stopped.
55    ///
56    /// ADR 0001 lists concurrency limits beside CPU and time, and a
57    /// thread is the one resource a program can take without asking for it.
58    /// So this limit is charged where the taking happens: `spawn` charges it
59    /// before a thread exists, and a `spawn` past the limit stops the run
60    /// rather than waiting for a sibling to finish, because waiting would be
61    /// a scheduling policy and ADR 0008 has none. Like fuel and host calls,
62    /// it bounds the *run*: every task alive anywhere in it counts, so
63    /// a program cannot stay under the limit by spreading its tasks over more
64    /// scopes.
65    pub max_tasks: Option<u64>,
66}
67
68/// Why execution was stopped.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum Stopped {
71    /// The fuel budget was exhausted.
72    Fuel,
73    /// The wall-clock deadline was exceeded.
74    Deadline,
75    /// The run was cancelled from outside.
76    Cancelled,
77    /// The call-depth limit was exceeded.
78    CallDepth,
79    /// The host-call limit was exceeded.
80    HostCalls,
81    /// A `spawn` would have left more tasks alive at once than the
82    /// concurrency limit allows.
83    Concurrency,
84}
85
86impl Stopped {
87    /// How a run stopped this way is classified in its terminal trace event.
88    ///
89    /// One [`RunOutcome`] per [`Stopped`], because each of these is a
90    /// different control and a reader deciding what to do about a stopped run
91    /// wants to know which one: a run out of fuel and a run past its deadline
92    /// are not the same report, however alike the two stops look from inside
93    /// the budget.
94    pub fn outcome(self) -> RunOutcome {
95        match self {
96            Stopped::Fuel => RunOutcome::Fuel,
97            Stopped::Deadline => RunOutcome::Deadline,
98            Stopped::Cancelled => RunOutcome::Cancelled,
99            Stopped::CallDepth => RunOutcome::CallDepth,
100            Stopped::HostCalls => RunOutcome::HostCalls,
101            Stopped::Concurrency => RunOutcome::Concurrency,
102        }
103    }
104}
105
106/// A cancellation flag shared with whoever may cancel the run.
107///
108/// Cloning shares the same underlying flag: cancelling one handle cancels
109/// every clone, including ones already handed to a [`Budget`].
110#[derive(Clone, Debug, Default)]
111pub struct Cancellation(Arc<AtomicBool>);
112
113impl Cancellation {
114    /// A fresh, not-yet-cancelled flag.
115    pub fn new() -> Self {
116        Cancellation(Arc::new(AtomicBool::new(false)))
117    }
118
119    /// Requests cancellation. Idempotent: cancelling twice is the same as
120    /// cancelling once.
121    pub fn cancel(&self) {
122        self.0.store(true, Ordering::SeqCst);
123    }
124
125    /// Whether [`Cancellation::cancel`] has been called on this flag or any
126    /// clone of it.
127    pub fn is_cancelled(&self) -> bool {
128        self.0.load(Ordering::SeqCst)
129    }
130}
131
132/// One run's accounting: what it was limited to, when it started, and what
133/// it has spent so far.
134///
135/// One allocation, reached by every thread of the run at once. ADR 0008 draws
136/// a task's fuel from the run's budget rather than giving each task one of
137/// its own, so there is exactly one of these per run however many tasks it
138/// has, and every counter in it is an atomic rather than a field behind a
139/// lock — see [`Meter`] for why that is the shape.
140///
141/// `limits`, `cancellation` and `started_at` do not change while a run lasts.
142/// A run that starts over gets a fresh one of these rather than having this
143/// one reset, which is what [`Budget::restart`] does and why `started_at` can
144/// be a plain [`Instant`] read without synchronization.
145#[derive(Debug)]
146struct Accounting {
147    limits: Limits,
148    cancellation: Cancellation,
149    started_at: Instant,
150    fuel_spent: AtomicU64,
151    host_calls: AtomicU64,
152    /// How many safepoints have been taken while a deadline was set, which is
153    /// what picks every [`DEADLINE_CHECK_INTERVAL`]th one to read the clock
154    /// at. It counts up and is never reset: a counter that were reset would
155    /// lose the increments of every thread that raced the reset, and how long
156    /// a run may go without reading the clock is a bound ADR 0024 states.
157    safepoints_under_deadline: AtomicU64,
158    /// How many spawned tasks are alive right now: charged before a task is
159    /// given a thread and released when the task that spawned it observes
160    /// its end.
161    live_tasks: AtomicU64,
162}
163
164/// One run's budget as a safepoint charges it: a handle every task thread can
165/// hold at once, over counters that need no lock.
166///
167/// # Why this is not a `&mut Budget`
168///
169/// It used to be. [`crate::host::HostRegistry::with_budget`] locked a mutex,
170/// handed the closure a `&mut Budget`, and unlocked — at every call and at
171/// every return, because every call and every return is a safepoint. Issue
172/// #182 measured what that cost: on `benches/call`, `with_budget` plus
173/// `pthread_mutex_lock` plus `pthread_mutex_unlock` were 36% of the run
174/// against the predecessor's `execute` at 46%.
175///
176/// The lock was not protecting anything that needed one. A safepoint adds to
177/// `fuel_spent`, reads an atomic flag, compares against a limit fixed before
178/// the run, and every so often reads a clock that started before the run.
179/// None of that is a multi-field invariant two threads could tear; the
180/// counters were plain integers because the struct holding them happened to
181/// be reached by `&mut`, not because anything wanted them to be. So they are
182/// atomics, this is the `&self` view of them, and the mutex is left to what
183/// installs a budget and what reads the counters back.
184///
185/// # What is still the mutex's
186///
187/// [`crate::host::HostRegistry::with_budget`] still exists and still locks. It
188/// is how a budget is installed, how `cove run --stats` reads what a run
189/// spent, and how the charges that are not per-instruction are made — a host
190/// call, a spawn, a task that ended. Every one of those is bounded by
191/// something far more expensive than a lock, and moving them would have been
192/// churn without a number behind it.
193///
194/// # Taking one, and restarts
195///
196/// A `Meter` names the accounting of the run it was taken from rather than
197/// "whatever budget the registry holds now". `Budget::restart` gives its
198/// budget fresh accounting, so a `Meter` taken before a restart charges the
199/// run that ended. Both backends therefore take theirs where a run begins:
200/// `Vm::new` and `Interpreter::new` take one, and `invoke_within` and
201/// `run_entry_within` take another immediately after installing the budget
202/// they were handed. A registry's budget cannot be replaced by any other
203/// route — `set_budget` needs `&mut HostRegistry` and a backend holds the
204/// registry by shared reference for as long as it exists — so those are all
205/// the places a stale one could come from.
206#[derive(Clone, Debug)]
207pub struct Meter {
208    state: Arc<Accounting>,
209}
210
211/// Tracks one run against its [`Limits`].
212///
213/// A `Budget` is not `Clone`: it is one run's, and a second one would be a
214/// second run. What is shared instead is [`Meter`], the view of the same
215/// accounting that a safepoint charges through, and every task thread of the
216/// run holds one — ADR 0008 draws a task's fuel from the run's budget rather
217/// than giving each task one of its own, so there is still exactly one
218/// authoritative count of what the run spent. Share a [`Cancellation`] when
219/// another thread needs to stop the run.
220///
221/// `max_call_depth` is the one limit a budget does not itself enforce. Call
222/// depth is a property of one stack, and with a thread per task there is a
223/// stack per task, so the interpreter checks its own depth against
224/// [`Limits::max_call_depth`]; counting every task's frames into one number
225/// would stop a shallow task because a sibling was deep.
226#[derive(Debug)]
227pub struct Budget {
228    meter: Meter,
229}
230
231impl Meter {
232    /// Fresh accounting for a run bounded by `limits` and stopped by
233    /// `cancellation`, with the deadline clock starting now.
234    fn new(limits: Limits, cancellation: Cancellation) -> Self {
235        Meter {
236            state: Arc::new(Accounting {
237                limits,
238                cancellation,
239                started_at: Instant::now(),
240                fuel_spent: AtomicU64::new(0),
241                host_calls: AtomicU64::new(0),
242                safepoints_under_deadline: AtomicU64::new(0),
243                live_tasks: AtomicU64::new(0),
244            }),
245        }
246    }
247
248    /// The limits the run was given, which do not change while it lasts.
249    pub fn limits(&self) -> &Limits {
250        &self.state.limits
251    }
252
253    /// Whether the run has been cancelled from outside.
254    ///
255    /// The *run's* flag, which every task of it shares. A task's own flag and
256    /// a bounded call's belong to one thread, and `crate::interp::stopped_here`
257    /// is where those two are read.
258    pub fn is_cancelled(&self) -> bool {
259        self.state.cancellation.is_cancelled()
260    }
261
262    /// Checks cancellation, the deadline, and fuel in one call. Both backends
263    /// call this at their safepoints. `fuel` is the cost of the work performed
264    /// since the last one.
265    ///
266    /// The order the three questions are asked in is the whole of what a
267    /// caller can observe about this, and it is the order they were asked in
268    /// when a mutex was held across all three.
269    pub fn safepoint(&self, fuel: u64) -> Result<(), Stopped> {
270        // Counted before anything can refuse, because `fuel` is work the run
271        // has already done and a stop does not un-do it. Reading the
272        // cancellation flag first and returning would have thrown away
273        // whatever the caller had gathered since its last safepoint, which
274        // on a backend that charges in batches is most of what it did.
275        // Nothing about *which* stop is reported moves: the limit is still
276        // checked after the flag, so a cancelled run is still cancelled and
277        // not out of fuel.
278        let spent = self.add_fuel(fuel);
279        if self.state.cancellation.is_cancelled() {
280            return Err(Stopped::Cancelled);
281        }
282
283        if let Some(limit) = self.state.limits.fuel {
284            if spent >= limit {
285                return Err(Stopped::Fuel);
286            }
287        }
288
289        if let Some(deadline) = self.state.limits.deadline {
290            // With no fuel limit nothing else bounds the run, so the clock is
291            // read at every safepoint. Otherwise one safepoint in
292            // `DEADLINE_CHECK_INTERVAL` reads it, chosen off a counter that
293            // only ever counts up rather than one reset at each check: a reset
294            // would discard whatever another task added between the check and
295            // the reset, and this interval is a bound rather than a heuristic.
296            let must_check_clock = self.state.limits.fuel.is_none()
297                || self
298                    .state
299                    .safepoints_under_deadline
300                    .fetch_add(1, Ordering::Relaxed)
301                    % DEADLINE_CHECK_INTERVAL
302                    == DEADLINE_CHECK_INTERVAL - 1;
303            if must_check_clock && self.state.started_at.elapsed() >= deadline {
304                return Err(Stopped::Deadline);
305            }
306        }
307
308        Ok(())
309    }
310
311    /// Adds `fuel` to the run's total without asking whether the run may
312    /// continue.
313    ///
314    /// A backend that charges fuel in batches holds some between two
315    /// safepoints, and the safepoint is where that holding is spent. A run
316    /// that ends anywhere else — by raising, by being stopped, by a task
317    /// thread finishing — reaches no further safepoint, so what it had
318    /// gathered would simply not be counted, and `fuel_spent` would report
319    /// less work than the run did. This is where the last of it is put back,
320    /// and it decides nothing: the run is already over, and a second stop
321    /// raised here would be answering a question nobody asked.
322    pub fn spend(&self, fuel: u64) {
323        self.add_fuel(fuel);
324    }
325
326    /// Adds `fuel` to the run's total and answers what the total is now.
327    ///
328    /// Saturating rather than wrapping, which is what it was when the total
329    /// was a plain field behind a lock: a run that has spent more fuel than a
330    /// `u64` can name has passed any limit that could have been set on it, and
331    /// wrapping would hand it a fresh budget. The correction is a second store
332    /// rather than a compare-and-swap loop because the branch is never taken,
333    /// and a safepoint is not a place to pay for a case that cannot arise.
334    fn add_fuel(&self, fuel: u64) -> u64 {
335        let before = self.state.fuel_spent.fetch_add(fuel, Ordering::Relaxed);
336        let after = before.wrapping_add(fuel);
337        if after < before {
338            self.state.fuel_spent.store(u64::MAX, Ordering::Relaxed);
339            return u64::MAX;
340        }
341        after
342    }
343
344    /// Total fuel spent so far, for reporting.
345    pub fn fuel_spent(&self) -> u64 {
346        self.state.fuel_spent.load(Ordering::Relaxed)
347    }
348
349    /// Wall-clock time elapsed since the run started.
350    pub fn elapsed(&self) -> Duration {
351        self.state.started_at.elapsed()
352    }
353
354    /// Converts why execution stopped into a [`RuntimeError`] naming the
355    /// limit and its configured value, quoting ADR 0001's position that these
356    /// are runtime controls rather than termination proofs.
357    pub fn to_runtime_error(&self, stopped: Stopped) -> RuntimeError {
358        let message = match stopped {
359            Stopped::Fuel => format!(
360                "execution stopped: fuel budget of {} exhausted",
361                self.state.limits.fuel.unwrap_or_default()
362            ),
363            Stopped::Deadline => format!(
364                "execution stopped: wall-clock deadline of {:?} exceeded",
365                self.state.limits.deadline.unwrap_or_default()
366            ),
367            Stopped::Cancelled => "execution stopped: the run was cancelled".to_string(),
368            Stopped::CallDepth => format!(
369                "execution stopped: call-depth limit of {} exceeded",
370                self.state.limits.max_call_depth.unwrap_or_default()
371            ),
372            Stopped::HostCalls => format!(
373                "execution stopped: host-call limit of {} exceeded",
374                self.state.limits.max_host_calls.unwrap_or_default()
375            ),
376            Stopped::Concurrency => format!(
377                "execution stopped: concurrency limit of {} task(s) exceeded, with {} already running",
378                self.state.limits.max_tasks.unwrap_or_default(),
379                self.state.live_tasks.load(Ordering::Relaxed),
380            ),
381        };
382        RuntimeError::new(message)
383            .with_rule(RULE)
384            .with_outcome(stopped.outcome())
385    }
386}
387
388impl Budget {
389    /// Tracks a run against `limits`, starting the deadline clock now.
390    pub fn new(limits: Limits) -> Self {
391        Budget::with_cancellation(limits, Cancellation::new())
392    }
393
394    /// Tracks a run against `limits`, using a [`Cancellation`] the caller
395    /// already holds a handle to, so it can be cancelled from elsewhere.
396    pub fn with_cancellation(limits: Limits, cancellation: Cancellation) -> Self {
397        Budget {
398            meter: Meter::new(limits, cancellation),
399        }
400    }
401
402    /// This run's accounting, in the handle a safepoint charges through.
403    ///
404    /// A caller that will charge more than once holds on to what this
405    /// answers: taking one costs an `Arc` clone, and charging through one
406    /// costs no lock at all. [`Meter`] says where each backend takes its own
407    /// and why that is where a run begins.
408    pub fn meter(&self) -> Meter {
409        self.meter.clone()
410    }
411
412    /// The cancellation flag for this run. Clone and hand it to whoever may
413    /// need to cancel the run from another thread.
414    pub fn cancellation(&self) -> Cancellation {
415        self.meter.state.cancellation.clone()
416    }
417
418    /// Starts this budget over, for the run that is about to begin.
419    ///
420    /// Every count goes back to zero and the deadline clock starts again from
421    /// now. That is the answer to the one question a per-invocation limit
422    /// raises that a per-run one does not: a `Budget` starts its clock when it
423    /// is built, and a budget built to bound an invocation that has not begun
424    /// would spend its deadline waiting for its turn. The deadline runs from
425    /// the invocation, so this is called as the invocation is entered and
426    /// nowhere else — [`crate::host::HostRegistry::begin_run`] is the only
427    /// caller.
428    ///
429    /// The [`Cancellation`] is *not* reset, and that is not an oversight. A
430    /// flag somebody raised stays raised: the handle is shared, whoever
431    /// cancelled did so on purpose, and a run that quietly un-cancelled itself
432    /// as it started would be a stop this crate promised and did not make.
433    /// A caller that wants a fresh flag builds a fresh budget with one.
434    ///
435    /// It is fresh accounting rather than counters written back to zero,
436    /// because zeroing counters a running task might still be charging is a
437    /// race with no answer — while a [`Meter`] handed out for the previous run
438    /// keeps charging the run it belongs to, which is the only thing it could
439    /// truthfully do. `begin_run` is the only caller and it holds the budget
440    /// alone at that moment, so nothing is charging this one either way; what
441    /// the shape buys is that a mistake about that would be a stale number in
442    /// a finished run's report rather than a torn one in a live run's limit.
443    pub(crate) fn restart(&mut self) {
444        self.meter = Meter::new(
445            self.meter.state.limits.clone(),
446            self.meter.state.cancellation.clone(),
447        );
448    }
449
450    /// The limits this budget was constructed with.
451    pub fn limits(&self) -> &Limits {
452        self.meter.limits()
453    }
454
455    /// Checks cancellation, the deadline, and fuel in one call. The
456    /// interpreter calls this at safepoints: loop back edges, calls, and
457    /// `await`. `fuel` is the cost of the work performed since the last
458    /// safepoint.
459    ///
460    /// [`Meter::safepoint`] is the whole of it. A backend on a per-instruction
461    /// path holds a [`Meter`] and calls that instead of reaching a `Budget`
462    /// through the registry's lock; this is here for a caller that has a
463    /// `Budget` in hand and charges once.
464    pub fn safepoint(&self, fuel: u64) -> Result<(), Stopped> {
465        self.meter.safepoint(fuel)
466    }
467
468    /// Adds `fuel` to the run's total without asking whether the run may
469    /// continue. [`Meter::spend`] says when that is what a backend wants.
470    pub fn spend(&self, fuel: u64) {
471        self.meter.spend(fuel);
472    }
473
474    /// Charges one host call against the budget, failing before the call is
475    /// dispatched if the run was cancelled, if its deadline has passed, or if
476    /// the call would exceed `max_host_calls`.
477    ///
478    /// A host call is a control point exactly as a safepoint is. ADR 0003
479    /// puts the controls at "loop back edges, calls, and `await`", and a run
480    /// whose work is waiting on a host reaches none of the other three: a
481    /// deadline checked only in Cove code would not bound a program that
482    /// spends its time inside calls. The clock is read on every call rather
483    /// than every `DEADLINE_CHECK_INTERVAL`th, because a host call already
484    /// costs far more than reading it does.
485    pub fn charge_host_call(&self) -> Result<(), Stopped> {
486        let state = &self.meter.state;
487        if state.cancellation.is_cancelled() {
488            return Err(Stopped::Cancelled);
489        }
490        if let Some(deadline) = state.limits.deadline {
491            if state.started_at.elapsed() >= deadline {
492                return Err(Stopped::Deadline);
493            }
494        }
495        let made = state
496            .host_calls
497            .fetch_add(1, Ordering::Relaxed)
498            .saturating_add(1);
499        if let Some(limit) = state.limits.max_host_calls {
500            if made > limit {
501                return Err(Stopped::HostCalls);
502            }
503        }
504        Ok(())
505    }
506
507    /// Charges one task against the concurrency limit, refusing it before it
508    /// is given a thread if the run already holds as many tasks as it may.
509    ///
510    /// Every other limit stops a run for work it has already done. This one
511    /// refuses work that has not started, because a thread is taken rather
512    /// than spent: by the time a safepoint could observe it, the resource is
513    /// already held. A refusal stops the run the way exhausted fuel does; a
514    /// `spawn` that waited for a sibling to finish would be a scheduler, and
515    /// ADR 0008 deliberately has no scheduling policy.
516    ///
517    /// The check and the taking are one step, so two `spawn`s racing for the
518    /// last place cannot both be told there is one. That used to be the
519    /// registry's mutex; it is this compare-and-swap now, which holds however
520    /// this is reached.
521    pub fn charge_task(&self) -> Result<(), Stopped> {
522        let live = &self.meter.state.live_tasks;
523        match self.meter.state.limits.max_tasks {
524            Some(limit) => live
525                .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |live| {
526                    (live < limit).then(|| live + 1)
527                })
528                .map(|_| ())
529                .map_err(|_| Stopped::Concurrency),
530            None => {
531                live.fetch_add(1, Ordering::Relaxed);
532                Ok(())
533            }
534        }
535    }
536
537    /// Forgets a task whose end has been observed, so its place is free
538    /// again.
539    ///
540    /// A task ends by finishing, by failing, by being cancelled, or by
541    /// breaking an invariant in its own thread, and all four reach the caller
542    /// as a join. Releasing anywhere else would make this a limit on how many
543    /// tasks a run may spawn in total rather than on how many it may hold at
544    /// once.
545    pub fn release_task(&self) {
546        let _ = self.meter.state.live_tasks.fetch_update(
547            Ordering::Relaxed,
548            Ordering::Relaxed,
549            |live| Some(live.saturating_sub(1)),
550        );
551    }
552
553    /// How many spawned tasks are alive right now: what the concurrency
554    /// limit bounds, and what a stop reports.
555    pub fn live_tasks(&self) -> u64 {
556        self.meter.state.live_tasks.load(Ordering::Relaxed)
557    }
558
559    /// Total fuel spent so far, for reporting.
560    pub fn fuel_spent(&self) -> u64 {
561        self.meter.fuel_spent()
562    }
563
564    /// Total host calls charged so far, including any that were then
565    /// rejected for exceeding the limit, for reporting.
566    pub fn host_calls(&self) -> u64 {
567        self.meter.state.host_calls.load(Ordering::Relaxed)
568    }
569
570    /// Wall-clock time elapsed since the budget was created.
571    pub fn elapsed(&self) -> Duration {
572        self.meter.elapsed()
573    }
574
575    /// Converts why execution stopped into a [`RuntimeError`] naming the
576    /// limit and its configured value, quoting ADR 0001's position that these
577    /// are runtime controls rather than termination proofs.
578    pub fn to_runtime_error(&self, stopped: Stopped) -> RuntimeError {
579        self.meter.to_runtime_error(stopped)
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use std::thread;
587
588    #[test]
589    fn fuel_limit_fires_when_exhausted() {
590        let budget = Budget::new(Limits {
591            fuel: Some(10),
592            ..Limits::default()
593        });
594        assert_eq!(budget.safepoint(5), Ok(()));
595        assert_eq!(budget.safepoint(4), Ok(()));
596        assert_eq!(budget.safepoint(1), Err(Stopped::Fuel));
597        assert_eq!(budget.fuel_spent(), 10);
598    }
599
600    #[test]
601    fn fuel_limit_absent_never_stops() {
602        let budget = Budget::new(Limits::default());
603        for _ in 0..1_000 {
604            assert_eq!(budget.safepoint(u64::MAX / 2000), Ok(()));
605        }
606    }
607
608    #[test]
609    fn deadline_fires_when_exceeded() {
610        let budget = Budget::new(Limits {
611            deadline: Some(Duration::from_millis(1)),
612            ..Limits::default()
613        });
614        thread::sleep(Duration::from_millis(20));
615        assert_eq!(budget.safepoint(0), Err(Stopped::Deadline));
616    }
617
618    #[test]
619    fn deadline_absent_never_stops() {
620        let budget = Budget::new(Limits::default());
621        thread::sleep(Duration::from_millis(5));
622        assert_eq!(budget.safepoint(0), Ok(()));
623    }
624
625    #[test]
626    fn deadline_alone_is_observed_on_the_first_safepoint() {
627        // With no fuel limit, the clock must be consulted every call, not
628        // merely every `DEADLINE_CHECK_INTERVAL`th one.
629        let budget = Budget::new(Limits {
630            deadline: Some(Duration::from_millis(1)),
631            ..Limits::default()
632        });
633        thread::sleep(Duration::from_millis(20));
634        assert_eq!(budget.safepoint(0), Err(Stopped::Deadline));
635    }
636
637    #[test]
638    fn the_call_depth_limit_is_reported_but_not_counted_here() {
639        // Depth belongs to one stack and a task has a stack of its own, so
640        // the interpreter counts frames and the budget only carries the
641        // limit and names it in the error.
642        let budget = Budget::new(Limits {
643            max_call_depth: Some(2),
644            ..Limits::default()
645        });
646        assert_eq!(budget.limits().max_call_depth, Some(2));
647        assert_eq!(
648            budget.to_runtime_error(Stopped::CallDepth).message,
649            "execution stopped: call-depth limit of 2 exceeded"
650        );
651    }
652
653    #[test]
654    fn max_host_calls_fires_when_exceeded() {
655        let budget = Budget::new(Limits {
656            max_host_calls: Some(2),
657            ..Limits::default()
658        });
659        assert_eq!(budget.charge_host_call(), Ok(()));
660        assert_eq!(budget.charge_host_call(), Ok(()));
661        assert_eq!(budget.charge_host_call(), Err(Stopped::HostCalls));
662        assert_eq!(budget.host_calls(), 3);
663    }
664
665    #[test]
666    fn max_host_calls_absent_never_stops() {
667        let budget = Budget::new(Limits::default());
668        for _ in 0..1_000 {
669            assert_eq!(budget.charge_host_call(), Ok(()));
670        }
671    }
672
673    #[test]
674    fn cancellation_from_another_thread_stops_the_run() {
675        let budget = Budget::new(Limits::default());
676        let cancellation = budget.cancellation();
677        let handle = thread::spawn(move || {
678            cancellation.cancel();
679        });
680        handle.join().unwrap();
681
682        assert_eq!(budget.safepoint(0), Err(Stopped::Cancelled));
683    }
684
685    /// The deadline bounds a run whose work is host calls, which reaches no
686    /// loop back edge, no Cove call, and no `await` to be stopped at.
687    #[test]
688    fn the_deadline_also_stops_host_call_charging() {
689        let budget = Budget::new(Limits {
690            deadline: Some(Duration::from_millis(1)),
691            ..Limits::default()
692        });
693        assert_eq!(budget.charge_host_call(), Ok(()));
694        thread::sleep(Duration::from_millis(20));
695        assert_eq!(budget.charge_host_call(), Err(Stopped::Deadline));
696        // A call refused for the deadline is not one the run made.
697        assert_eq!(budget.host_calls(), 1);
698    }
699
700    #[test]
701    fn cancellation_also_stops_host_call_charging() {
702        let cancellation = Cancellation::new();
703        let budget = Budget::with_cancellation(Limits::default(), cancellation.clone());
704        cancellation.cancel();
705        assert_eq!(budget.charge_host_call(), Err(Stopped::Cancelled));
706    }
707
708    #[test]
709    fn the_concurrency_limit_fires_on_the_spawn_that_would_pass_it() {
710        let budget = Budget::new(Limits {
711            max_tasks: Some(2),
712            ..Limits::default()
713        });
714        assert_eq!(budget.charge_task(), Ok(()));
715        assert_eq!(budget.charge_task(), Ok(()));
716        assert_eq!(budget.charge_task(), Err(Stopped::Concurrency));
717        // A refused task is not one the run holds: the limit refuses work
718        // before it starts rather than counting work that did.
719        assert_eq!(budget.live_tasks(), 2);
720    }
721
722    /// The limit bounds the tasks alive at once, not the tasks a run spawns
723    /// over its life: a run that ends each task before starting the next may
724    /// start as many as it likes.
725    #[test]
726    fn a_task_that_ended_frees_its_place_for_the_next_one() {
727        let budget = Budget::new(Limits {
728            max_tasks: Some(1),
729            ..Limits::default()
730        });
731        for _ in 0..1_000 {
732            assert_eq!(budget.charge_task(), Ok(()));
733            budget.release_task();
734        }
735        assert_eq!(budget.live_tasks(), 0);
736    }
737
738    /// Releasing more tasks than were charged cannot lend a run capacity it
739    /// never had, whatever a caller does.
740    #[test]
741    fn releasing_a_task_that_was_never_charged_frees_nothing() {
742        let budget = Budget::new(Limits::default());
743        budget.release_task();
744        assert_eq!(budget.live_tasks(), 0);
745    }
746
747    #[test]
748    fn concurrency_limit_absent_never_stops() {
749        let budget = Budget::new(Limits::default());
750        for _ in 0..1_000 {
751            assert_eq!(budget.charge_task(), Ok(()));
752        }
753    }
754
755    /// The concurrency diagnostic has the same shape as the memory one: it
756    /// names the limit that was configured, says what the run was holding,
757    /// and cites the rule.
758    #[test]
759    fn the_concurrency_diagnostic_names_the_limit_and_what_is_running() {
760        let budget = Budget::new(Limits {
761            max_tasks: Some(4),
762            ..Limits::default()
763        });
764        for _ in 0..4 {
765            assert_eq!(budget.charge_task(), Ok(()));
766        }
767        assert_eq!(budget.charge_task(), Err(Stopped::Concurrency));
768        let error = budget.to_runtime_error(Stopped::Concurrency);
769        assert!(
770            error.message.contains("concurrency limit of 4 task(s)"),
771            "{}",
772            error.message
773        );
774        assert!(
775            error.message.contains("4 already running"),
776            "{}",
777            error.message
778        );
779        assert!(error.rule.is_some());
780    }
781
782    /// Every task of a run charges the one budget, so what it reports is the
783    /// sum of what they all did and not the last writer's share of it. A
784    /// counter that were read, added to, and written back would lose most of
785    /// this; a `fetch_add` loses none of it.
786    #[test]
787    fn nothing_is_lost_when_every_thread_charges_at_once() {
788        const THREADS: u64 = 8;
789        const EACH: u64 = 20_000;
790
791        let budget = Arc::new(Budget::new(Limits::default()));
792        let meters: Vec<_> = (0..THREADS).map(|_| budget.meter()).collect();
793        let handles: Vec<_> = meters
794            .into_iter()
795            .map(|meter| {
796                thread::spawn(move || {
797                    for _ in 0..EACH {
798                        assert_eq!(meter.safepoint(1), Ok(()));
799                    }
800                })
801            })
802            .collect();
803        for handle in handles {
804            handle.join().unwrap();
805        }
806        assert_eq!(budget.fuel_spent(), THREADS * EACH);
807    }
808
809    /// The fuel limit bounds the *run*, so it is the total across every task
810    /// that reaches it, and every task that asks after it has been reached is
811    /// told so. ADR 0008 draws a task's fuel from the run's budget and this is
812    /// what that means when the tasks are actually concurrent.
813    #[test]
814    fn a_fuel_limit_stops_every_thread_that_shares_the_run() {
815        const THREADS: u64 = 8;
816        const LIMIT: u64 = 10_000;
817
818        let budget = Arc::new(Budget::new(Limits {
819            fuel: Some(LIMIT),
820            ..Limits::default()
821        }));
822        let handles: Vec<_> = (0..THREADS)
823            .map(|_| budget.meter())
824            .map(|meter| {
825                thread::spawn(move || {
826                    // Every thread runs until the run refuses it, which it
827                    // must: the limit is the run's, so one thread spending it
828                    // stops the others too.
829                    let mut charged = 0u64;
830                    loop {
831                        charged += 1;
832                        if meter.safepoint(1) == Err(Stopped::Fuel) {
833                            return charged;
834                        }
835                        assert!(charged <= LIMIT, "a thread outran the run's whole budget");
836                    }
837                })
838            })
839            .collect();
840        let charged: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
841        // Nothing is spent twice and nothing is dropped: what the budget
842        // reports is exactly what the threads between them charged.
843        assert_eq!(budget.fuel_spent(), charged);
844        assert!(budget.fuel_spent() >= LIMIT);
845    }
846
847    /// A place under the concurrency limit is taken by one `spawn` or the
848    /// other and never by both. The mutex the registry holds used to make the
849    /// check and the taking one step; this holds without it, which is what
850    /// lets a `spawn` be charged from wherever a `spawn` happens.
851    #[test]
852    fn two_spawns_racing_for_the_last_place_cannot_both_take_it() {
853        const THREADS: u64 = 8;
854        const LIMIT: u64 = 3;
855
856        for _ in 0..20 {
857            let budget = Arc::new(Budget::new(Limits {
858                max_tasks: Some(LIMIT),
859                ..Limits::default()
860            }));
861            let handles: Vec<_> = (0..THREADS)
862                .map(|_| Arc::clone(&budget))
863                .map(|budget| thread::spawn(move || budget.charge_task().is_ok()))
864                .collect();
865            let taken = handles
866                .into_iter()
867                .map(|handle| handle.join().unwrap())
868                .filter(|took| *took)
869                .count() as u64;
870            assert_eq!(taken, LIMIT);
871            assert_eq!(budget.live_tasks(), LIMIT);
872        }
873    }
874
875    /// `max_host_calls` bounds what a run does to the outside world, which
876    /// ADR 0024 makes the control that bounds effects exactly. A call counted
877    /// twice or not at all on one thread would make that bound a guess.
878    #[test]
879    fn every_host_call_is_counted_once_however_many_threads_make_them() {
880        const THREADS: u64 = 8;
881        const EACH: u64 = 5_000;
882
883        let budget = Arc::new(Budget::new(Limits::default()));
884        let handles: Vec<_> = (0..THREADS)
885            .map(|_| Arc::clone(&budget))
886            .map(|budget| {
887                thread::spawn(move || {
888                    for _ in 0..EACH {
889                        assert_eq!(budget.charge_host_call(), Ok(()));
890                    }
891                })
892            })
893            .collect();
894        for handle in handles {
895            handle.join().unwrap();
896        }
897        assert_eq!(budget.host_calls(), THREADS * EACH);
898    }
899
900    /// Fuel saturates rather than wrapping, so a run that has spent more than
901    /// a `u64` can name cannot come back under a limit it has passed.
902    #[test]
903    fn fuel_saturates_rather_than_wrapping() {
904        let budget = Budget::new(Limits {
905            fuel: Some(u64::MAX),
906            ..Limits::default()
907        });
908        budget.spend(u64::MAX - 1);
909        assert_eq!(budget.safepoint(1_000), Err(Stopped::Fuel));
910        assert_eq!(budget.fuel_spent(), u64::MAX);
911    }
912
913    /// A restart is fresh accounting rather than counters written back to
914    /// zero, so a [`Meter`] taken before one keeps charging the run it was
915    /// taken from. Both backends take theirs where a run begins for exactly
916    /// this reason, and this is the fact they are relying on.
917    #[test]
918    fn a_meter_taken_before_a_restart_belongs_to_the_run_that_ended() {
919        let mut budget = Budget::new(Limits::default());
920        let before = budget.meter();
921        before.spend(100);
922        assert_eq!(budget.fuel_spent(), 100);
923
924        budget.restart();
925        assert_eq!(budget.fuel_spent(), 0);
926
927        before.spend(7);
928        assert_eq!(budget.fuel_spent(), 0, "the new run is charged nothing");
929        assert_eq!(before.fuel_spent(), 107, "the old run kept its own total");
930
931        budget.meter().spend(7);
932        assert_eq!(budget.fuel_spent(), 7);
933    }
934
935    /// A restart keeps the flag for the reason `restart` gives, and it keeps
936    /// it through the fresh accounting: a run cancelled before it started is
937    /// still cancelled.
938    #[test]
939    fn a_restart_keeps_the_cancellation_it_was_built_with() {
940        let cancellation = Cancellation::new();
941        let mut budget = Budget::with_cancellation(Limits::default(), cancellation.clone());
942        cancellation.cancel();
943        budget.restart();
944        assert_eq!(budget.safepoint(0), Err(Stopped::Cancelled));
945        assert!(budget.cancellation().is_cancelled());
946    }
947
948    #[test]
949    fn to_runtime_error_names_the_configured_value() {
950        let budget = Budget::new(Limits {
951            fuel: Some(42),
952            ..Limits::default()
953        });
954        let error = budget.to_runtime_error(Stopped::Fuel);
955        assert!(error.message.contains('4') && error.message.contains('2'));
956        assert!(error.rule.is_some());
957    }
958}