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
@@ -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(),
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user