mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat!: add cooperative execution-time limits across engine, VM, and binding (#539)
- Introduce ExecutionTimer/ExecutionTimerConfig to allow limiting evaluating time. - To amortize time checking costs, checking interval can be configured via the notion of work units - A global fallback time limit can be set to universally limit all evaluation in addition to engine level limit setting. - Implement limnits in interpreter and RVM. In RVM, also handle suspend/resume so that time during pause is not counted. - Add engine-level APIs to set/clear per-engine timer configuration and apply global fallback defaults. - Surface execution-time limits through FFI and C# bindings - Add C# tests and example usage to validate engine overrides, global fallback behavior, and compiled policy enforcement. - Expand docs for execution-time limit - Add interpreter YAML cases and VM unit tests for time-limit behavior and deterministic time sources. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
e68e852ee3
commit
394625d4bc
@@ -8,11 +8,13 @@ use crate::interpreter::*;
|
||||
use crate::lexer::*;
|
||||
use crate::parser::*;
|
||||
use crate::scheduler::*;
|
||||
use crate::utils::{gather_functions, limits};
|
||||
use crate::utils::gather_functions;
|
||||
use crate::utils::limits::{self, fallback_execution_timer_config, ExecutionTimerConfig};
|
||||
use crate::value::*;
|
||||
use crate::*;
|
||||
use crate::{Extension, QueryResults};
|
||||
|
||||
use crate::Rc;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
/// The Rego evaluation engine.
|
||||
@@ -23,6 +25,7 @@ pub struct Engine {
|
||||
interpreter: Interpreter,
|
||||
prepared: bool,
|
||||
rego_v1: bool,
|
||||
execution_timer_config: Option<ExecutionTimerConfig>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
@@ -62,14 +65,27 @@ impl Default for Engine {
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
fn effective_execution_timer_config(&self) -> Option<ExecutionTimerConfig> {
|
||||
self.execution_timer_config
|
||||
.or_else(fallback_execution_timer_config)
|
||||
}
|
||||
|
||||
fn apply_effective_execution_timer_config(&mut self) {
|
||||
let config = self.effective_execution_timer_config();
|
||||
self.interpreter.set_execution_timer_config(config);
|
||||
}
|
||||
|
||||
/// Create an instance of [Engine].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
let mut engine = Self {
|
||||
modules: Rc::new(vec![]),
|
||||
interpreter: Interpreter::new(),
|
||||
prepared: false,
|
||||
rego_v1: true,
|
||||
}
|
||||
execution_timer_config: None,
|
||||
};
|
||||
engine.apply_effective_execution_timer_config();
|
||||
engine
|
||||
}
|
||||
|
||||
/// Enable rego v0.
|
||||
@@ -101,6 +117,60 @@ impl Engine {
|
||||
self.rego_v1 = !rego_v0;
|
||||
}
|
||||
|
||||
/// Configure the execution timer.
|
||||
///
|
||||
/// Stores the supplied configuration and ensures the next evaluation is checked against those
|
||||
/// limits. Engines start without a time limit and otherwise fall back to the global
|
||||
/// configuration (if provided).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU32;
|
||||
/// use std::time::Duration;
|
||||
/// use regorus::utils::limits::ExecutionTimerConfig;
|
||||
/// use regorus::Engine;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// let config = ExecutionTimerConfig {
|
||||
/// limit: Duration::from_millis(10),
|
||||
/// check_interval: NonZeroU32::new(1).unwrap(),
|
||||
/// };
|
||||
///
|
||||
/// engine.set_execution_timer_config(config);
|
||||
/// ```
|
||||
pub fn set_execution_timer_config(&mut self, config: ExecutionTimerConfig) {
|
||||
self.execution_timer_config = Some(config);
|
||||
self.interpreter.set_execution_timer_config(Some(config));
|
||||
}
|
||||
|
||||
/// Clear the engine-specific execution timer configuration, falling back to the global value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU32;
|
||||
/// use std::time::Duration;
|
||||
/// use regorus::utils::limits::{
|
||||
/// set_fallback_execution_timer_config,
|
||||
/// ExecutionTimerConfig,
|
||||
/// };
|
||||
/// use regorus::Engine;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// let global = ExecutionTimerConfig {
|
||||
/// limit: Duration::from_millis(5),
|
||||
/// check_interval: NonZeroU32::new(1).unwrap(),
|
||||
/// };
|
||||
/// set_fallback_execution_timer_config(Some(global));
|
||||
///
|
||||
/// engine.clear_execution_timer_config();
|
||||
/// ```
|
||||
pub fn clear_execution_timer_config(&mut self) {
|
||||
self.execution_timer_config = None;
|
||||
self.apply_effective_execution_timer_config();
|
||||
}
|
||||
|
||||
/// Add a policy.
|
||||
///
|
||||
/// The policy file will be parsed and converted to AST representation.
|
||||
@@ -524,6 +594,7 @@ impl Engine {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]
|
||||
pub fn compile_for_target(&mut self) -> Result<CompiledPolicy> {
|
||||
self.prepare_for_eval(false, true)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
self.interpreter.compile(None).map(CompiledPolicy::new)
|
||||
}
|
||||
@@ -648,6 +719,7 @@ impl Engine {
|
||||
/// - [`crate::compile_policy_with_entrypoint`] for a higher-level convenience function
|
||||
pub fn compile_with_entrypoint(&mut self, rule: &Rc<str>) -> Result<CompiledPolicy> {
|
||||
self.prepare_for_eval(false, false)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
self.interpreter
|
||||
.compile(Some(rule.clone()))
|
||||
@@ -696,6 +768,7 @@ impl Engine {
|
||||
/// ```
|
||||
pub fn eval_rule(&mut self, rule: String) -> Result<Value> {
|
||||
self.prepare_for_eval(false, false)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
self.interpreter.eval_rule_in_path(rule)
|
||||
}
|
||||
@@ -737,6 +810,7 @@ impl Engine {
|
||||
/// ```
|
||||
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
|
||||
self.prepare_for_eval(enable_tracing, false)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
|
||||
self.interpreter.create_rule_prefixes()?;
|
||||
@@ -936,6 +1010,8 @@ impl Engine {
|
||||
enable_tracing: bool,
|
||||
) -> Result<QueryResults> {
|
||||
self.eval_modules(enable_tracing)?;
|
||||
// Restart the timer window for the user query after module evaluation.
|
||||
self.apply_effective_execution_timer_config();
|
||||
|
||||
let (query_module, query_node, query_schedule) = self.make_query(query)?;
|
||||
self.interpreter
|
||||
@@ -1011,6 +1087,7 @@ impl Engine {
|
||||
enable_tracing: bool,
|
||||
) -> Result<Value> {
|
||||
self.prepare_for_eval(enable_tracing, false)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
|
||||
self.interpreter.eval_rule(module, rule)?;
|
||||
@@ -1021,6 +1098,7 @@ impl Engine {
|
||||
#[doc(hidden)]
|
||||
pub fn eval_modules(&mut self, enable_tracing: bool) -> Result<Value> {
|
||||
self.prepare_for_eval(enable_tracing, false)?;
|
||||
self.apply_effective_execution_timer_config();
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
|
||||
// Ensure that empty modules are created.
|
||||
@@ -1440,11 +1518,14 @@ impl Engine {
|
||||
compiled_policy: Rc<crate::compiled_policy::CompiledPolicyData>,
|
||||
) -> Self {
|
||||
let modules = compiled_policy.modules.clone();
|
||||
Self {
|
||||
let mut engine = Self {
|
||||
modules,
|
||||
interpreter: Interpreter::new_from_compiled_policy(compiled_policy),
|
||||
rego_v1: true, // Value doesn't matter since this is used only for policy parsing
|
||||
prepared: true,
|
||||
}
|
||||
execution_timer_config: None,
|
||||
};
|
||||
engine.apply_effective_execution_timer_config();
|
||||
engine
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ use crate::lexer::*;
|
||||
use crate::lookup::Lookup;
|
||||
use crate::parser::Parser;
|
||||
use crate::scheduler::*;
|
||||
use crate::utils::limits::{monotonic_now, ExecutionTimer, ExecutionTimerConfig};
|
||||
#[cfg(feature = "std")]
|
||||
use crate::utils::*;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use crate::utils::{get_extra_arg, get_path_string, get_root_var, FunctionTable};
|
||||
use crate::value::*;
|
||||
use crate::*;
|
||||
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
|
||||
@@ -23,6 +27,7 @@ use crate::{Expression, Extension, Location, QueryResult, QueryResults};
|
||||
#[cfg(feature = "coverage")]
|
||||
use crate::query::traversal::traverse;
|
||||
|
||||
use crate::Rc;
|
||||
use alloc::collections::btree_map::Entry as BTreeMapEntry;
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -96,6 +101,7 @@ pub struct Interpreter {
|
||||
active_rules: Vec<Ref<Rule>>,
|
||||
builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
|
||||
no_rules_lookup: bool,
|
||||
execution_timer: ExecutionTimer,
|
||||
}
|
||||
|
||||
impl Default for Interpreter {
|
||||
@@ -143,6 +149,7 @@ impl Clone for Interpreter {
|
||||
query_module: None,
|
||||
module: None,
|
||||
no_rules_lookup: false,
|
||||
execution_timer: ExecutionTimer::new(self.execution_timer.config()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +230,7 @@ impl Interpreter {
|
||||
|
||||
gather_prints: false,
|
||||
prints: Vec::default(),
|
||||
execution_timer: ExecutionTimer::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,9 +274,38 @@ impl Interpreter {
|
||||
.data
|
||||
.clone()
|
||||
.unwrap_or_else(Value::new_object),
|
||||
execution_timer: ExecutionTimer::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_execution_timer_state(&mut self) {
|
||||
self.execution_timer.reset();
|
||||
if self.execution_timer.limit().is_none() {
|
||||
return;
|
||||
}
|
||||
if let Some(now) = monotonic_now() {
|
||||
self.execution_timer.start(now);
|
||||
}
|
||||
}
|
||||
|
||||
fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
|
||||
if self.execution_timer.limit().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(now) = monotonic_now() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.execution_timer.tick(work_units, now)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn check_execution_time(&mut self) -> Result<()> {
|
||||
self.execution_timer_tick(1)
|
||||
}
|
||||
|
||||
fn compiled_policy_mut(&mut self) -> &mut CompiledPolicyData {
|
||||
Rc::make_mut(&mut self.compiled_policy)
|
||||
}
|
||||
@@ -329,6 +366,11 @@ impl Interpreter {
|
||||
self.compiled_policy_mut().strict_builtin_errors = b;
|
||||
}
|
||||
|
||||
pub fn set_execution_timer_config(&mut self, config: Option<ExecutionTimerConfig>) {
|
||||
self.execution_timer = ExecutionTimer::new(config);
|
||||
self.reset_execution_timer_state();
|
||||
}
|
||||
|
||||
pub fn set_input(&mut self, input: Value) {
|
||||
self.input = input.clone();
|
||||
// Update with_document["input"] too, in case if engine is being reused and was already prepared
|
||||
@@ -357,6 +399,7 @@ impl Interpreter {
|
||||
self.contexts = vec![];
|
||||
self.rule_values.clear();
|
||||
self.builtins_cache.clear();
|
||||
self.reset_execution_timer_state();
|
||||
}
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
@@ -366,7 +409,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "allocator-memory-limits"))]
|
||||
fn memory_check(&mut self) -> Result<()> {
|
||||
const fn memory_check(&mut self) -> Result<()> {
|
||||
let _ = self; // quiet clippy::unused_self; retained for symmetry with VM path
|
||||
Ok(())
|
||||
}
|
||||
@@ -640,6 +683,7 @@ impl Interpreter {
|
||||
domain: &ExprRef,
|
||||
query: &Ref<Query>,
|
||||
) -> Result<bool> {
|
||||
self.check_execution_time()?;
|
||||
let domain = self.eval_expr(domain)?;
|
||||
|
||||
self.scopes.push(Scope::new());
|
||||
@@ -700,6 +744,7 @@ impl Interpreter {
|
||||
plan: &DestructuringPlan,
|
||||
value: &Value,
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
if value == &Value::Undefined {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
@@ -799,6 +844,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn execute_assignment_plan(&mut self, plan: &AssignmentPlan) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
match plan {
|
||||
AssignmentPlan::ColonEquals {
|
||||
lhs_expr: _,
|
||||
@@ -894,6 +940,7 @@ impl Interpreter {
|
||||
collection: &ExprRef,
|
||||
stmts: &[&LiteralStmt],
|
||||
) -> Result<bool> {
|
||||
self.check_execution_time()?;
|
||||
let scope_saved = self.current_scope()?.clone();
|
||||
let mut count: usize = 0;
|
||||
|
||||
@@ -1051,7 +1098,7 @@ impl Interpreter {
|
||||
|
||||
fn eval_stmt_impl(&mut self, stmt: &LiteralStmt, stmts: &[&LiteralStmt]) -> Result<bool> {
|
||||
self.memory_check()?;
|
||||
|
||||
self.check_execution_time()?;
|
||||
Ok(match &stmt.literal {
|
||||
Literal::Expr { span, expr, .. } => {
|
||||
let value = match expr.as_ref() {
|
||||
@@ -1343,7 +1390,7 @@ impl Interpreter {
|
||||
loops: &[HoistedLoop],
|
||||
) -> Result<bool> {
|
||||
self.memory_check()?;
|
||||
|
||||
self.check_execution_time()?;
|
||||
if loops.is_empty() {
|
||||
if let Some((first_stmt, tail_stmts)) = stmts.split_first() {
|
||||
// Evaluate the current statement whose loop expressions have been hoisted.
|
||||
@@ -1569,6 +1616,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_rule_ref(&mut self, rule_refr: &ExprRef) -> Result<Vec<Value>> {
|
||||
self.check_execution_time()?;
|
||||
let mut comps = vec![];
|
||||
let mut expr = rule_refr;
|
||||
loop {
|
||||
@@ -1711,6 +1759,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_output_expr_in_loop(&mut self, loops: &[HoistedLoop]) -> Result<bool> {
|
||||
self.check_execution_time()?;
|
||||
if loops.is_empty() {
|
||||
let (key_expr, output_expr) = self.get_exprs_from_context()?;
|
||||
|
||||
@@ -1944,6 +1993,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_output_expr(&mut self) -> Result<bool> {
|
||||
self.check_execution_time()?;
|
||||
// Evaluate output expression after all the statements have been executed.
|
||||
|
||||
let (key_expr, output_expr) = self.get_exprs_from_context()?;
|
||||
@@ -2074,6 +2124,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_query(&mut self, query: &Ref<Query>) -> Result<bool> {
|
||||
self.check_execution_time()?;
|
||||
// Execute the query in a new scope
|
||||
self.scopes.push(Scope::new());
|
||||
let order_indices = {
|
||||
@@ -2157,6 +2208,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_array(&mut self, items: &Vec<ExprRef>) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
let mut array = Vec::new();
|
||||
|
||||
for item in items {
|
||||
@@ -2172,6 +2224,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_object(&mut self, fields: &Vec<(Span, ExprRef, ExprRef)>) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
let mut object = BTreeMap::new();
|
||||
|
||||
for (_, key, value) in fields {
|
||||
@@ -2194,6 +2247,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_set(&mut self, items: &Vec<ExprRef>) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
let mut set = BTreeSet::new();
|
||||
|
||||
for item in items {
|
||||
@@ -2213,6 +2267,7 @@ impl Interpreter {
|
||||
value: &ExprRef,
|
||||
collection: &ExprRef,
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
let value = self.eval_expr(value)?;
|
||||
let collection = self.eval_expr(collection)?;
|
||||
|
||||
@@ -2250,6 +2305,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_array_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// Push new context
|
||||
self.contexts.push(Context {
|
||||
output_expr: Some(term.clone()),
|
||||
@@ -2268,6 +2324,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_set_compr(&mut self, term: &ExprRef, query: &Ref<Query>) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// Push new context
|
||||
self.contexts.push(Context {
|
||||
output_expr: Some(term.clone()),
|
||||
@@ -2290,6 +2347,7 @@ impl Interpreter {
|
||||
value: &ExprRef,
|
||||
query: &Ref<Query>,
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// Push new context
|
||||
self.contexts.push(Context {
|
||||
key_expr: Some(key.clone()),
|
||||
@@ -2327,6 +2385,7 @@ impl Interpreter {
|
||||
params: &[ExprRef],
|
||||
args: Vec<Value>,
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// If any argument is undefined, then the call is undefined.
|
||||
if args.iter().any(|a| a == &Value::Undefined) {
|
||||
return Ok(Value::Undefined);
|
||||
@@ -2467,6 +2526,7 @@ impl Interpreter {
|
||||
fcn: &ExprRef,
|
||||
params: &[ExprRef],
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// Return generated values of walk builtin.
|
||||
if let Some(v) = self.get_loop_var_value(expr)? {
|
||||
return Ok(v.clone());
|
||||
@@ -2783,6 +2843,7 @@ impl Interpreter {
|
||||
extra_arg: Option<ExprRef>,
|
||||
allow_return_arg: bool,
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
// TODO: global var check; interop with `some var`
|
||||
if extra_arg.is_some() {
|
||||
let (last_param, arg_prefix) = params
|
||||
@@ -2832,6 +2893,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn ensure_module_evaluated(&mut self, path: String) -> Result<()> {
|
||||
self.check_execution_time()?;
|
||||
for module in self.compiled_policy.modules.clone().iter().cloned() {
|
||||
if Some(&module) == self.module.as_ref() {
|
||||
// Prevent cyclic evaluation.
|
||||
@@ -2875,6 +2937,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn ensure_rule_evaluated(&mut self, path: String) -> Result<()> {
|
||||
self.check_execution_time()?;
|
||||
let mut matched = false;
|
||||
if let Some(rules) = self.compiled_policy.rules.get(&path) {
|
||||
matched = true;
|
||||
@@ -3055,6 +3118,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_expr(&mut self, expr: &ExprRef) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
#[cfg(feature = "coverage")]
|
||||
if self.enable_coverage {
|
||||
let span = expr.span();
|
||||
@@ -3235,6 +3299,7 @@ impl Interpreter {
|
||||
span: &Span,
|
||||
bodies: &[RuleBody],
|
||||
) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
let n_scopes = self.scopes.len();
|
||||
let result = if bodies.is_empty() {
|
||||
self.contexts.push(ctx.clone());
|
||||
@@ -3496,6 +3561,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
pub fn eval_default_rule(&mut self, rule: &Ref<Rule>) -> Result<()> {
|
||||
self.check_execution_time()?;
|
||||
// Skip reprocessing rule.
|
||||
if self.processed.contains(rule) {
|
||||
return Ok(());
|
||||
@@ -3569,6 +3635,7 @@ impl Interpreter {
|
||||
/// Evaluate a default rule and return the resulting value for compiler consumers.
|
||||
#[cfg(feature = "rvm")]
|
||||
pub fn eval_default_rule_for_compiler(&mut self, rule_path: &str) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
self.input = Value::Undefined;
|
||||
self.data = Value::Undefined;
|
||||
self.ensure_loop_var_values_capacity();
|
||||
@@ -3658,6 +3725,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_rule_impl(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
|
||||
self.check_execution_time()?;
|
||||
match rule.as_ref() {
|
||||
Rule::Spec {
|
||||
span,
|
||||
@@ -3744,6 +3812,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
pub fn eval_rule(&mut self, module: &Ref<Module>, rule: &Ref<Rule>) -> Result<()> {
|
||||
self.check_execution_time()?;
|
||||
// Set current module index
|
||||
self.current_module_index = self.find_module_index(module);
|
||||
|
||||
@@ -3802,6 +3871,7 @@ impl Interpreter {
|
||||
query_schedule: Schedule,
|
||||
enable_tracing: bool,
|
||||
) -> Result<QueryResults> {
|
||||
self.check_execution_time()?;
|
||||
self.traces = match enable_tracing {
|
||||
true => Some(vec![]),
|
||||
false => None,
|
||||
@@ -4308,6 +4378,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
pub fn eval_rule_in_path(&mut self, path: String) -> Result<Value> {
|
||||
self.check_execution_time()?;
|
||||
if !self.compiled_policy.rule_paths.contains(&path) {
|
||||
bail!("not a valid rule path");
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ mod schema;
|
||||
pub mod target;
|
||||
#[cfg(any(test, all(feature = "yaml", feature = "std")))]
|
||||
pub mod test_utils;
|
||||
mod utils;
|
||||
pub mod utils;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
|
||||
@@ -18,8 +18,17 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rvm::program::Program;
|
||||
use crate::rvm::tests::instruction_parser::{parse_instruction, parse_loop_mode};
|
||||
use crate::rvm::tests::test_utils::test_round_trip_serialization;
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
use crate::utils::limits::set_time_source;
|
||||
use crate::utils::limits::{
|
||||
acquire_limits_test_lock, fallback_execution_timer_config,
|
||||
set_fallback_execution_timer_config, ExecutionTimerConfig, TimeSource,
|
||||
};
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
struct RuleInfoSpec {
|
||||
rule_type: String,
|
||||
@@ -48,11 +57,122 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::sync::{Mutex, Once};
|
||||
use test_generator::test_resources;
|
||||
|
||||
extern crate alloc;
|
||||
extern crate std;
|
||||
|
||||
struct FallbackGuard(Option<ExecutionTimerConfig>);
|
||||
impl Drop for FallbackGuard {
|
||||
fn drop(&mut self) {
|
||||
set_fallback_execution_timer_config(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_fallback_config(config: Option<ExecutionTimerConfig>) -> FallbackGuard {
|
||||
let previous = fallback_execution_timer_config();
|
||||
set_fallback_execution_timer_config(config);
|
||||
FallbackGuard(previous)
|
||||
}
|
||||
|
||||
struct TimeSourceGuard {
|
||||
previous_default: Duration,
|
||||
previous_template: Vec<Duration>,
|
||||
}
|
||||
|
||||
impl Drop for TimeSourceGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
state.default_increment = self.previous_default;
|
||||
state.template_increments = self.previous_template.clone();
|
||||
state.reset_from_template();
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_time_source(
|
||||
increments: Vec<Duration>,
|
||||
default_increment: Duration,
|
||||
) -> TimeSourceGuard {
|
||||
ensure_time_source_registered();
|
||||
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
|
||||
let guard = TimeSourceGuard {
|
||||
previous_default: state.default_increment,
|
||||
previous_template: state.template_increments.clone(),
|
||||
};
|
||||
|
||||
state.default_increment = default_increment;
|
||||
state.template_increments = increments;
|
||||
state.reset_from_template();
|
||||
|
||||
guard
|
||||
}
|
||||
|
||||
struct TestTimeSource;
|
||||
|
||||
struct TimeSourceState {
|
||||
current: Duration,
|
||||
started: bool,
|
||||
default_increment: Duration,
|
||||
increments: VecDeque<Duration>,
|
||||
template_increments: Vec<Duration>,
|
||||
}
|
||||
|
||||
impl TimeSourceState {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
current: Duration::ZERO,
|
||||
started: false,
|
||||
default_increment: Duration::from_millis(1),
|
||||
increments: VecDeque::new(),
|
||||
template_increments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_template(&mut self) {
|
||||
self.current = Duration::ZERO;
|
||||
self.started = false;
|
||||
self.increments = VecDeque::from(self.template_increments.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeSource for TestTimeSource {
|
||||
fn now(&self) -> Option<Duration> {
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
|
||||
if !state.started {
|
||||
state.started = true;
|
||||
return Some(state.current);
|
||||
}
|
||||
|
||||
let increment = state
|
||||
.increments
|
||||
.pop_front()
|
||||
.unwrap_or(state.default_increment);
|
||||
state.current = state.current.saturating_add(increment);
|
||||
Some(state.current)
|
||||
}
|
||||
}
|
||||
|
||||
static TEST_TIME_SOURCE: TestTimeSource = TestTimeSource;
|
||||
static TIME_SOURCE_STATE: Mutex<TimeSourceState> = Mutex::new(TimeSourceState::new());
|
||||
static TIME_SOURCE_ONCE: Once = Once::new();
|
||||
|
||||
fn ensure_time_source_registered() {
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
TIME_SOURCE_ONCE.call_once(|| {
|
||||
let _ = set_time_source(&TEST_TIME_SOURCE);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct HostAwaitResponseSpec {
|
||||
id: crate::Value,
|
||||
@@ -62,6 +182,13 @@ mod tests {
|
||||
values: Vec<crate::Value>,
|
||||
}
|
||||
|
||||
fn default_vm_test_execution_timer_config() -> ExecutionTimerConfig {
|
||||
ExecutionTimerConfig {
|
||||
limit: Duration::from_secs(1),
|
||||
check_interval: NonZeroU32::new(100).unwrap_or(NonZeroU32::MIN),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct VmTestCase {
|
||||
note: String,
|
||||
@@ -644,6 +771,7 @@ mod tests {
|
||||
vm.set_execution_mode(mode);
|
||||
vm.set_step_mode(use_step_mode);
|
||||
vm.set_strict_builtin_errors(strict);
|
||||
vm.set_execution_timer_config(Some(default_vm_test_execution_timer_config()));
|
||||
|
||||
if let Some(data_value) = processed_data.clone() {
|
||||
vm.set_data(data_value)?;
|
||||
@@ -1001,6 +1129,158 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_execution_time_limit_triggers_error() -> Result<()> {
|
||||
use crate::rvm::instructions::Instruction;
|
||||
use crate::utils::limits::acquire_limits_test_lock;
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
|
||||
let _lock = acquire_limits_test_lock();
|
||||
let config = ExecutionTimerConfig {
|
||||
limit: Duration::from_nanos(1),
|
||||
check_interval: NonZeroU32::new(1).unwrap(),
|
||||
};
|
||||
let _guard = install_fallback_config(Some(config));
|
||||
|
||||
let mut program = Program::new();
|
||||
program.dispatch_window_size = 2;
|
||||
program.max_rule_window_size = 2;
|
||||
program.entry_points.insert("main".to_string(), 0);
|
||||
|
||||
const INSTRUCTION_COUNT: usize = 60_000;
|
||||
program.instructions = (0..INSTRUCTION_COUNT)
|
||||
.map(|_| Instruction::LoadNull { dest: 0 })
|
||||
.collect();
|
||||
program.instructions.push(Instruction::Return { value: 0 });
|
||||
program.instruction_spans = alloc::vec![None; program.instructions.len()];
|
||||
program.main_entry_point = 0;
|
||||
|
||||
let program = Arc::new(program);
|
||||
|
||||
let mut vm = RegoVM::new();
|
||||
vm.set_max_instructions(usize::MAX);
|
||||
vm.load_program(program);
|
||||
|
||||
let result = vm.execute();
|
||||
assert!(
|
||||
matches!(result, Err(VmError::TimeLimitExceeded { .. })),
|
||||
"expected time limit error but got {result:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_execution_time_limit_override_allows_completion() -> Result<()> {
|
||||
use crate::rvm::instructions::Instruction;
|
||||
use crate::utils::limits::acquire_limits_test_lock;
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
|
||||
let _lock = acquire_limits_test_lock();
|
||||
let strict_config = ExecutionTimerConfig {
|
||||
limit: Duration::from_nanos(1),
|
||||
check_interval: NonZeroU32::new(1).unwrap(),
|
||||
};
|
||||
let _guard = install_fallback_config(Some(strict_config));
|
||||
|
||||
let mut program = Program::new();
|
||||
program.dispatch_window_size = 2;
|
||||
program.max_rule_window_size = 2;
|
||||
program.entry_points.insert("main".to_string(), 0);
|
||||
program.instructions = alloc::vec![
|
||||
Instruction::LoadNull { dest: 0 },
|
||||
Instruction::Return { value: 0 },
|
||||
];
|
||||
program.instruction_spans = alloc::vec![None; program.instructions.len()];
|
||||
program.main_entry_point = 0;
|
||||
|
||||
let program = Arc::new(program);
|
||||
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(program);
|
||||
|
||||
let relaxed_config = ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(10),
|
||||
check_interval: NonZeroU32::new(1).unwrap(),
|
||||
};
|
||||
vm.set_execution_timer_config(Some(relaxed_config));
|
||||
|
||||
let result = vm.execute();
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"expected successful execution, got {result:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vm_suspend_resume_excludes_suspended_time_from_limit() -> Result<()> {
|
||||
use crate::rvm::instructions::Instruction;
|
||||
|
||||
let _lock = acquire_limits_test_lock();
|
||||
let _guard = install_fallback_config(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(10),
|
||||
check_interval: NonZeroU32::new(1).unwrap(),
|
||||
}));
|
||||
|
||||
let _time_guard = configure_time_source(
|
||||
alloc::vec![
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(100),
|
||||
Duration::from_millis(1),
|
||||
],
|
||||
Duration::from_millis(1),
|
||||
);
|
||||
|
||||
let mut program = Program::new();
|
||||
program.dispatch_window_size = 3;
|
||||
program.max_rule_window_size = 3;
|
||||
program.entry_points.insert("main".to_string(), 0);
|
||||
program.literals = alloc::vec![Value::from("id"), Value::from(1)];
|
||||
program.instructions = alloc::vec![
|
||||
Instruction::Load {
|
||||
dest: 0,
|
||||
literal_idx: 0
|
||||
},
|
||||
Instruction::Load {
|
||||
dest: 1,
|
||||
literal_idx: 1
|
||||
},
|
||||
Instruction::HostAwait {
|
||||
dest: 2,
|
||||
arg: 1,
|
||||
id: 0
|
||||
},
|
||||
Instruction::Return { value: 2 },
|
||||
];
|
||||
program.instruction_spans = alloc::vec![None; program.instructions.len()];
|
||||
program.main_entry_point = 0;
|
||||
|
||||
let program = Arc::new(program);
|
||||
let mut vm = RegoVM::new();
|
||||
vm.set_execution_mode(ExecutionMode::Suspendable);
|
||||
vm.load_program(program);
|
||||
|
||||
let _ = vm.execute()?;
|
||||
match vm.execution_state() {
|
||||
ExecutionState::Suspended { reason, .. } => {
|
||||
assert!(matches!(reason, SuspendReason::HostAwait { .. }));
|
||||
}
|
||||
other => panic!("expected suspension, got {other:?}"),
|
||||
}
|
||||
|
||||
let resumed = vm.resume(Some(Value::from(42)))?;
|
||||
assert_eq!(resumed, Value::from(42));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_resources("tests/rvm/vm/suites/*.yaml")]
|
||||
fn run_vm_test_file(file: &str) {
|
||||
run_vm_test_suite(file).unwrap()
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::execution_model::SuspendReason;
|
||||
use crate::value::Value;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
/// VM execution errors
|
||||
@@ -17,7 +18,14 @@ pub enum VmError {
|
||||
pc: usize,
|
||||
},
|
||||
|
||||
#[error("Execution stopped: exceeded maximum memory limit of {limit} bytes with usage {usage} bytes (pc={pc})")]
|
||||
#[error("Execution exceeded time limit (elapsed={elapsed:?}, limit={limit:?}, pc={pc})")]
|
||||
TimeLimitExceeded {
|
||||
elapsed: Duration,
|
||||
limit: Duration,
|
||||
pc: usize,
|
||||
},
|
||||
|
||||
#[error("Execution exceeded memory limit (usage={usage} bytes, limit={limit} bytes, pc={pc})")]
|
||||
MemoryLimitExceeded { usage: u64, limit: u64, pc: usize },
|
||||
|
||||
#[error("Literal index {index} out of bounds (pc={pc})")]
|
||||
|
||||
@@ -59,6 +59,7 @@ impl RegoVM {
|
||||
match self.execution_mode {
|
||||
ExecutionMode::RunToCompletion => {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
|
||||
self.validate_vm_state()?;
|
||||
let entry_point_pc_u32 = u32::try_from(entry_point_pc).map_err(|_| {
|
||||
@@ -73,6 +74,7 @@ impl RegoVM {
|
||||
}
|
||||
ExecutionMode::Suspendable => {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
|
||||
self.validate_vm_state()?;
|
||||
self.execute_suspendable_entry(entry_point_pc)
|
||||
@@ -101,6 +103,7 @@ impl RegoVM {
|
||||
match self.execution_mode {
|
||||
ExecutionMode::RunToCompletion => {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
|
||||
self.validate_vm_state()?;
|
||||
let entry_point_pc_u32 = u32::try_from(entry_point_pc).map_err(|_| {
|
||||
@@ -115,6 +118,7 @@ impl RegoVM {
|
||||
}
|
||||
ExecutionMode::Suspendable => {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
|
||||
self.validate_vm_state()?;
|
||||
self.execute_suspendable_entry(entry_point_pc)
|
||||
@@ -136,6 +140,7 @@ impl RegoVM {
|
||||
});
|
||||
}
|
||||
|
||||
self.execution_timer_tick(1)?;
|
||||
self.executed_instructions = self.executed_instructions.saturating_add(1);
|
||||
let instruction = program.instructions.get(self.pc).cloned().ok_or(
|
||||
VmError::ProgramCounterOutOfBounds {
|
||||
@@ -168,6 +173,7 @@ impl RegoVM {
|
||||
|
||||
fn execute_run_to_completion(&mut self) -> Result<Value> {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
self.execution_state = ExecutionState::Running;
|
||||
match self.jump_to(0_u32) {
|
||||
Ok(value) => {
|
||||
@@ -185,6 +191,7 @@ impl RegoVM {
|
||||
|
||||
fn execute_suspendable(&mut self) -> Result<Value> {
|
||||
self.reset_execution_state();
|
||||
self.reset_execution_timer_state();
|
||||
self.execution_state = ExecutionState::Running;
|
||||
match self.run_stackless_from(0) {
|
||||
Ok(result) => Ok(result),
|
||||
@@ -197,6 +204,7 @@ impl RegoVM {
|
||||
|
||||
fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result<Value> {
|
||||
self.execution_state = ExecutionState::Running;
|
||||
self.reset_execution_timer_state();
|
||||
match self.run_stackless_from(entry_point_pc) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(err) => {
|
||||
@@ -256,6 +264,7 @@ impl RegoVM {
|
||||
}
|
||||
|
||||
self.execution_state = ExecutionState::Running;
|
||||
self.restore_execution_timer_after_resume();
|
||||
|
||||
let program = self.program.clone();
|
||||
self.run_stackless_loop(&program, &mut last_result)?;
|
||||
@@ -376,6 +385,10 @@ impl RegoVM {
|
||||
}
|
||||
|
||||
self.pc = frame_pc;
|
||||
if let Err(err) = self.execution_timer_tick(1) {
|
||||
self.execution_state = ExecutionState::Error { error: err.clone() };
|
||||
return Err(err);
|
||||
}
|
||||
let instruction = program.instructions.get(self.pc).cloned().ok_or(
|
||||
VmError::ProgramCounterOutOfBounds {
|
||||
pc: self.pc,
|
||||
@@ -460,6 +473,7 @@ impl RegoVM {
|
||||
}
|
||||
}
|
||||
|
||||
self.snapshot_execution_timer_on_suspend();
|
||||
self.execution_state = ExecutionState::Suspended {
|
||||
reason,
|
||||
pc: self.pc,
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
|
||||
use crate::rvm::program::Program;
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
use crate::utils::limits::{self, LimitError};
|
||||
use crate::utils::limits;
|
||||
use crate::utils::limits::{
|
||||
fallback_execution_timer_config, monotonic_now, ExecutionTimer, ExecutionTimerConfig,
|
||||
LimitError,
|
||||
};
|
||||
use crate::value::Value;
|
||||
use crate::CompiledPolicy;
|
||||
use alloc::collections::{btree_map::Entry, BTreeMap, VecDeque};
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::time::Duration;
|
||||
|
||||
use super::context::{CallRuleContext, ComprehensionContext, LoopContext};
|
||||
use super::errors::{Result, VmError};
|
||||
@@ -105,6 +111,15 @@ pub struct RegoVM {
|
||||
|
||||
/// Cache for builtin calls that must stay deterministic across a single evaluation
|
||||
pub(super) builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
|
||||
|
||||
/// Optional override for the execution timer configuration
|
||||
pub(super) execution_timer_config: Option<ExecutionTimerConfig>,
|
||||
|
||||
/// Cooperative execution timer used to enforce wall-clock limits
|
||||
pub(super) execution_timer: ExecutionTimer,
|
||||
|
||||
/// Elapsed wall-clock time recorded when the VM entered a suspended state
|
||||
pub(super) execution_timer_elapsed_at_suspend: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for RegoVM {
|
||||
@@ -116,6 +131,8 @@ impl Default for RegoVM {
|
||||
impl RegoVM {
|
||||
/// Create a new virtual machine
|
||||
pub fn new() -> Self {
|
||||
let fallback_timer = fallback_execution_timer_config();
|
||||
|
||||
RegoVM {
|
||||
registers: Vec::new(), // Start with no registers - will be resized when program is loaded
|
||||
pc: 0,
|
||||
@@ -143,6 +160,9 @@ impl RegoVM {
|
||||
frame_pc_overridden: false,
|
||||
strict_builtin_errors: false,
|
||||
builtins_cache: BTreeMap::new(),
|
||||
execution_timer_config: None,
|
||||
execution_timer: ExecutionTimer::new(fallback_timer),
|
||||
execution_timer_elapsed_at_suspend: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +339,93 @@ impl RegoVM {
|
||||
self.execution_mode
|
||||
}
|
||||
|
||||
/// Configure the execution timer to use the supplied configuration, or fall back to the global
|
||||
/// default when `None` is provided.
|
||||
pub fn set_execution_timer_config(&mut self, config: Option<ExecutionTimerConfig>) {
|
||||
self.execution_timer_config = config;
|
||||
self.reset_execution_timer_state();
|
||||
}
|
||||
|
||||
/// Returns the currently configured execution timer, if any.
|
||||
pub const fn execution_timer_config(&self) -> Option<ExecutionTimerConfig> {
|
||||
self.execution_timer_config
|
||||
}
|
||||
|
||||
pub(super) fn reset_execution_timer_state(&mut self) {
|
||||
let config = self.effective_execution_timer_config();
|
||||
self.execution_timer = ExecutionTimer::new(config);
|
||||
self.execution_timer_elapsed_at_suspend = None;
|
||||
|
||||
if config.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(now) = monotonic_now() {
|
||||
self.execution_timer.start(now);
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_execution_timer_config(&self) -> Option<ExecutionTimerConfig> {
|
||||
self.execution_timer_config
|
||||
.or_else(fallback_execution_timer_config)
|
||||
}
|
||||
|
||||
pub(super) fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
|
||||
if self.execution_timer.limit().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(now) = monotonic_now() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.execution_timer
|
||||
.tick(work_units, now)
|
||||
.map_err(|err| match err {
|
||||
LimitError::TimeLimitExceeded { elapsed, limit } => VmError::TimeLimitExceeded {
|
||||
elapsed,
|
||||
limit,
|
||||
pc: self.pc,
|
||||
},
|
||||
LimitError::MemoryLimitExceeded { usage, limit } => VmError::MemoryLimitExceeded {
|
||||
usage,
|
||||
limit,
|
||||
pc: self.pc,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_execution_timer_on_suspend(&mut self) {
|
||||
if self.execution_timer.config().is_none() {
|
||||
self.execution_timer_elapsed_at_suspend = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(now) = monotonic_now() else {
|
||||
self.execution_timer_elapsed_at_suspend = None;
|
||||
return;
|
||||
};
|
||||
|
||||
self.execution_timer_elapsed_at_suspend = self.execution_timer.elapsed(now);
|
||||
}
|
||||
|
||||
pub(super) fn restore_execution_timer_after_resume(&mut self) {
|
||||
if self.execution_timer.config().is_none() {
|
||||
self.execution_timer_elapsed_at_suspend = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(elapsed) = self.execution_timer_elapsed_at_suspend.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(now) = monotonic_now() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.execution_timer.resume_from_elapsed(now, elapsed);
|
||||
}
|
||||
|
||||
/// Get the current execution state of the VM
|
||||
pub const fn execution_state(&self) -> &ExecutionState {
|
||||
&self.execution_state
|
||||
|
||||
@@ -15,11 +15,197 @@
|
||||
use std::env;
|
||||
|
||||
use crate::test_utils::{check_output, ValueOrVec};
|
||||
use crate::utils::limits::{
|
||||
acquire_limits_test_lock, fallback_execution_timer_config, ExecutionTimerConfig,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use test_generator::test_resources;
|
||||
use timer_test_support::{
|
||||
apply_engine_timer, configure_time_source, reset_time_source, GlobalTimerGuard,
|
||||
};
|
||||
|
||||
mod timer_test_support {
|
||||
use super::{ExecutionTimerTestConfig, TimeSourceTestConfig};
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
use crate::utils::limits::set_time_source;
|
||||
use crate::utils::limits::{
|
||||
fallback_execution_timer_config, set_fallback_execution_timer_config, ExecutionTimerConfig,
|
||||
TimeSource,
|
||||
};
|
||||
use crate::Engine;
|
||||
use anyhow::{anyhow, Result};
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Mutex, Once};
|
||||
use std::vec::Vec;
|
||||
|
||||
pub struct GlobalTimerGuard {
|
||||
previous: Option<ExecutionTimerConfig>,
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
impl GlobalTimerGuard {
|
||||
pub fn apply(spec: Option<&ExecutionTimerTestConfig>) -> Result<Self> {
|
||||
let previous = fallback_execution_timer_config();
|
||||
let mut changed = false;
|
||||
|
||||
if let Some(config_spec) = spec {
|
||||
if config_spec.disable.unwrap_or(false) {
|
||||
set_fallback_execution_timer_config(None);
|
||||
changed = true;
|
||||
} else {
|
||||
let config = config_from_spec(config_spec)?;
|
||||
set_fallback_execution_timer_config(config);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { previous, changed })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlobalTimerGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.changed {
|
||||
set_fallback_execution_timer_config(self.previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_time_source(spec: Option<&TimeSourceTestConfig>) {
|
||||
ensure_time_source_registered();
|
||||
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.expect("time source mutex poisoned");
|
||||
|
||||
if let Some(cfg) = spec {
|
||||
state.default_increment = cfg
|
||||
.default_increment_ms
|
||||
.map(Duration::from_millis)
|
||||
.unwrap_or(DEFAULT_INCREMENT);
|
||||
state.template_increments = cfg
|
||||
.increments_ms
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Duration::from_millis)
|
||||
.collect();
|
||||
} else {
|
||||
state.default_increment = DEFAULT_INCREMENT;
|
||||
state.template_increments.clear();
|
||||
}
|
||||
|
||||
state.reset_from_template();
|
||||
}
|
||||
|
||||
pub fn reset_time_source() {
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.expect("time source mutex poisoned");
|
||||
state.reset_from_template();
|
||||
}
|
||||
|
||||
pub fn apply_engine_timer(engine: &mut Engine, spec: &ExecutionTimerTestConfig) -> Result<()> {
|
||||
if spec.disable.unwrap_or(false) {
|
||||
engine.clear_execution_timer_config();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match config_from_spec(spec)? {
|
||||
Some(config) => engine.set_execution_timer_config(config),
|
||||
None => engine.clear_execution_timer_config(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DEFAULT_INCREMENT: Duration = Duration::from_millis(1);
|
||||
|
||||
struct TestTimeSource;
|
||||
|
||||
struct TimeSourceState {
|
||||
current: Duration,
|
||||
started: bool,
|
||||
default_increment: Duration,
|
||||
increments: VecDeque<Duration>,
|
||||
template_increments: Vec<Duration>,
|
||||
}
|
||||
|
||||
impl TimeSourceState {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
current: Duration::ZERO,
|
||||
started: false,
|
||||
default_increment: DEFAULT_INCREMENT,
|
||||
increments: VecDeque::new(),
|
||||
template_increments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_from_template(&mut self) {
|
||||
self.current = Duration::ZERO;
|
||||
self.started = false;
|
||||
self.increments = VecDeque::from(self.template_increments.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeSource for TestTimeSource {
|
||||
fn now(&self) -> Option<Duration> {
|
||||
let mut state = TIME_SOURCE_STATE
|
||||
.lock()
|
||||
.expect("time source mutex poisoned");
|
||||
|
||||
if !state.started {
|
||||
state.started = true;
|
||||
return Some(state.current);
|
||||
}
|
||||
|
||||
let increment = state
|
||||
.increments
|
||||
.pop_front()
|
||||
.unwrap_or(state.default_increment);
|
||||
state.current = state.current.saturating_add(increment);
|
||||
Some(state.current)
|
||||
}
|
||||
}
|
||||
|
||||
static TEST_TIME_SOURCE: TestTimeSource = TestTimeSource;
|
||||
static TIME_SOURCE_STATE: Mutex<TimeSourceState> = Mutex::new(TimeSourceState::new());
|
||||
static TIME_SOURCE_ONCE: Once = Once::new();
|
||||
|
||||
fn ensure_time_source_registered() {
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
TIME_SOURCE_ONCE.call_once(|| {
|
||||
let _ = set_time_source(&TEST_TIME_SOURCE);
|
||||
});
|
||||
}
|
||||
|
||||
fn config_from_spec(spec: &ExecutionTimerTestConfig) -> Result<Option<ExecutionTimerConfig>> {
|
||||
let limit_ms = match spec.limit_ms {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let check_interval = spec
|
||||
.check_interval
|
||||
.map(|interval| {
|
||||
NonZeroU32::new(interval)
|
||||
.ok_or_else(|| anyhow!("execution_timer.check_interval must be non-zero"))
|
||||
})
|
||||
.transpose()? // Result<Option<NonZeroU32>>
|
||||
.unwrap_or(NonZeroU32::MIN);
|
||||
|
||||
Ok(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(limit_ms),
|
||||
check_interval,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod load_target_definitions {
|
||||
@@ -158,6 +344,7 @@ fn push_query_results(query_results: QueryResults, results: &mut Vec<Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn eval_file(
|
||||
regos: &[String],
|
||||
data_opt: Option<Value>,
|
||||
@@ -166,6 +353,7 @@ pub fn eval_file(
|
||||
enable_tracing: bool,
|
||||
strict: bool,
|
||||
v0: bool,
|
||||
execution_timer: Option<&ExecutionTimerTestConfig>,
|
||||
) -> Result<(Vec<Value>, Vec<String>)> {
|
||||
let mut engine: Engine = Engine::new();
|
||||
engine.set_rego_v0(v0);
|
||||
@@ -175,6 +363,15 @@ pub fn eval_file(
|
||||
#[cfg(feature = "coverage")]
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
let use_default_timer =
|
||||
execution_timer.is_none() && fallback_execution_timer_config().is_none();
|
||||
|
||||
if let Some(spec) = execution_timer {
|
||||
apply_engine_timer(&mut engine, spec)?;
|
||||
} else if use_default_timer {
|
||||
engine.set_execution_timer_config(default_engine_execution_timer_config());
|
||||
}
|
||||
|
||||
let mut results = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
@@ -199,10 +396,17 @@ pub fn eval_file(
|
||||
}
|
||||
|
||||
let mut engine_full = engine.clone();
|
||||
if let Some(spec) = execution_timer {
|
||||
apply_engine_timer(&mut engine_full, spec)?;
|
||||
} else if use_default_timer {
|
||||
engine_full.set_execution_timer_config(default_engine_execution_timer_config());
|
||||
}
|
||||
|
||||
if inputs.is_empty() {
|
||||
// Now eval the query.
|
||||
reset_time_source();
|
||||
let r = engine.eval_query(query.to_string(), enable_tracing)?;
|
||||
reset_time_source();
|
||||
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
|
||||
if r != r_full {
|
||||
std::println!(
|
||||
@@ -220,7 +424,9 @@ pub fn eval_file(
|
||||
engine_full.set_input(input);
|
||||
|
||||
// Now eval the query.
|
||||
reset_time_source();
|
||||
let r = engine.eval_query(query.to_string(), enable_tracing)?;
|
||||
reset_time_source();
|
||||
let r_full = engine_full.eval_query_and_all_rules(query.to_string(), enable_tracing)?;
|
||||
if r != r_full {
|
||||
std::println!(
|
||||
@@ -239,6 +445,7 @@ pub fn eval_file(
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn eval_file_with_rule_evaluation(
|
||||
regos: &[String],
|
||||
data_opt: Option<Value>,
|
||||
@@ -247,6 +454,7 @@ pub fn eval_file_with_rule_evaluation(
|
||||
_enable_tracing: bool,
|
||||
strict: bool,
|
||||
v0: bool,
|
||||
execution_timer: Option<&ExecutionTimerTestConfig>,
|
||||
) -> Result<(Vec<Value>, Vec<String>)> {
|
||||
let mut engine: Engine = Engine::new();
|
||||
engine.set_rego_v0(v0);
|
||||
@@ -256,6 +464,15 @@ pub fn eval_file_with_rule_evaluation(
|
||||
#[cfg(feature = "coverage")]
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
let use_default_timer =
|
||||
execution_timer.is_none() && fallback_execution_timer_config().is_none();
|
||||
|
||||
if let Some(spec) = execution_timer {
|
||||
apply_engine_timer(&mut engine, spec)?;
|
||||
} else if use_default_timer {
|
||||
engine.set_execution_timer_config(default_engine_execution_timer_config());
|
||||
}
|
||||
|
||||
let mut results = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
@@ -288,7 +505,9 @@ pub fn eval_file_with_rule_evaluation(
|
||||
for input in inputs {
|
||||
engine.set_input(input.clone());
|
||||
// Use eval_rule instead of eval_query for target tests
|
||||
reset_time_source();
|
||||
let r_engine = engine.eval_rule(query.to_string())?;
|
||||
reset_time_source();
|
||||
let r_compiled_policy = compiled_policy.eval_with_input(input)?;
|
||||
assert_eq!(r_engine, r_compiled_policy);
|
||||
results.push(r_engine);
|
||||
@@ -297,6 +516,21 @@ pub fn eval_file_with_rule_evaluation(
|
||||
Ok((results, engine.take_prints()?))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ExecutionTimerTestConfig {
|
||||
limit_ms: Option<u64>,
|
||||
check_interval: Option<u32>,
|
||||
disable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default)]
|
||||
#[serde(default)]
|
||||
pub struct TimeSourceTestConfig {
|
||||
increments_ms: Vec<u64>,
|
||||
default_increment_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct TestCase {
|
||||
data: Option<Value>,
|
||||
@@ -315,18 +549,33 @@ struct TestCase {
|
||||
want_error_code: Option<String>,
|
||||
#[serde(default = "default_strict")]
|
||||
strict: bool,
|
||||
#[serde(default)]
|
||||
execution_timer: Option<ExecutionTimerTestConfig>,
|
||||
#[serde(default)]
|
||||
global_execution_timer: Option<ExecutionTimerTestConfig>,
|
||||
#[serde(default)]
|
||||
time_source: Option<TimeSourceTestConfig>,
|
||||
}
|
||||
|
||||
fn default_strict() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_engine_execution_timer_config() -> ExecutionTimerConfig {
|
||||
ExecutionTimerConfig {
|
||||
limit: Duration::from_secs(5),
|
||||
check_interval: NonZeroU32::new(100).unwrap_or(NonZeroU32::MIN),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct YamlTest {
|
||||
cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
let _limits_lock = acquire_limits_test_lock();
|
||||
|
||||
let yaml_str = std::fs::read_to_string(file)?;
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
|
||||
|
||||
@@ -382,6 +631,9 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _timer_guard = GlobalTimerGuard::apply(case.global_execution_timer.as_ref())?;
|
||||
configure_time_source(case.time_source.as_ref());
|
||||
|
||||
match (&case.want_result, &case.error) {
|
||||
(Some(_), None) | (None, Some(_)) => (),
|
||||
_ if case.no_result != Some(true) => {
|
||||
@@ -405,6 +657,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
enable_tracing,
|
||||
case.strict,
|
||||
v0,
|
||||
case.execution_timer.as_ref(),
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "azure_policy"))]
|
||||
@@ -420,6 +673,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
enable_tracing,
|
||||
case.strict,
|
||||
v0,
|
||||
case.execution_timer.as_ref(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
mod error;
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
mod memory;
|
||||
mod time;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use error::LimitError;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
pub use memory::{
|
||||
@@ -18,6 +21,19 @@ pub use memory::{
|
||||
thread_memory_flush_threshold,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use time::{
|
||||
fallback_execution_timer_config, monotonic_now, set_fallback_execution_timer_config,
|
||||
ExecutionTimer, ExecutionTimerConfig, TimeSource,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use time::acquire_limits_test_lock;
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
#[allow(unused_imports)]
|
||||
pub use time::{set_time_source, TimeSourceRegistrationError};
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
#[inline]
|
||||
pub fn check_memory_limit_if_needed() -> core::result::Result<(), LimitError> {
|
||||
@@ -26,12 +42,12 @@ pub fn check_memory_limit_if_needed() -> core::result::Result<(), LimitError> {
|
||||
|
||||
#[cfg(not(feature = "allocator-memory-limits"))]
|
||||
#[inline]
|
||||
pub fn enforce_memory_limit() -> core::result::Result<(), LimitError> {
|
||||
pub const fn enforce_memory_limit() -> core::result::Result<(), LimitError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "allocator-memory-limits"))]
|
||||
#[inline]
|
||||
pub fn check_memory_limit_if_needed() -> core::result::Result<(), LimitError> {
|
||||
pub const fn check_memory_limit_if_needed() -> core::result::Result<(), LimitError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
474
src/utils/limits/time.rs
Normal file
474
src/utils/limits/time.rs
Normal file
@@ -0,0 +1,474 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
/*
|
||||
ExecutionTimer provides cooperative wall-clock enforcement for long-running
|
||||
policy evaluations. The timer tracks three pieces of state:
|
||||
- ExecutionTimerConfig, which holds the optional wall-clock budget and the
|
||||
interval (in work units) between time checks.
|
||||
- The monotonic start instant recorded via start(now), expressed as a
|
||||
Duration from whatever time source the engine uses.
|
||||
- An accumulator that counts work units so callers can amortize expensive
|
||||
time queries; once the counter reaches the configured interval, tick()
|
||||
performs a check and preserves any remainder.
|
||||
|
||||
The timer never calls into a clock directly. Instead, callers pass the
|
||||
current monotonic Duration to start(), tick(), check_now(), or elapsed().
|
||||
Helper monotonic_now() returns that Duration by selecting a TimeSource
|
||||
implementation:
|
||||
- On std builds we use StdTimeSource, which anchors a std::time::Instant via
|
||||
OnceLock and reports elapsed() for stable, monotonic measurements.
|
||||
- In tests and truly no_std builds we allow integrators to inject a global
|
||||
&'static dyn TimeSource using set_time_source(). This override lives behind
|
||||
a spin::Mutex<Option<...>> so the critical section stays small (just a
|
||||
pointer read) while remaining usable in bare-metal environments.
|
||||
|
||||
With this design the interpreter can cheaply interleave work with periodic
|
||||
limit checks. Std builds automatically use the Instant-backed source, while
|
||||
embedded users configure both their ExecutionTimerConfig and a single global
|
||||
time source without paying for per-interpreter callbacks or unsafe code.
|
||||
*/
|
||||
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
|
||||
use spin::Mutex;
|
||||
|
||||
use super::LimitError;
|
||||
|
||||
#[cfg(test)]
|
||||
use std::sync::{Mutex as StdMutex, MutexGuard as StdMutexGuard};
|
||||
|
||||
/// Public configuration for the cooperative execution time limiter.
|
||||
///
|
||||
/// The limiter reads this struct to determine how often it should check for wall-clock overruns and
|
||||
/// what deadline to enforce. Engines without a configuration skip time checks; when a configuration
|
||||
/// is present, it normally pairs a concrete deadline with a small [`NonZeroU32`] interval so
|
||||
/// interpreter loops amortize their clock reads without skipping checks for long stretches of
|
||||
/// repetitive work. The process-wide fallback installed via [`set_fallback_execution_timer_config`]
|
||||
/// supplies this configuration when an engine lacks its own override.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ExecutionTimerConfig {
|
||||
/// Maximum allowed wall-clock duration.
|
||||
pub limit: Duration,
|
||||
/// Number of work units between time checks (minimum 1).
|
||||
pub check_interval: NonZeroU32,
|
||||
}
|
||||
|
||||
/// Cooperative time-limit tracker shared across interpreter and VM loops.
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutionTimer {
|
||||
config: Option<ExecutionTimerConfig>,
|
||||
start: Option<Duration>,
|
||||
accumulated_units: u32,
|
||||
last_elapsed: Duration,
|
||||
}
|
||||
|
||||
/// Monotonic time provider.
|
||||
pub trait TimeSource: Send + Sync {
|
||||
/// Returns a non-decreasing duration since an arbitrary anchor.
|
||||
fn now(&self) -> Option<Duration>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Debug)]
|
||||
struct StdTimeSource;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl StdTimeSource {
|
||||
const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl TimeSource for StdTimeSource {
|
||||
fn now(&self) -> Option<Duration> {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static ANCHOR: OnceLock<std::time::Instant> = OnceLock::new();
|
||||
let anchor = ANCHOR.get_or_init(std::time::Instant::now);
|
||||
Some(anchor.elapsed())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
static STD_TIME_SOURCE: StdTimeSource = StdTimeSource::new();
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
static TIME_SOURCE_OVERRIDE: Mutex<Option<&'static dyn TimeSource>> = Mutex::new(None);
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TimeSourceRegistrationError {
|
||||
AlreadySet,
|
||||
}
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
impl core::fmt::Display for TimeSourceRegistrationError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::AlreadySet => f.write_str("time source already configured"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
impl core::error::Error for TimeSourceRegistrationError {}
|
||||
|
||||
static FALLBACK_EXECUTION_TIMER_CONFIG: Mutex<Option<ExecutionTimerConfig>> = Mutex::new(None);
|
||||
|
||||
#[cfg(test)]
|
||||
static LIMITS_TEST_LOCK: StdMutex<()> = StdMutex::new(());
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn acquire_limits_test_lock() -> StdMutexGuard<'static, ()> {
|
||||
LIMITS_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
/// Returns the duration supplied by the chosen source for this build.
|
||||
pub fn monotonic_now() -> Option<Duration> {
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
// Spin mutex acquisition incurs only a few atomic ops; the critical section
|
||||
// is a single pointer read, so uncontended overhead stays tiny.
|
||||
if let Some(source) = {
|
||||
let guard = TIME_SOURCE_OVERRIDE.lock();
|
||||
*guard
|
||||
} {
|
||||
if let Some(duration) = source.now() {
|
||||
return Some(duration);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
STD_TIME_SOURCE.now()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, not(feature = "std")))]
|
||||
pub fn set_time_source(source: &'static dyn TimeSource) -> Result<(), TimeSourceRegistrationError> {
|
||||
let mut slot = TIME_SOURCE_OVERRIDE.lock();
|
||||
if slot.is_some() {
|
||||
Err(TimeSourceRegistrationError::AlreadySet)
|
||||
} else {
|
||||
*slot = Some(source);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the process-wide fallback configuration for the execution time limiter. Engine instances can
|
||||
/// override this fallback via [`Engine::set_execution_timer_config`](crate::Engine::set_execution_timer_config).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU32;
|
||||
/// use std::time::Duration;
|
||||
/// use regorus::utils::limits::{
|
||||
/// fallback_execution_timer_config,
|
||||
/// set_fallback_execution_timer_config,
|
||||
/// ExecutionTimerConfig,
|
||||
/// };
|
||||
///
|
||||
/// let config = ExecutionTimerConfig {
|
||||
/// limit: Duration::from_secs(1),
|
||||
/// check_interval: NonZeroU32::new(10).unwrap(),
|
||||
/// };
|
||||
/// set_fallback_execution_timer_config(Some(config));
|
||||
/// assert_eq!(fallback_execution_timer_config(), Some(config));
|
||||
/// ```
|
||||
pub fn set_fallback_execution_timer_config(config: Option<ExecutionTimerConfig>) {
|
||||
*FALLBACK_EXECUTION_TIMER_CONFIG.lock() = config;
|
||||
}
|
||||
|
||||
/// Returns the process-wide fallback configuration for the execution time limiter, if any.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use regorus::utils::limits::fallback_execution_timer_config;
|
||||
///
|
||||
/// // By default no fallback execution timer is configured.
|
||||
/// assert!(fallback_execution_timer_config().is_none());
|
||||
/// ```
|
||||
pub fn fallback_execution_timer_config() -> Option<ExecutionTimerConfig> {
|
||||
let guard = FALLBACK_EXECUTION_TIMER_CONFIG.lock();
|
||||
guard.as_ref().copied()
|
||||
}
|
||||
|
||||
impl ExecutionTimer {
|
||||
/// Construct a new timer with the provided configuration.
|
||||
pub const fn new(config: Option<ExecutionTimerConfig>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
start: None,
|
||||
accumulated_units: 0,
|
||||
last_elapsed: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the timer state to its initial configuration without recording a start instant.
|
||||
pub const fn reset(&mut self) {
|
||||
self.start = None;
|
||||
self.accumulated_units = 0;
|
||||
self.last_elapsed = Duration::ZERO;
|
||||
}
|
||||
|
||||
/// Reset any prior state and record the start instant.
|
||||
pub const fn start(&mut self, now: Duration) {
|
||||
self.start = Some(now);
|
||||
self.accumulated_units = 0;
|
||||
self.last_elapsed = Duration::ZERO;
|
||||
}
|
||||
|
||||
/// Returns the timer configuration.
|
||||
pub const fn config(&self) -> Option<ExecutionTimerConfig> {
|
||||
self.config
|
||||
}
|
||||
|
||||
/// Returns the configured limit.
|
||||
pub const fn limit(&self) -> Option<Duration> {
|
||||
match self.config {
|
||||
Some(config) => Some(config.limit),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the last elapsed duration recorded by a check.
|
||||
pub const fn last_elapsed(&self) -> Duration {
|
||||
self.last_elapsed
|
||||
}
|
||||
|
||||
/// Increment work units and run the periodic limit check when necessary.
|
||||
pub fn tick(&mut self, work_units: u32, now: Duration) -> Result<(), LimitError> {
|
||||
let Some(config) = self.config else {
|
||||
return Ok(());
|
||||
};
|
||||
self.accumulated_units = self.accumulated_units.saturating_add(work_units);
|
||||
if self.accumulated_units < config.check_interval.get() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Preserve the remainder so that callers do not lose fractional work.
|
||||
let interval = config.check_interval.get();
|
||||
self.accumulated_units %= interval;
|
||||
self.check_now(now)
|
||||
}
|
||||
|
||||
/// Force an immediate check against the configured deadline.
|
||||
pub fn check_now(&mut self, now: Duration) -> Result<(), LimitError> {
|
||||
let Some(config) = self.config else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(start) = self.start else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let elapsed = now.checked_sub(start).unwrap_or(Duration::ZERO);
|
||||
self.last_elapsed = elapsed;
|
||||
if elapsed > config.limit {
|
||||
return Err(LimitError::TimeLimitExceeded {
|
||||
elapsed,
|
||||
limit: config.limit,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute elapsed time relative to the recorded start, if available.
|
||||
pub fn elapsed(&self, now: Duration) -> Option<Duration> {
|
||||
let start = self.start?;
|
||||
Some(now.checked_sub(start).unwrap_or(Duration::ZERO))
|
||||
}
|
||||
|
||||
/// Realign the timer start so that a previously consumed `elapsed` duration is preserved while
|
||||
/// ignoring any wall-clock time that passed during a suspension window.
|
||||
pub const fn resume_from_elapsed(&mut self, now: Duration, elapsed: Duration) {
|
||||
if self.config.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.start = Some(now.saturating_sub(elapsed));
|
||||
self.last_elapsed = elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::num::NonZeroU32;
|
||||
use core::sync::atomic::{AtomicU64, Ordering};
|
||||
use core::time::Duration;
|
||||
|
||||
fn nz(value: u32) -> NonZeroU32 {
|
||||
NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_defers_checks_until_interval_is_reached() {
|
||||
let mut timer = ExecutionTimer::new(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(100),
|
||||
check_interval: nz(4),
|
||||
}));
|
||||
|
||||
timer.start(Duration::from_millis(0));
|
||||
|
||||
for step in 1..4 {
|
||||
let now = Duration::from_millis((step * 10) as u64);
|
||||
let result = timer.tick(1, now);
|
||||
assert_eq!(result, Ok(()), "tick before reaching interval must succeed");
|
||||
assert_eq!(timer.last_elapsed(), Duration::ZERO);
|
||||
}
|
||||
|
||||
let result = timer.tick(1, Duration::from_millis(40));
|
||||
assert_eq!(result, Ok(()), "tick at interval boundary must succeed");
|
||||
assert_eq!(timer.last_elapsed(), Duration::from_millis(40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_now_reports_limit_exceeded() {
|
||||
let mut timer = ExecutionTimer::new(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(25),
|
||||
check_interval: nz(1),
|
||||
}));
|
||||
|
||||
timer.start(Duration::from_millis(0));
|
||||
assert_eq!(
|
||||
timer.tick(1, Duration::from_millis(10)),
|
||||
Ok(()),
|
||||
"tick before limit breach must succeed"
|
||||
);
|
||||
|
||||
let result = timer.check_now(Duration::from_millis(30));
|
||||
assert!(matches!(&result, Err(LimitError::TimeLimitExceeded { .. })));
|
||||
|
||||
if let Err(LimitError::TimeLimitExceeded { elapsed, limit }) = result {
|
||||
assert!(elapsed > limit);
|
||||
assert_eq!(limit, Duration::from_millis(25));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_reports_limit_exceeded() {
|
||||
let mut timer = ExecutionTimer::new(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(30),
|
||||
check_interval: nz(2),
|
||||
}));
|
||||
|
||||
timer.start(Duration::from_millis(0));
|
||||
assert_eq!(
|
||||
timer.tick(1, Duration::from_millis(10)),
|
||||
Ok(()),
|
||||
"initial tick must succeed"
|
||||
);
|
||||
|
||||
let result = timer.tick(1, Duration::from_millis(35));
|
||||
assert!(matches!(&result, Err(LimitError::TimeLimitExceeded { .. })));
|
||||
|
||||
if let Err(LimitError::TimeLimitExceeded { elapsed, limit }) = result {
|
||||
assert!(elapsed > limit);
|
||||
assert_eq!(limit, Duration::from_millis(30));
|
||||
assert_eq!(timer.last_elapsed(), elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_before_start_is_noop() {
|
||||
let mut timer = ExecutionTimer::new(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_secs(1),
|
||||
check_interval: nz(1),
|
||||
}));
|
||||
|
||||
let result = timer.tick(1, Duration::from_millis(100));
|
||||
assert_eq!(result, Ok(()), "tick before start should be ignored");
|
||||
assert_eq!(timer.last_elapsed(), Duration::ZERO);
|
||||
assert!(timer.elapsed(Duration::from_millis(200)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_now_allows_elapsed_equal_to_limit() {
|
||||
let mut timer = ExecutionTimer::new(Some(ExecutionTimerConfig {
|
||||
limit: Duration::from_millis(50),
|
||||
check_interval: nz(1),
|
||||
}));
|
||||
|
||||
timer.start(Duration::from_millis(0));
|
||||
assert_eq!(
|
||||
timer.tick(1, Duration::from_millis(30)),
|
||||
Ok(()),
|
||||
"tick prior to equality check must succeed"
|
||||
);
|
||||
let result = timer.check_now(Duration::from_millis(50));
|
||||
assert_eq!(result, Ok(()), "elapsed equal to limit must not fail");
|
||||
assert_eq!(timer.last_elapsed(), Duration::from_millis(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_is_noop_when_limit_disabled() {
|
||||
let mut timer = ExecutionTimer::new(None);
|
||||
|
||||
timer.start(Duration::from_millis(0));
|
||||
|
||||
for step in 0..8 {
|
||||
let now = Duration::from_millis((step + 1) as u64);
|
||||
assert_eq!(
|
||||
timer.tick(1, now),
|
||||
Ok(()),
|
||||
"ticks with disabled limit must succeed"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(timer.last_elapsed(), Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_now_is_noop_before_start() {
|
||||
let mut timer = ExecutionTimer::new(None);
|
||||
let result = timer.check_now(Duration::from_secs(1));
|
||||
assert_eq!(result, Ok(()), "check before start must be ignored");
|
||||
assert!(timer.elapsed(Duration::from_secs(2)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elapsed_reports_offset_from_start() {
|
||||
let mut timer = ExecutionTimer::new(None);
|
||||
timer.start(Duration::from_millis(5));
|
||||
let elapsed = timer.elapsed(Duration::from_millis(20));
|
||||
assert_eq!(elapsed, Some(Duration::from_millis(15)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monotonic_now_uses_override_when_present() {
|
||||
static TEST_TIME: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestSource;
|
||||
|
||||
impl TimeSource for TestSource {
|
||||
fn now(&self) -> Option<Duration> {
|
||||
Some(Duration::from_nanos(TEST_TIME.load(Ordering::Relaxed)))
|
||||
}
|
||||
}
|
||||
|
||||
static SOURCE: TestSource = TestSource;
|
||||
|
||||
let _suite_guard = super::acquire_limits_test_lock();
|
||||
|
||||
let mut slot = super::TIME_SOURCE_OVERRIDE.lock();
|
||||
let previous = (*slot).replace(&SOURCE);
|
||||
drop(slot);
|
||||
|
||||
TEST_TIME.store(123_000_000, Ordering::Relaxed);
|
||||
assert_eq!(monotonic_now(), Some(Duration::from_nanos(123_000_000)));
|
||||
|
||||
let mut slot = super::TIME_SOURCE_OVERRIDE.lock();
|
||||
*slot = previous;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user