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
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,6 +28,7 @@ bindings/*/target
|
||||
# C# build folders
|
||||
**bin
|
||||
**obj
|
||||
bindings/csharp/.nuget/
|
||||
|
||||
# Bundler binstubs regenerated during ruby setup
|
||||
bindings/ruby/bin/
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1269,6 +1269,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"test-generator",
|
||||
"thiserror",
|
||||
"url",
|
||||
|
||||
@@ -105,6 +105,7 @@ thiserror = { version = "2.0", default-features = false }
|
||||
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
|
||||
num-bigint = { version = "0.4", default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
spin = { version = "0.9.8", default-features = false, features = ["mutex", "spin_mutex"] }
|
||||
|
||||
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true }
|
||||
regex = {version = "1.11.1", optional = true, default-features = false }
|
||||
|
||||
@@ -29,8 +29,11 @@ Once the workflow run completes, the generated Nuget can be downloaded by follow
|
||||
|
||||
## Local
|
||||
|
||||
<<<<<<< HEAD
|
||||
TODO
|
||||
The `cargo xtask` runner provides helpers for local builds:
|
||||
|
||||
1. `cargo xtask ffi` builds the `bindings/ffi` crate for the host platform in debug mode. Add `--target <triple>` (repeatable) to cross-compile, or `--release` to produce optimised artefacts. Results land under `bindings/ffi/target/<triple>/<profile>`.
|
||||
2. `cargo xtask nuget` reuses those artefacts to pack the C# library. It defaults to debug builds for the host but accepts `--target`, `--release`, `--artifacts-dir <path>` to reuse existing binaries, and `--enforce-artifacts` to require every officially supported platform.
|
||||
3. `cargo xtask test-csharp` ensures a NuGet is available (rebuilding when required or when `--force-nuget` is passed) and then runs `Regorus.Tests`, `TestApp`, and `TargetExampleApp` against it. The command accepts the same build flags as `cargo xtask nuget`.
|
||||
|
||||
## Memory Usage Safeguards
|
||||
|
||||
@@ -48,11 +51,11 @@ using var engine = new Regorus.Engine();
|
||||
var veryLargeJson = new string('x', 128 * 1024);
|
||||
try
|
||||
{
|
||||
engine.SetInputJson(veryLargeJson);
|
||||
engine.SetInputJson(veryLargeJson);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.WriteLine($"Allocator reported: {ex.Message}");
|
||||
Console.WriteLine($"Allocator reported: {ex.Message}");
|
||||
}
|
||||
|
||||
// Restore defaults once done
|
||||
@@ -60,11 +63,4 @@ Regorus.MemoryLimits.SetGlobalMemoryLimit(null);
|
||||
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null);
|
||||
```
|
||||
|
||||
See `bindings/csharp/Regorus.Tests/RegorusTests.cs` for scenario coverage and `bindings/csharp/TargetExampleApp/Program.cs` for end-to-end usage.
|
||||
=======
|
||||
The `cargo xtask` runner provides helpers for local builds:
|
||||
|
||||
1. `cargo xtask ffi` builds the `bindings/ffi` crate for the host platform in debug mode. Add `--target <triple>` (repeatable) to cross-compile, or `--release` to produce optimised artefacts. Results land under `bindings/ffi/target/<triple>/<profile>`.
|
||||
2. `cargo xtask nuget` reuses those artefacts to pack the C# library. It defaults to debug builds for the host but accepts `--target`, `--release`, `--artifacts-dir <path>` to reuse existing binaries, and `--enforce-artifacts` to require every officially supported platform.
|
||||
3. `cargo xtask test-csharp` ensures a NuGet is available (rebuilding when required or when `--force-nuget` is passed) and then runs `Regorus.Tests`, `TestApp`, and `TargetExampleApp` against it. The command accepts the same build flags as `cargo xtask nuget`.
|
||||
>>>>>>> 380a27c (feat(xtask): consolidate CI workflows onto xtask helpers)
|
||||
See bindings/csharp/Regorus.Tests/RegorusTests.cs for scenario coverage and bindings/csharp/TargetExampleApp/Program.cs for end-to-end usage.
|
||||
|
||||
131
bindings/csharp/Regorus.Tests/ExecutionTimerTests.cs
Normal file
131
bindings/csharp/Regorus.Tests/ExecutionTimerTests.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Regorus;
|
||||
|
||||
namespace Regorus.Tests;
|
||||
|
||||
[DoNotParallelize] // Uses global fallback config; must run sequentially.
|
||||
[TestClass]
|
||||
public class ExecutionTimerTests
|
||||
{
|
||||
private const string Policy = @"
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
triplet_count := count([1 |
|
||||
x := data.values[_]
|
||||
y := data.values[_]
|
||||
z := data.values[_]
|
||||
])
|
||||
";
|
||||
|
||||
private const string Query = "data.limits.timer.triplet_count";
|
||||
private const int ValueCount = 160;
|
||||
|
||||
[TestMethod]
|
||||
public void Engine_limit_enforced()
|
||||
{
|
||||
Engine.ClearFallbackExecutionTimerConfig();
|
||||
using var engine = CreateEngine(ValueCount);
|
||||
var config = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
|
||||
engine.SetExecutionTimerConfig(config);
|
||||
|
||||
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
|
||||
StringAssert.Contains(ex.Message, "execution exceeded time limit");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Fallback_applies_to_new_engines()
|
||||
{
|
||||
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
|
||||
Engine.SetFallbackExecutionTimerConfig(fallback);
|
||||
try
|
||||
{
|
||||
using var engine = CreateEngine(ValueCount);
|
||||
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
|
||||
StringAssert.Contains(ex.Message, "execution exceeded time limit");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Engine.ClearFallbackExecutionTimerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Engine_override_relaxes_fallback()
|
||||
{
|
||||
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
|
||||
Engine.SetFallbackExecutionTimerConfig(fallback);
|
||||
try
|
||||
{
|
||||
using var engine = CreateEngine(ValueCount);
|
||||
var relaxed = new ExecutionTimerConfig(TimeSpan.FromSeconds(12), checkInterval: 1);
|
||||
engine.SetExecutionTimerConfig(relaxed);
|
||||
|
||||
var resultJson = engine.EvalRule(Query);
|
||||
var result = JsonSerializer.Deserialize<int>(resultJson!);
|
||||
Assert.IsTrue(result > 0, "Expected a positive triplet count when limit is relaxed.");
|
||||
|
||||
engine.ClearExecutionTimerConfig();
|
||||
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
|
||||
StringAssert.Contains(ex.Message, "execution exceeded time limit");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Engine.ClearFallbackExecutionTimerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CompiledPolicy_limit_enforced()
|
||||
{
|
||||
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
|
||||
Engine.SetFallbackExecutionTimerConfig(fallback);
|
||||
try
|
||||
{
|
||||
using var policy = CreateCompiledPolicy(ValueCount);
|
||||
var ex = Assert.ThrowsException<InvalidOperationException>(() => policy.EvalWithInput("null"));
|
||||
StringAssert.Contains(ex.Message, "execution exceeded time limit");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Engine.ClearFallbackExecutionTimerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CompiledPolicy_uses_engine_limits_only()
|
||||
{
|
||||
// Compiled policies no longer store per-policy execution timers; limits are managed by Engine.
|
||||
Engine.ClearFallbackExecutionTimerConfig();
|
||||
using var policy = CreateCompiledPolicy(ValueCount);
|
||||
var resultJson = policy.EvalWithInput("null");
|
||||
var result = JsonSerializer.Deserialize<int>(resultJson!);
|
||||
Assert.IsTrue(result > 0, "CompiledPolicy should evaluate using engine defaults without its own timer");
|
||||
}
|
||||
|
||||
private static Engine CreateEngine(int valueCount)
|
||||
{
|
||||
var engine = new Engine();
|
||||
engine.AddPolicy("limits_timer.rego", Policy);
|
||||
engine.AddDataJson(CreateData(valueCount));
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static CompiledPolicy CreateCompiledPolicy(int valueCount)
|
||||
{
|
||||
var modules = new[] { new PolicyModule("limits_timer.rego", Policy) };
|
||||
return Compiler.CompilePolicyWithEntrypoint(CreateData(valueCount), modules, Query);
|
||||
}
|
||||
|
||||
private static string CreateData(int valueCount)
|
||||
{
|
||||
var payload = new { values = Enumerable.Range(0, valueCount).ToArray() };
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Regorus.Tests;
|
||||
[TestClass]
|
||||
public class RegorusTests
|
||||
{
|
||||
private static readonly object LimitLock = new();
|
||||
private static readonly object LimitLock = new();
|
||||
|
||||
[TestMethod]
|
||||
public void Basic_evaluation_succeeds()
|
||||
@@ -374,17 +374,17 @@ stretched := concat("", [input.block | numbers.range(0, input.repeat - 1)[_]])
|
||||
|
||||
private sealed class MemoryLimitScope : IDisposable
|
||||
{
|
||||
private readonly ulong? _originalLimit;
|
||||
private readonly ulong? _originalLimit;
|
||||
|
||||
public MemoryLimitScope()
|
||||
{
|
||||
_originalLimit = MemoryLimits.GetGlobalMemoryLimit();
|
||||
}
|
||||
public MemoryLimitScope()
|
||||
{
|
||||
_originalLimit = MemoryLimits.GetGlobalMemoryLimit();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MemoryLimits.SetGlobalMemoryLimit(_originalLimit);
|
||||
MemoryLimits.FlushThreadMemoryCounters();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
MemoryLimits.SetGlobalMemoryLimit(_originalLimit);
|
||||
MemoryLimits.FlushThreadMemoryCounters();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,5 +203,14 @@ namespace Regorus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UseHandle(Action<IntPtr> action)
|
||||
{
|
||||
UseHandle<object?>(handlePtr =>
|
||||
{
|
||||
action(handlePtr);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Regorus.Internal;
|
||||
|
||||
|
||||
@@ -24,6 +26,17 @@ namespace Regorus
|
||||
_handle = RegorusEngineHandle.Create();
|
||||
}
|
||||
|
||||
public static void SetFallbackExecutionTimerConfig(ExecutionTimerConfig config)
|
||||
{
|
||||
var nativeConfig = config.ToNative();
|
||||
CheckAndDropResult(Regorus.Internal.API.regorus_set_fallback_execution_timer_config(nativeConfig));
|
||||
}
|
||||
|
||||
public static void ClearFallbackExecutionTimerConfig()
|
||||
{
|
||||
CheckAndDropResult(Regorus.Internal.API.regorus_clear_fallback_execution_timer_config());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
@@ -87,6 +100,32 @@ namespace Regorus
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SetExecutionTimerConfig(ExecutionTimerConfig config)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var nativeConfig = config.ToNative();
|
||||
UseHandle(enginePtr =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var localConfig = nativeConfig;
|
||||
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ClearExecutionTimerConfig()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
UseHandle(enginePtr =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr));
|
||||
}
|
||||
});
|
||||
}
|
||||
public string? AddPolicy(string path, string rego)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -355,7 +394,21 @@ namespace Regorus
|
||||
});
|
||||
}
|
||||
|
||||
string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
|
||||
private static string? StringFromUtf8(IntPtr ptr)
|
||||
{
|
||||
|
||||
#if NETSTANDARD2_1
|
||||
return Marshal.PtrToStringUTF8(ptr);
|
||||
#else
|
||||
int len = 0;
|
||||
while (Marshal.ReadByte(ptr, len) != 0) { ++len; }
|
||||
byte[] buffer = new byte[len];
|
||||
Marshal.Copy(ptr, buffer, 0, buffer.Length);
|
||||
return Encoding.UTF8.GetString(buffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
62
bindings/csharp/Regorus/ExecutionTimerConfig.cs
Normal file
62
bindings/csharp/Regorus/ExecutionTimerConfig.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Managed representation of the execution timer configuration used by the engine.
|
||||
/// </summary>
|
||||
public readonly struct ExecutionTimerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExecutionTimerConfig"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="limit">Maximum wall-clock duration allowed for evaluation. Must be non-negative.</param>
|
||||
/// <param name="checkInterval">Number of work units between timer checks. Must be non-zero.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="limit"/> is negative or <paramref name="checkInterval"/> is zero.</exception>
|
||||
public ExecutionTimerConfig(TimeSpan limit, uint checkInterval)
|
||||
{
|
||||
if (limit < TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(limit), "Execution timer limit must be non-negative.");
|
||||
}
|
||||
|
||||
if (checkInterval == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(checkInterval), "Execution timer check interval must be non-zero.");
|
||||
}
|
||||
|
||||
Limit = limit;
|
||||
CheckInterval = checkInterval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximum wall-clock duration allowed for an evaluation.
|
||||
/// </summary>
|
||||
public TimeSpan Limit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of work units between timer checks.
|
||||
/// </summary>
|
||||
public uint CheckInterval { get; }
|
||||
|
||||
internal Regorus.Internal.RegorusExecutionTimerConfig ToNative()
|
||||
{
|
||||
if (Limit < TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Execution timer limit must be non-negative.");
|
||||
}
|
||||
|
||||
ulong ticks = checked((ulong)Limit.Ticks);
|
||||
ulong limitNanoseconds = checked(ticks * 100UL);
|
||||
|
||||
return new Regorus.Internal.RegorusExecutionTimerConfig
|
||||
{
|
||||
limit_ns = limitNanoseconds,
|
||||
check_interval = CheckInterval,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ namespace Regorus.Internal
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_engine_compile_with_entrypoint(RegorusEngine* engine, byte* rule);
|
||||
|
||||
#if REGORUS_FFI_TEST_HOOKS
|
||||
#if REGORUS_FFI_TEST_HOOKS
|
||||
/// <summary>
|
||||
/// Trigger a panic inside the engine for testing purposes.
|
||||
/// </summary>
|
||||
@@ -269,7 +269,35 @@ namespace Regorus.Internal
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_reset_poison", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern void regorus_engine_test_reset_poison();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Configure the execution timer for a specific engine instance.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_engine_set_execution_timer_config(RegorusEngine* engine, RegorusExecutionTimerConfig* config);
|
||||
|
||||
/// <summary>
|
||||
/// Clear the execution timer configuration for a specific engine instance.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_engine_clear_execution_timer_config(RegorusEngine* engine);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Execution Timer Global Methods
|
||||
|
||||
/// <summary>
|
||||
/// Set the process-wide fallback execution timer configuration.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_set_fallback_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_set_fallback_execution_timer_config(RegorusExecutionTimerConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// Clear the process-wide fallback execution timer configuration.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_clear_fallback_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_clear_fallback_execution_timer_config();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -579,6 +607,16 @@ namespace Regorus.Internal
|
||||
public byte* error_message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FFI representation of the execution timer configuration.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct RegorusExecutionTimerConfig
|
||||
{
|
||||
public ulong limit_ns;
|
||||
public uint check_interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for regorus::Engine.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TargetExampleApp;
|
||||
@@ -32,14 +33,14 @@ allow if {
|
||||
|
||||
# Allow network security groups with proper inbound rules
|
||||
allow if {
|
||||
input.type == ""Microsoft.Network/networkSecurityGroups""
|
||||
count([rule |
|
||||
rule := input.properties.securityRules[_]
|
||||
rule.properties.direction == ""Inbound""
|
||||
rule.properties.access == ""Allow""
|
||||
rule.properties.sourceAddressPrefix == ""*""
|
||||
rule.properties.destinationPortRange in [parameters.allowedPorts]
|
||||
]) == 0
|
||||
input.type == ""Microsoft.Network/networkSecurityGroups""
|
||||
count([rule |
|
||||
rule := input.properties.securityRules[_]
|
||||
rule.properties.direction == ""Inbound""
|
||||
rule.properties.access == ""Allow""
|
||||
rule.properties.sourceAddressPrefix == ""*""
|
||||
rule.properties.destinationPortRange in [parameters.allowedPorts]
|
||||
]) == 0
|
||||
}";
|
||||
|
||||
private const string AZURE_STORAGE_POLICY_ASSIGNMENT = @"
|
||||
@@ -50,44 +51,58 @@ import rego.v1
|
||||
parameters.requiredTLSVersion = ""TLS1_2""
|
||||
parameters.allowedPorts = [""22"", ""3389""]";
|
||||
|
||||
private const string EXECUTION_TIMER_POLICY = @"
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
triplet_count := count([1 |
|
||||
x := data.values[_]
|
||||
y := data.values[_]
|
||||
z := data.values[_]
|
||||
])
|
||||
";
|
||||
|
||||
private const string EXECUTION_TIMER_QUERY = "data.limits.timer.triplet_count";
|
||||
private const int EXECUTION_TIMER_VALUE_COUNT = 40;
|
||||
|
||||
// Test data constants
|
||||
private const string COMPLIANT_STORAGE_ACCOUNT = @"{
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""name"": ""compliantstorageacct"",
|
||||
""location"": ""eastus"",
|
||||
""kind"": ""StorageV2"",
|
||||
""properties"": {
|
||||
""supportsHttpsTrafficOnly"": true,
|
||||
""minimumTlsVersion"": ""TLS1_2"",
|
||||
""allowBlobPublicAccess"": false,
|
||||
""encryption"": {
|
||||
""services"": {
|
||||
""blob"": { ""enabled"": true },
|
||||
""file"": { ""enabled"": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
""tags"": {
|
||||
""environment"": ""production""
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""name"": ""compliantstorageacct"",
|
||||
""location"": ""eastus"",
|
||||
""kind"": ""StorageV2"",
|
||||
""properties"": {
|
||||
""supportsHttpsTrafficOnly"": true,
|
||||
""minimumTlsVersion"": ""TLS1_2"",
|
||||
""allowBlobPublicAccess"": false,
|
||||
""encryption"": {
|
||||
""services"": {
|
||||
""blob"": { ""enabled"": true },
|
||||
""file"": { ""enabled"": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
""tags"": {
|
||||
""environment"": ""production""
|
||||
}
|
||||
}";
|
||||
|
||||
private const string NON_COMPLIANT_STORAGE_ACCOUNT = @"{
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""name"": ""insecurestorageacct"",
|
||||
""location"": ""westus"",
|
||||
""kind"": ""Storage"",
|
||||
""properties"": {
|
||||
""supportsHttpsTrafficOnly"": false,
|
||||
""minimumTlsVersion"": ""TLS1_0"",
|
||||
""allowBlobPublicAccess"": true,
|
||||
""encryption"": {
|
||||
""services"": {
|
||||
""blob"": { ""enabled"": false },
|
||||
""file"": { ""enabled"": false }
|
||||
}
|
||||
}
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""name"": ""insecurestorageacct"",
|
||||
""location"": ""westus"",
|
||||
""kind"": ""Storage"",
|
||||
""properties"": {
|
||||
""supportsHttpsTrafficOnly"": false,
|
||||
""minimumTlsVersion"": ""TLS1_0"",
|
||||
""allowBlobPublicAccess"": true,
|
||||
""encryption"": {
|
||||
""services"": {
|
||||
""blob"": { ""enabled"": false },
|
||||
""file"": { ""enabled"": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
static void Main(string[] args)
|
||||
@@ -96,7 +111,6 @@ parameters.allowedPorts = [""22"", ""3389""]";
|
||||
|
||||
try
|
||||
{
|
||||
DemonstrateMemoryLimitHelpers();
|
||||
DemonstrateTargetFunctionality();
|
||||
Console.WriteLine("\n=== Target demonstration completed successfully! ===");
|
||||
}
|
||||
@@ -157,6 +171,9 @@ parameters.allowedPorts = [""22"", ""3389""]";
|
||||
// 4. Demonstrate thread-safe concurrent evaluation
|
||||
Console.WriteLine("\n4. Testing concurrent evaluation from multiple threads:");
|
||||
DemonstrateConcurrentEvaluation(compiledPolicy);
|
||||
|
||||
Console.WriteLine("\n5. Execution timer configuration:");
|
||||
DemonstrateExecutionTimer();
|
||||
}
|
||||
|
||||
static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy)
|
||||
@@ -214,45 +231,6 @@ parameters.allowedPorts = [""22"", ""3389""]";
|
||||
Console.WriteLine("✓ No locks required - CompiledPolicy is thread-safe!");
|
||||
}
|
||||
|
||||
static void DemonstrateMemoryLimitHelpers()
|
||||
{
|
||||
Console.WriteLine("REGORUS MEMORY LIMIT UTILITIES");
|
||||
Console.WriteLine("==============================");
|
||||
|
||||
var originalLimit = Regorus.MemoryLimits.GetGlobalMemoryLimit();
|
||||
var originalThreshold = Regorus.MemoryLimits.GetThreadMemoryFlushThreshold();
|
||||
|
||||
try
|
||||
{
|
||||
const ulong demoLimit = 64 * 1024 * 1024;
|
||||
Regorus.MemoryLimits.SetGlobalMemoryLimit(demoLimit);
|
||||
Console.WriteLine($"✓ Global memory limit set to {Regorus.MemoryLimits.GetGlobalMemoryLimit():N0} bytes");
|
||||
|
||||
const ulong flushThreshold = 256 * 1024;
|
||||
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(flushThreshold);
|
||||
Console.WriteLine($"✓ Thread flush threshold configured for {Regorus.MemoryLimits.GetThreadMemoryFlushThreshold():N0} bytes");
|
||||
|
||||
Console.WriteLine("Attempting an allocation that exceeds a 1 byte budget...");
|
||||
Regorus.MemoryLimits.SetGlobalMemoryLimit(1);
|
||||
|
||||
using var engine = new Regorus.Engine();
|
||||
var payload = new string('x', 128 * 1024);
|
||||
engine.SetInputJson($"{{\"payload\":\"{payload}\"}}");
|
||||
}
|
||||
catch (InvalidOperationException error)
|
||||
{
|
||||
Console.WriteLine($"✗ Memory limit exceeded: {error.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null);
|
||||
Regorus.MemoryLimits.SetGlobalMemoryLimit(originalLimit);
|
||||
Regorus.MemoryLimits.FlushThreadMemoryCounters();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy)
|
||||
{
|
||||
Console.WriteLine("Getting policy metadata using GetPolicyInfo()...");
|
||||
@@ -328,4 +306,57 @@ parameters.allowedPorts = [""22"", ""3389""]";
|
||||
Console.WriteLine($"✗ Failed to get policy info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
static void DemonstrateExecutionTimer()
|
||||
{
|
||||
var dataJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
values = Enumerable.Range(0, EXECUTION_TIMER_VALUE_COUNT).ToArray()
|
||||
});
|
||||
|
||||
var fallback = new Regorus.ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
|
||||
var relaxed = new Regorus.ExecutionTimerConfig(TimeSpan.FromMilliseconds(1000), checkInterval: 1);
|
||||
|
||||
Console.WriteLine($" Configuring fallback timer (limit={fallback.Limit.TotalMilliseconds:F0} ms, interval={fallback.CheckInterval})...");
|
||||
|
||||
Regorus.Engine.SetFallbackExecutionTimerConfig(fallback);
|
||||
try
|
||||
{
|
||||
using var engine = new Regorus.Engine();
|
||||
engine.AddPolicy("limits_timer.rego", EXECUTION_TIMER_POLICY);
|
||||
engine.AddDataJson(dataJson);
|
||||
|
||||
Console.WriteLine(" Evaluating under fallback limit (expected failure)...");
|
||||
try
|
||||
{
|
||||
engine.EvalRule(EXECUTION_TIMER_QUERY);
|
||||
Console.WriteLine(" ⚠ Evaluation unexpectedly succeeded under fallback limit.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" ✓ Fallback enforced: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine($" Applying per-engine override ({relaxed.Limit.TotalMilliseconds:F0} ms) and retrying...");
|
||||
engine.SetExecutionTimerConfig(relaxed);
|
||||
var result = engine.EvalRule(EXECUTION_TIMER_QUERY);
|
||||
Console.WriteLine($" ✓ Override succeeded; triplet_count = {result}");
|
||||
|
||||
Console.WriteLine(" Clearing engine override to restore fallback...");
|
||||
engine.ClearExecutionTimerConfig();
|
||||
try
|
||||
{
|
||||
engine.EvalRule(EXECUTION_TIMER_QUERY);
|
||||
Console.WriteLine(" ⚠ Evaluation unexpectedly succeeded after clearing override.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" ✓ Fallback restored: {ex.Message}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Regorus.Engine.ClearFallbackExecutionTimerConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
bindings/ffi/Cargo.lock
generated
78
bindings/ffi/Cargo.lock
generated
@@ -153,9 +153,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.28.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff"
|
||||
checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"heck",
|
||||
@@ -401,9 +401,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -973,6 +973,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -1071,11 +1072,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1109,6 +1110,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -1188,44 +1195,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
version = "0.9.11+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -1425,9 +1430,6 @@ name = "winnow"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
|
||||
@@ -43,4 +43,4 @@ contention_checks = ["parking_lot"]
|
||||
custom_allocator = []
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.28.0"
|
||||
cbindgen = "0.29.2"
|
||||
|
||||
@@ -52,6 +52,7 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
|
||||
})
|
||||
}
|
||||
|
||||
/// Configure the execution timer for evaluations of this compiled policy.
|
||||
/// Get information about the compiled policy including metadata about modules,
|
||||
/// target configuration, and resource types.
|
||||
///
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::common::{
|
||||
from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus,
|
||||
};
|
||||
use crate::compiled_policy::RegorusCompiledPolicy;
|
||||
use crate::limits::RegorusExecutionTimerConfig;
|
||||
use crate::lock::{new_handle, read, try_read, try_write, Handle, ReadGuard, WriteGuard};
|
||||
use crate::panic_guard::with_unwind_guard;
|
||||
use alloc::boxed::Box;
|
||||
@@ -450,6 +451,39 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// Configure the execution timer for a specific engine instance.
|
||||
pub extern "C" fn regorus_engine_set_execution_timer_config(
|
||||
engine: *mut RegorusEngine,
|
||||
config: *const RegorusExecutionTimerConfig,
|
||||
) -> RegorusResult {
|
||||
to_regorus_result(|| -> Result<()> {
|
||||
let engine = to_ref(engine)?;
|
||||
let config = unsafe {
|
||||
config
|
||||
.as_ref()
|
||||
.copied()
|
||||
.ok_or_else(|| anyhow!("execution timer config pointer is null"))?
|
||||
};
|
||||
let mut guard = engine.try_write()?;
|
||||
guard.set_execution_timer_config(config.to_execution_timer_config()?);
|
||||
Ok(())
|
||||
}())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
/// Clear the engine-specific execution timer configuration.
|
||||
pub extern "C" fn regorus_engine_clear_execution_timer_config(
|
||||
engine: *mut RegorusEngine,
|
||||
) -> RegorusResult {
|
||||
to_regorus_result(|| -> Result<()> {
|
||||
let engine = to_ref(engine)?;
|
||||
let mut guard = engine.try_write()?;
|
||||
guard.clear_execution_timer_config();
|
||||
Ok(())
|
||||
}())
|
||||
}
|
||||
|
||||
/// Get pretty printed coverage report.
|
||||
///
|
||||
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::common::{RegorusResult, RegorusStatus};
|
||||
use crate::common::{to_regorus_result, RegorusResult, RegorusStatus};
|
||||
use alloc::format;
|
||||
use anyhow::{anyhow, Result};
|
||||
use core::num::NonZeroU32;
|
||||
use core::time::Duration;
|
||||
use regorus::utils::limits::{self, ExecutionTimerConfig};
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
fn some_or_none(flag: bool, value: u64) -> Option<u64> {
|
||||
@@ -131,6 +135,45 @@ fn feature_disabled(function: &str) -> RegorusResult {
|
||||
)
|
||||
}
|
||||
|
||||
/// FFI representation of [`ExecutionTimerConfig`].
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RegorusExecutionTimerConfig {
|
||||
/// Wall-clock limit expressed in nanoseconds.
|
||||
pub limit_ns: u64,
|
||||
/// Number of work units between timer checks (must be non-zero).
|
||||
pub check_interval: u32,
|
||||
}
|
||||
|
||||
impl RegorusExecutionTimerConfig {
|
||||
pub fn to_execution_timer_config(self) -> Result<ExecutionTimerConfig> {
|
||||
let check_interval = NonZeroU32::new(self.check_interval)
|
||||
.ok_or_else(|| anyhow!("execution_timer.check_interval must be non-zero"))?;
|
||||
let limit = Duration::from_nanos(self.limit_ns);
|
||||
|
||||
Ok(ExecutionTimerConfig {
|
||||
limit,
|
||||
check_interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_set_fallback_execution_timer_config(
|
||||
config: RegorusExecutionTimerConfig,
|
||||
) -> RegorusResult {
|
||||
to_regorus_result(|| -> Result<()> {
|
||||
limits::set_fallback_execution_timer_config(Some(config.to_execution_timer_config()?));
|
||||
Ok(())
|
||||
}())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_clear_fallback_execution_timer_config() -> RegorusResult {
|
||||
limits::set_fallback_execution_timer_config(None);
|
||||
RegorusResult::ok_void()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
|
||||
7
bindings/java/Cargo.lock
generated
7
bindings/java/Cargo.lock
generated
@@ -848,6 +848,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -970,6 +971,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
|
||||
7
bindings/python/Cargo.lock
generated
7
bindings/python/Cargo.lock
generated
@@ -907,6 +907,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -1021,6 +1022,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
|
||||
11
bindings/wasm/Cargo.lock
generated
11
bindings/wasm/Cargo.lock
generated
@@ -510,9 +510,9 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -890,6 +890,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -1021,6 +1022,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
|
||||
203
docs/limits/execution_time.md
Normal file
203
docs/limits/execution_time.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Execution Time Limiting Design
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce configurable execution-time limits so every evaluation stays bounded.
|
||||
Policy service can configure these limits to ensure that a single runaway policy never degrades overall availability.
|
||||
|
||||
## Design Goals
|
||||
|
||||
- Support execution limits per engine. Also support configuring a global fallback execution limit.
|
||||
- Use best time sources automatically in std builds but provide ability to set the time source in no-std builds and for deterministic testing.
|
||||
- Since computing elapsed time is relatively expensive, provide ability to control how often it is computed during evaluation.
|
||||
|
||||
## Prevalent Approaches
|
||||
|
||||
The following approaches are used by various execution engines.
|
||||
|
||||
### Cooperative Tick Budgets
|
||||
Evaluations burn through a configurable “fuel” counter one work unit at a time.
|
||||
When fuel hits zero the engine raises an error, forcing callers to top up or abandon the run.
|
||||
- **Examples**: Wasmtime fuel, Wasmer metering, Lua debug hooks
|
||||
- **Pros**: Precise control over work units; compatible with no_std targets
|
||||
- **Cons**: Requires instrumentation at every checkpoint; budget choice affects responsiveness
|
||||
|
||||
### Wall-Clock Watchdogs
|
||||
Hosts schedule a real-time deadline alongside the policy evaluation.
|
||||
When a timer fires, the engine aborts or interrupts, guaranteeing a hard wall-clock cap.
|
||||
- **Examples**: V8 termination handler, SpiderMonkey interrupt callback, PostgreSQL statement_timeout
|
||||
- **Pros**: Tracks real elapsed time; easy to reason about deadlines
|
||||
- **Cons**: Needs host timers or threads; harder to support in no_std environments, and Rust threads cannot be force-cancelled so watchdogs must coordinate cooperative shutdown
|
||||
|
||||
### Scheduler Time-Slicing
|
||||
Policies run inside a cooperative scheduler that yields after fixed work slices.
|
||||
The host can deprioritize or cancel long tasks while allowing others to continue.
|
||||
- **Examples**: Erlang BEAM reductions, .NET ThreadPool throttling
|
||||
- **Pros**: Isolates runaway workloads by design; integrates with host schedulers
|
||||
- **Cons**: Significant state bookkeeping; higher overhead for frequent yields, and feasible for VM control loops but practically impossible for the interpreter without a deep rewrite
|
||||
|
||||
|
||||
## Regorus Approach
|
||||
|
||||
Regorus adopts the cooperative tick-budget model. In std builds, the best time source is used without any special configuration. In no_std builds (there is no standard time source) and test builds (which need to control the time source to avoid flakiness), the time source can be configured via a trait instance.
|
||||
|
||||
Instead of checking elapsed time at each
|
||||
Evaluations accrue work units via `tick`, only reading the clock once the configured `check_interval` elapses, which balances responsiveness with low overhead when limits stay off.
|
||||
- **Closest Prevalent Approach**: Cooperative tick budgets
|
||||
- **Rationale**: Runs on std and no_std targets by swapping time sources, keeps tests deterministic, avoids watchdog threads or complex schedulers, and applies uniformly across evaluation modes without deep architectural changes.
|
||||
|
||||
|
||||
|
||||
## Configuration Model
|
||||
|
||||
### ExecutionTimerConfig
|
||||
|
||||
`ExecutionTimerConfig` lives in `utils::limits::time` and contains:
|
||||
|
||||
- `limit: Duration` — enforced wall-clock budget.
|
||||
- `check_interval: NonZeroU32` — number of work units between timer checks.
|
||||
|
||||
### Work Units
|
||||
|
||||
The timer does not prescribe what constitutes a single “work unit.” Instead, each caller chooses a
|
||||
granularity that matches its execution model:
|
||||
|
||||
- The interpreter treats each scheduling step (e.g., evaluating a single statement or expression in
|
||||
a rule) as a unit.
|
||||
- The VM typically reports one instruction per unit.
|
||||
|
||||
This abstraction keeps the timer flexible while still guaranteeing that, regardless of the unit
|
||||
definition, the timer observes elapsed wall-clock time at predictable checkpoints controlled by
|
||||
`check_interval`.
|
||||
|
||||
Interpreters treat the absence of an `ExecutionTimerConfig` as "no limit". When a configuration is present, callers can increase `check_interval` to amortize the cost of frequent checks.
|
||||
|
||||
### Global Fallback vs Engine Overrides
|
||||
|
||||
- `set_fallback_execution_timer_config` installs a process-wide fallback stored behind a spin mutex. Engines without an explicit override consult this value before every evaluation.
|
||||
- Each engine holds an optional `execution_timer_config`. When `Engine::set_execution_timer_config` is invoked, the engine stores the provided configuration and applies it to the interpreter immediately. Clearing the override via `Engine::clear_execution_timer_config` restores reliance on the global fallback.
|
||||
- Engines default to no time limit. Newly created engines apply the effective configuration (engine override or global fallback or default) during construction so that any first evaluation honors the expected budget.
|
||||
|
||||
### Effective Configuration Lifecycle
|
||||
|
||||
Before any evaluation entry point (query, rule, compilation), the engine:
|
||||
|
||||
1. Computes the effective configuration via `execution_timer_config.or_else(fallback_execution_timer_config)`.
|
||||
2. Applies it to the interpreter using `apply_effective_execution_timer_config`, which resets the timer to ensure a fresh window.
|
||||
3. Proceeds with evaluation, relying on interpreter checkpoints to enforce the deadline.
|
||||
|
||||
This approach guarantees that changing the global fallback impacts both new and existing engines on their next evaluation, while engine overrides remain isolated.
|
||||
|
||||
## ExecutionTimer Behavior
|
||||
|
||||
`ExecutionTimer` maintains four fields:
|
||||
|
||||
- `config`: the active `ExecutionTimerConfig`, if any.
|
||||
- `start`: optional start instant.
|
||||
- `accumulated_units`: tracks work units until the next check.
|
||||
- `last_elapsed`: caches the most recent elapsed duration.
|
||||
|
||||
### Key Operations
|
||||
|
||||
- `start(now)` records the baseline instant and clears accumulated counters.
|
||||
- `tick(work_units, now)` increments the accumulator and triggers `check_now` when the accumulator reaches `check_interval`. If no configuration is installed, the function returns early with `Ok(())`.
|
||||
- `check_now(now)` computes elapsed time, updates `last_elapsed`, and returns `LimitError::TimeLimitExceeded` when elapsed > limit.
|
||||
- `elapsed(now)` reports elapsed time without mutating state, enabling diagnostics and tests.
|
||||
|
||||
Because ticks only perform the expensive comparison after the configured interval, callers can tune `check_interval` to their workloads.
|
||||
|
||||
## Time Sources
|
||||
|
||||
To avoid direct dependencies on `Instant`, the timer expects callers to supply a monotonic `Duration` via `monotonic_now()` or custom sources.
|
||||
|
||||
- On `std` builds, `StdTimeSource` captures a single `Instant` per process (via `OnceLock`) and reports elapsed durations. This keeps time monotonic and stable across threads.
|
||||
- Tests and `no_std` builds can install overrides through `set_time_source`, which stores an `&'static dyn TimeSource` in a spin mutex. YAML tests use this hook to provide deterministic timestamps, ensuring repeatable limit violations.
|
||||
|
||||
If no source is available (e.g., `no_std` without an override), `monotonic_now` returns `None`; the interpreter treats this as “time limiting unavailable,” effectively disabling checks.
|
||||
|
||||
## Interpreter Integration
|
||||
|
||||
The interpreter carries an `ExecutionTimer`. Evaluation steps integrate with the timer as follows:
|
||||
|
||||
1. `prepare_for_eval` applies the effective configuration and calls `reset` on internal state.
|
||||
2. At key checkpoints (rule scheduling, loop iterations, query evaluation steps) the interpreter:
|
||||
- Calls `monotonic_now` to fetch the current time (when available).
|
||||
- Invokes `tick(1, now)` to check for limit violations.
|
||||
3. When `LimitError::TimeLimitExceeded` is returned, the interpreter converts it into an error surface consistent with existing APIs (e.g., `anyhow::Error` on Rust, host-specific exceptions on bindings).
|
||||
|
||||
Because the interpreter amortizes clock reads via `check_interval`, the overhead remains low even with many evaluation steps.
|
||||
|
||||
## Compiled Policy Integration
|
||||
|
||||
Compiled policies (VM paths) share the interpreter’s timer via `apply_effective_execution_timer_config`. Before VM execution begins, the engine ensures the VM’s interpreter state reflects the current timer configuration and resets any per-evaluation state.
|
||||
|
||||
- VM loops call `tick` with the number of instructions executed since the last check (commonly `1`).
|
||||
- Helper functions responsible for longer-running host interactions (e.g., print gathering) may call `check_now` to enforce the deadline before crossing the FFI boundary.
|
||||
|
||||
This shared timer model avoids duplicate configuration state and maintains consistent semantics across evaluation modes.
|
||||
|
||||
## Public API Surface
|
||||
|
||||
The design exposes these primary methods:
|
||||
|
||||
- `Engine::set_execution_timer_config(config: ExecutionTimerConfig)` stores a per-engine override and reapplies it immediately, ensuring the next evaluation enforces the new limits.
|
||||
- `Engine::clear_execution_timer_config()` removes the override and reverts to the global fallback.
|
||||
- `set_fallback_execution_timer_config(config: Option<ExecutionTimerConfig>)` installs an optional global fallback. Passing `None` clears it.
|
||||
- `fallback_execution_timer_config() -> Option<ExecutionTimerConfig>` returns the currently active fallback for diagnostics.
|
||||
|
||||
Documentation highlights that engines default to no time limit, global settings provide a quick way to protect all engines, and overrides preempt the global value until cleared.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit Tests** in `utils::limits::time` verify timer configuration handling, `tick` behavior, limit enforcement, and custom time sources.
|
||||
- **Integration Tests (YAML)** configure deterministic time sources and assert that engine-level and global configurations interact correctly (override precedence, clearing behavior, fresh windows per evaluation).
|
||||
- **Binding Tests** (planned) will demonstrate that FFI surfaces propagate timer errors.
|
||||
|
||||
Each test resets global configuration and time sources via RAII guards to avoid cross-test interference.
|
||||
|
||||
## Operational Guidance
|
||||
|
||||
- Choose conservative `check_interval` values (e.g., 1–10) for latency-sensitive workloads to catch runaway loops quickly. Larger intervals reduce overhead but increase the window between checks.
|
||||
- When applying global limits in multi-tenant services, consider setting per-engine overrides for trusted workloads that need higher budgets.
|
||||
- Combine with monitoring of `last_elapsed` to understand how close evaluations come to their deadlines.
|
||||
|
||||
## Pros and Cons of the Current Design
|
||||
|
||||
### Pros
|
||||
- **Low overhead via amortized checks**: `check_interval` keeps clock reads cheap while bounding elapsed time.
|
||||
- **Works on std + no_std**: `TimeSource` abstraction enables std `Instant` or user-provided clocks.
|
||||
- **Simple API surface**: One config struct, plus global fallback and per-engine override.
|
||||
- **Suspend-aware VM**: suspendable execution snapshots elapsed time and resumes from that value, so suspended time is not counted.
|
||||
|
||||
### Cons
|
||||
- **Best-effort when no time source**: If `monotonic_now()` returns `None`, time limits effectively disable for that run.
|
||||
- **Granularity depends on `check_interval`**: Large intervals can overshoot the limit before the next check fires.
|
||||
- **Cooperative gaps**: Any long-running host work outside evaluation loops is not accounted for.
|
||||
- **Global fallback is process-wide**: Requires coordination in tests or multi-tenant hosts.
|
||||
|
||||
## Binding Surface (C# / FFI)
|
||||
|
||||
Regorus exposes execution timers in bindings through a thin FFI layer:
|
||||
|
||||
- **FFI struct**: `RegorusExecutionTimerConfig` uses `limit_ns` + `check_interval` and validates non-zero intervals.
|
||||
- **Engine methods**: per-engine `SetExecutionTimerConfig` / `ClearExecutionTimerConfig` map to `regorus_engine_set_execution_timer_config` / `regorus_engine_clear_execution_timer_config`.
|
||||
- **Fallback**: static `Engine.SetFallbackExecutionTimerConfig` / `ClearFallbackExecutionTimerConfig` map to `regorus_set_fallback_execution_timer_config` and `regorus_clear_fallback_execution_timer_config`.
|
||||
|
||||
These bindings preserve Rust semantics and propagate the `LimitError` message (“execution exceeded time limit”) up to host exceptions.
|
||||
|
||||
## Comparison: Cancellation Token Approach
|
||||
|
||||
A cancellation token design would add an explicit “stop now” signal that evaluators check at the same checkpoints used for time limiting. This has value for host‑initiated cancellation, but it is **not a replacement** for the current time‑based approach.
|
||||
|
||||
- **Semantics**: Tokens require the host to decide when to cancel; the current design enforces wall‑clock budgets inside Regorus.
|
||||
- **Portability**: Tokens are portable, but time‑based cancellation would require a watchdog or scheduler to flip the token at a deadline.
|
||||
- **Threads**: A watchdog typically implies background threads or an async runtime. We explicitly want to **avoid introducing threads** inside Regorus for simplicity, no_std compatibility, and predictability.
|
||||
- **Performance**: Frequent timer‑thread ticks or per‑evaluation scheduling can add overhead for microsecond‑scale evaluations. The current cooperative checks amortize clock reads and avoid extra threads, keeping steady‑state costs low.
|
||||
|
||||
For these reasons, Regorus keeps the cooperative time‑limit checks as the primary mechanism and treats cancellation tokens (if added) as an optional, host‑driven complement rather than a replacement.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Expose per-evaluation overrides (e.g., request-scoped budgets) to complement global and engine-level configuration.
|
||||
- Surface telemetry events whenever limits are hit, providing elapsed time at breach for observability pipelines.
|
||||
- Investigate dynamic adjustment of `check_interval` based on observed evaluation patterns to balance overhead and responsiveness.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
90
tests/interpreter/cases/limits/execution_timer.yaml
Normal file
90
tests/interpreter/cases/limits/execution_timer.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
cases:
|
||||
- note: engine-configured timer triggers limit error
|
||||
data:
|
||||
values: [1, 2, 3, 4, 5]
|
||||
modules:
|
||||
- |
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
count_values := count({value | value := data.values[_]})
|
||||
query: data.limits.timer.count_values
|
||||
execution_timer:
|
||||
limit_ms: 1
|
||||
check_interval: 1
|
||||
time_source:
|
||||
default_increment_ms: 5
|
||||
error: execution exceeded time limit
|
||||
|
||||
- note: global timer limit applies without engine override
|
||||
data:
|
||||
values: [1, 2, 3, 4, 5]
|
||||
modules:
|
||||
- |
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
count_values := count({value | value := data.values[_]})
|
||||
query: data.limits.timer.count_values
|
||||
global_execution_timer:
|
||||
limit_ms: 1
|
||||
check_interval: 1
|
||||
time_source:
|
||||
default_increment_ms: 5
|
||||
error: execution exceeded time limit
|
||||
|
||||
- note: engine override increases limit above global default
|
||||
data:
|
||||
values: [1, 2, 3, 4, 5]
|
||||
modules:
|
||||
- |
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
count_values := count({value | value := data.values[_]})
|
||||
query: data.limits.timer.count_values
|
||||
global_execution_timer:
|
||||
limit_ms: 1
|
||||
check_interval: 1
|
||||
execution_timer:
|
||||
limit_ms: 500
|
||||
check_interval: 1
|
||||
time_source:
|
||||
default_increment_ms: 5
|
||||
want_result: 5
|
||||
|
||||
- note: repeated ticks eventually exceed limit
|
||||
data:
|
||||
values: [1, 2, 3, 4, 5]
|
||||
modules:
|
||||
- |
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
count_values := count({value | value := data.values[_]})
|
||||
query: data.limits.timer.count_values
|
||||
execution_timer:
|
||||
limit_ms: 6
|
||||
check_interval: 1
|
||||
time_source:
|
||||
default_increment_ms: 2
|
||||
error: execution exceeded time limit
|
||||
|
||||
- note: global timer disabled removes limit
|
||||
data:
|
||||
values: [1, 2, 3, 4, 5]
|
||||
modules:
|
||||
- |
|
||||
package limits.timer
|
||||
import rego.v1
|
||||
|
||||
count_values := count({value | value := data.values[_]})
|
||||
query: data.limits.timer.count_values
|
||||
global_execution_timer:
|
||||
disable: true
|
||||
time_source:
|
||||
default_increment_ms: 5
|
||||
want_result: 5
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -141,7 +142,7 @@ fn invoke_dotnet_pack(
|
||||
let dir_arg = format!("/p:RegorusFFIArtifactsDir={}", artifacts_dir_str);
|
||||
|
||||
if clean {
|
||||
clean_msbuild_project(&project_dir, configuration)?;
|
||||
clean_msbuild_project(&project_dir)?;
|
||||
let artefact_root = project_dir.join("bin").join(configuration);
|
||||
if artefact_root.exists() {
|
||||
fs::remove_dir_all(&artefact_root).with_context(|| {
|
||||
@@ -340,7 +341,44 @@ impl TestCsharpCommand {
|
||||
println!(" {}", package.display());
|
||||
}
|
||||
|
||||
run_regorus_tests(&workspace, configuration, &package_dir, self.clean)?;
|
||||
let package_cache = workspace.join("bindings/csharp/.nuget/packages");
|
||||
fs::create_dir_all(&package_cache).with_context(|| {
|
||||
format!(
|
||||
"failed to create NuGet cache directory at {}",
|
||||
package_cache.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut package_versions = HashSet::new();
|
||||
for package in &packages {
|
||||
if let Some(version) = package
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.and_then(|stem| stem.strip_prefix("Regorus."))
|
||||
{
|
||||
package_versions.insert(version.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
for version in &package_versions {
|
||||
let cache_entry = package_cache.join("regorus").join(version);
|
||||
if cache_entry.exists() {
|
||||
fs::remove_dir_all(&cache_entry).with_context(|| {
|
||||
format!(
|
||||
"failed to remove cached package at {}",
|
||||
cache_entry.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
run_regorus_tests(
|
||||
&workspace,
|
||||
configuration,
|
||||
&package_dir,
|
||||
self.clean,
|
||||
&package_cache,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -351,21 +389,30 @@ fn run_regorus_tests(
|
||||
configuration: &str,
|
||||
package_dir: &Path,
|
||||
clean: bool,
|
||||
package_cache: &Path,
|
||||
) -> Result<()> {
|
||||
let nuget_source = package_dir
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("NuGet directory path contains invalid UTF-8"))?;
|
||||
let properties = vec![format!(
|
||||
"/p:RestoreAdditionalProjectSources={}",
|
||||
nuget_source
|
||||
)];
|
||||
let cache_path = package_cache
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("NuGet cache path contains invalid UTF-8"))?;
|
||||
let properties = vec![
|
||||
format!("/p:RestoreAdditionalProjectSources={}", nuget_source),
|
||||
format!("/p:RestorePackagesPath={}", cache_path),
|
||||
];
|
||||
let property_args: Vec<&str> = properties.iter().map(|value| value.as_str()).collect();
|
||||
|
||||
let regorus_tests = workspace.join("bindings/csharp/Regorus.Tests");
|
||||
if clean {
|
||||
clean_msbuild_project(®orus_tests, configuration)?;
|
||||
clean_msbuild_project(®orus_tests)?;
|
||||
}
|
||||
restore_with_source(®orus_tests, &property_args, "Regorus.Tests")?;
|
||||
restore_with_source(
|
||||
®orus_tests,
|
||||
&property_args,
|
||||
"Regorus.Tests",
|
||||
package_cache,
|
||||
)?;
|
||||
|
||||
let mut test = Command::new("dotnet");
|
||||
test.current_dir(®orus_tests);
|
||||
@@ -375,13 +422,14 @@ fn run_regorus_tests(
|
||||
test.arg(configuration);
|
||||
test.arg("--arch");
|
||||
test.arg(dotnet_host_arch());
|
||||
test.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(test, "dotnet test (Regorus.Tests)")?;
|
||||
|
||||
let test_app = workspace.join("bindings/csharp/TestApp");
|
||||
if clean {
|
||||
clean_msbuild_project(&test_app, configuration)?;
|
||||
clean_msbuild_project(&test_app)?;
|
||||
}
|
||||
restore_with_source(&test_app, &property_args, "TestApp")?;
|
||||
restore_with_source(&test_app, &property_args, "TestApp", package_cache)?;
|
||||
let mut build = Command::new("dotnet");
|
||||
build.current_dir(&test_app);
|
||||
build.arg("build");
|
||||
@@ -390,6 +438,7 @@ fn run_regorus_tests(
|
||||
build.arg(configuration);
|
||||
build.arg("--arch");
|
||||
build.arg(dotnet_host_arch());
|
||||
build.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(build, "dotnet build (TestApp)")?;
|
||||
|
||||
let mut run = Command::new("dotnet");
|
||||
@@ -402,13 +451,19 @@ fn run_regorus_tests(
|
||||
run.arg(configuration);
|
||||
run.arg("--arch");
|
||||
run.arg(dotnet_host_arch());
|
||||
run.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(run, "dotnet run (TestApp)")?;
|
||||
|
||||
let target_example = workspace.join("bindings/csharp/TargetExampleApp");
|
||||
if clean {
|
||||
clean_msbuild_project(&target_example, configuration)?;
|
||||
clean_msbuild_project(&target_example)?;
|
||||
}
|
||||
restore_with_source(&target_example, &property_args, "TargetExampleApp")?;
|
||||
restore_with_source(
|
||||
&target_example,
|
||||
&property_args,
|
||||
"TargetExampleApp",
|
||||
package_cache,
|
||||
)?;
|
||||
let mut build_example = Command::new("dotnet");
|
||||
build_example.current_dir(&target_example);
|
||||
build_example.arg("build");
|
||||
@@ -417,6 +472,7 @@ fn run_regorus_tests(
|
||||
build_example.arg(configuration);
|
||||
build_example.arg("--arch");
|
||||
build_example.arg(dotnet_host_arch());
|
||||
build_example.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(build_example, "dotnet build (TargetExampleApp)")?;
|
||||
|
||||
let mut run_example = Command::new("dotnet");
|
||||
@@ -429,12 +485,18 @@ fn run_regorus_tests(
|
||||
run_example.arg(configuration);
|
||||
run_example.arg("--arch");
|
||||
run_example.arg(dotnet_host_arch());
|
||||
run_example.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(run_example, "dotnet run (TargetExampleApp)")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_with_source(project_dir: &Path, properties: &[&str], label: &str) -> Result<()> {
|
||||
fn restore_with_source(
|
||||
project_dir: &Path,
|
||||
properties: &[&str],
|
||||
label: &str,
|
||||
package_cache: &Path,
|
||||
) -> Result<()> {
|
||||
let mut restore = Command::new("dotnet");
|
||||
restore.current_dir(project_dir);
|
||||
restore.arg("restore");
|
||||
@@ -443,23 +505,15 @@ fn restore_with_source(project_dir: &Path, properties: &[&str], label: &str) ->
|
||||
for property in properties {
|
||||
restore.arg(property);
|
||||
}
|
||||
restore.env("NUGET_PACKAGES", package_cache);
|
||||
run_command(restore, &format!("dotnet restore ({label})"))
|
||||
}
|
||||
|
||||
fn clean_msbuild_project(project_dir: &Path, configuration: &str) -> Result<()> {
|
||||
fn clean_msbuild_project(project_dir: &Path) -> Result<()> {
|
||||
if !project_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut clean = Command::new("dotnet");
|
||||
clean.current_dir(project_dir);
|
||||
clean.arg("clean");
|
||||
clean.arg("-c");
|
||||
clean.arg(configuration);
|
||||
clean.arg("--verbosity");
|
||||
clean.arg("minimal");
|
||||
run_command(clean, "dotnet clean")?;
|
||||
|
||||
let bin_dir = project_dir.join("bin");
|
||||
if bin_dir.exists() {
|
||||
fs::remove_dir_all(&bin_dir).with_context(|| {
|
||||
|
||||
Reference in New Issue
Block a user