mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Rate Policy
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Protocol;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class AsyncStreamIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AsyncEnumerable_IsPulledAcrossProtocol()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Numbers));
|
||||
var stream = remote._InvokeStream<int>(
|
||||
function.Index,
|
||||
new Map<byte, object> { [0] = 4 });
|
||||
|
||||
var values = new List<int>();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
await foreach (var value in stream.WithCancellation(timeout.Token))
|
||||
values.Add(value);
|
||||
|
||||
Assert.Equal(new[] { 0, 1, 2, 3 }, values);
|
||||
Assert.True(stream.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposingAsyncEnumerable_TerminatesRemoteExecution()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Infinite));
|
||||
var stream = remote._InvokeStream<int>(function.Index, Array.Empty<object>());
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
Assert.True(await enumerator.MoveNextAsync());
|
||||
Assert.Equal(0, enumerator.Current);
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
Assert.True(stream.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PausablePushStream_HaltsAndResumesAcrossProtocol()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/stream"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(StreamResource.Pausable));
|
||||
var stream = remote._InvokeStream<int>(function.Index, Array.Empty<object>());
|
||||
var enumerator = stream.GetAsyncEnumerator();
|
||||
|
||||
Assert.True(await enumerator.MoveNextAsync());
|
||||
Assert.Equal(0, enumerator.Current);
|
||||
|
||||
await stream.Halt();
|
||||
var haltedMove = enumerator.MoveNextAsync().AsTask();
|
||||
await Task.Delay(200);
|
||||
Assert.False(haltedMove.IsCompleted);
|
||||
|
||||
await stream.Resume();
|
||||
Assert.True(await haltedMove.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
Assert.Equal(1, enumerator.Current);
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
|
||||
static Task<IntegrationCluster> StartCluster()
|
||||
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||
{
|
||||
await warehouse.Put("sys/stream", new StreamResource());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Permissions;
|
||||
using Esiur.Security.RateLimiting;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class RateControlIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FunctionCalls_AreDeniedWhenPolicyIsExhausted()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
var first = await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
|
||||
Assert.Equal(1, Convert.ToInt32(first));
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PropertySets_AreDeniedWhenPolicyIsExhausted()
|
||||
{
|
||||
await using var cluster = await StartCluster().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var property = remote.Instance.Definition.GetPropertyDefByName(nameof(RateLimitedResource.Value));
|
||||
|
||||
await remote.SetResourcePropertyAsync(property.Index, 10);
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote.SetResourcePropertyAsync(property.Index, 20));
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RepeatedDenials_BlockTheConnection()
|
||||
{
|
||||
await using var cluster = await StartCluster(denialsBeforeBlock: 1)
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
cluster.Connection.AutoReconnect = false;
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
await WaitUntilAsync(() => !cluster.Connection.IsConnected, TimeSpan.FromSeconds(3));
|
||||
Assert.False(cluster.Connection.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueuedCalls_AreDelayedAndQueueOverflowIsDenied()
|
||||
{
|
||||
await using var cluster = await StartCluster(
|
||||
queueLimit: 1,
|
||||
period: TimeSpan.FromMilliseconds(250)).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
var remote = await GetRemote(cluster);
|
||||
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(RateLimitedResource.Call));
|
||||
|
||||
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var queued = remote._Invoke(function.Index, Array.Empty<object>());
|
||||
var exception = await Assert.ThrowsAsync<AsyncException>(async () =>
|
||||
await remote._Invoke(function.Index, Array.Empty<object>()));
|
||||
var result = await queued;
|
||||
|
||||
Assert.Equal(ExceptionCode.RateLimitExceeded, exception.Code);
|
||||
Assert.Equal(2, Convert.ToInt32(result));
|
||||
Assert.True(stopwatch.Elapsed >= TimeSpan.FromMilliseconds(150));
|
||||
}
|
||||
|
||||
static Task<IntegrationCluster> StartCluster(
|
||||
int denialsBeforeBlock = 10,
|
||||
int queueLimit = 0,
|
||||
TimeSpan? period = null)
|
||||
=> IntegrationCluster.StartAsync(async warehouse =>
|
||||
{
|
||||
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = denialsBeforeBlock;
|
||||
warehouse.Configuration.RateControl.ConnectionBlockDelay = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = period ?? TimeSpan.FromMinutes(1),
|
||||
QueueLimit = queueLimit,
|
||||
});
|
||||
warehouse.AddRatePolicy(new BurstRatePolicy("standard-set")
|
||||
{
|
||||
PermitLimit = 1,
|
||||
Period = period ?? TimeSpan.FromMinutes(1),
|
||||
QueueLimit = queueLimit,
|
||||
});
|
||||
|
||||
var resource = await warehouse.Put("sys/rate", new RateLimitedResource());
|
||||
resource.Instance!.Managers.Add(new AllowPropertySetPermissions());
|
||||
});
|
||||
|
||||
static async Task<EpResource> GetRemote(IntegrationCluster cluster)
|
||||
=> await Task.Run(async () =>
|
||||
(EpResource)await cluster.Connection.Get("sys/rate"))
|
||||
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
while (!condition())
|
||||
{
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
throw new TimeoutException("The expected condition was not reached.");
|
||||
await Task.Delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllowPropertySetPermissions : IPermissionsManager
|
||||
{
|
||||
public Map<string, object> Settings { get; } = new();
|
||||
|
||||
public Ruling Applicable(
|
||||
IResource resource,
|
||||
Session session,
|
||||
ActionType action,
|
||||
MemberDef member,
|
||||
object inquirer = null!)
|
||||
=> action == ActionType.SetProperty ? Ruling.Allowed : Ruling.DontCare;
|
||||
|
||||
public bool Initialize(Map<string, object> settings, IResource resource) => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Esiur.Resource;
|
||||
using System.Threading;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Resource]
|
||||
public partial class RateLimitedResource
|
||||
{
|
||||
int _callCount;
|
||||
int _value;
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-call")]
|
||||
public int Call() => Interlocked.Increment(ref _callCount);
|
||||
|
||||
[Export]
|
||||
[RateControl("standard-set")]
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set => _value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Esiur.Core;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Resource;
|
||||
|
||||
namespace Esiur.Tests.Unit.Integration;
|
||||
|
||||
[Resource]
|
||||
public partial class StreamResource
|
||||
{
|
||||
[Export]
|
||||
public async IAsyncEnumerable<int> Numbers(int count, InvocationContext context)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await Task.Delay(5, context.CancellationToken);
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
public async IAsyncEnumerable<int> Infinite(InvocationContext context)
|
||||
{
|
||||
var value = 0;
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(5, context.CancellationToken);
|
||||
yield return value++;
|
||||
}
|
||||
}
|
||||
|
||||
[Export]
|
||||
[Stream(StreamMode.Push, Pausable = true)]
|
||||
public AsyncReply<int> Pausable(InvocationContext context)
|
||||
{
|
||||
var reply = new AsyncReply<int>();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
await context.WaitWhileHaltedAsync();
|
||||
reply.TriggerChunk(i);
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
reply.Trigger(0);
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user