-
Notifications
You must be signed in to change notification settings - Fork 74
feat!: Ability to limit policy evaluation time #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Anand Krishnamoorthi (anakrish)
merged 1 commit into
microsoft:main
from
anakrish:execution-limits
Jan 28, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I don't know how hot is this code path but we are boxing
nullasobject?. We could instead copy/paste the UseHandle method without the return:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative option would be to refactor the handle handling :-) logic to a separate method and call it from both:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #554 to track this. There is one C# specific PR remaining (expose VM to C#) that will wrap up the recent stream of work to improve robustness at Azure scale (super strict clippy lints, memory limits, executions limits). I will address this along with that.