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}