Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ bindings/*/target
# C# build folders
**bin
**obj
bindings/csharp/.nuget/

# Bundler binstubs regenerated during ruby setup
bindings/ruby/bin/
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
20 changes: 8 additions & 12 deletions bindings/csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -48,23 +51,16 @@ 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
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 changes: 131 additions & 0 deletions bindings/csharp/Regorus.Tests/ExecutionTimerTests.cs
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);
}
}
22 changes: 11 additions & 11 deletions bindings/csharp/Regorus.Tests/RegorusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[TestClass]
public class RegorusTests
{
private static readonly object LimitLock = new();
private static readonly object LimitLock = new();

[TestMethod]
public void Basic_evaluation_succeeds()
Expand Down Expand Up @@ -195,8 +195,8 @@

var packageNames = JsonNode.Parse(result!);

Assert.AreEqual("test", packageNames![0]["package_name"].ToString());

Check warning on line 198 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 198 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 198 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Check warning on line 198 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.
Assert.AreEqual("test.nested.name", packageNames![1]["package_name"].ToString());

Check warning on line 199 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 199 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 199 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Check warning on line 199 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.
}

[TestMethod]
Expand All @@ -211,10 +211,10 @@

var parameters = JsonNode.Parse(result!);

Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count);

Check warning on line 214 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 214 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 214 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Check warning on line 214 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.
Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count);

Check warning on line 215 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 215 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 215 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Check warning on line 215 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString());

Check warning on line 217 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 217 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-unknown-linux-gnu)

Dereference of a possibly null reference.

Check warning on line 217 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.

Check warning on line 217 in bindings/csharp/Regorus.Tests/RegorusTests.cs

View workflow job for this annotation

GitHub Actions / Test Regorus Nuget: (x86_64-pc-windows-msvc)

Dereference of a possibly null reference.
Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString());
}

Expand Down Expand Up @@ -374,17 +374,17 @@

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();
}
}
}
9 changes: 9 additions & 0 deletions bindings/csharp/Regorus/CompiledPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,14 @@ private T UseHandle<T>(Func<IntPtr, T> func)
}
}
}

private void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
action(handlePtr);
return null;
});
}

Copy link
Copy Markdown

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 null as object?. We could instead copy/paste the UseHandle method without the return:

private void UseHandle(Action<IntPtr> action)
{
    var handle = GetHandleForUse();
    bool addedRef = false;
    try
    {
        handle.DangerousAddRef(ref addedRef);
        var pointer = handle.DangerousGetHandle();
        if (pointer == IntPtr.Zero)
        {
            throw new ObjectDisposedException(nameof(CompiledPolicy));
        }

        action(pointer);
    }
    finally
    {
        if (addedRef)
        {
            handle.DangerousRelease();
        }
    }
}

Copy link
Copy Markdown

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:

private T UseHandle<T>(Func<IntPtr, T> func)
{
    T? result = default!;
    WithHandle(ptr => result = func(ptr));
    return result!;
}

private void UseHandle(Action<IntPtr> action)
{
    WithHandle(action);
}

private void WithHandle(Action<IntPtr> body)
{
    var handle = GetHandleForUse();
    bool addedRef = false;
    try
    {
        handle.DangerousAddRef(ref addedRef);
        var pointer = handle.DangerousGetHandle();
        if (pointer == IntPtr.Zero)
        {
            throw new ObjectDisposedException(nameof(CompiledPolicy));
        }

        body(pointer);
    }
    finally
    {
        if (addedRef)
        {
            handle.DangerousRelease();
        }
    }
}

Copy link
Copy Markdown
Collaborator Author

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.

}
}
55 changes: 54 additions & 1 deletion bindings/csharp/Regorus/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT License.

using System;
using System.Runtime.InteropServices;
using System.Text;
using Regorus.Internal;


Expand All @@ -24,6 +26,17 @@ public Engine()
_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);
Expand Down Expand Up @@ -87,6 +100,32 @@ public void SetStrictBuiltinErrors(bool strict)
}
});
}

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();
Expand Down Expand Up @@ -355,7 +394,21 @@ public void SetGatherPrints(bool enable)
});
}

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
{
Expand Down
62 changes: 62 additions & 0 deletions bindings/csharp/Regorus/ExecutionTimerConfig.cs
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,
};
}
}
}
Loading
Loading