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:
@@ -271,7 +271,7 @@ public class AsyncReply
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TriggerError(Exception exception)
|
public virtual void TriggerError(Exception exception)
|
||||||
{
|
{
|
||||||
//timeout?.Dispose();
|
//timeout?.Dispose();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2017-2026 Ahmed Kh. Zamil
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Data.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Esiur.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a remotely executing stream and exposes its lifecycle controls.
|
||||||
|
/// </summary>
|
||||||
|
public class AsyncStreamReply : AsyncReply
|
||||||
|
{
|
||||||
|
readonly Func<AsyncReply> _pull;
|
||||||
|
readonly Func<AsyncReply> _terminate;
|
||||||
|
readonly Func<AsyncReply> _halt;
|
||||||
|
readonly Func<AsyncReply> _resume;
|
||||||
|
|
||||||
|
readonly object _streamLock = new object();
|
||||||
|
readonly TaskCompletionSource<bool> _started = NewCompletionSource();
|
||||||
|
|
||||||
|
Exception _streamException;
|
||||||
|
bool _streamStarted;
|
||||||
|
bool _streamCompleted;
|
||||||
|
bool _terminationSent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the delivery mode declared by the remote function.
|
||||||
|
/// </summary>
|
||||||
|
public StreamMode StreamMode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the peer acknowledged that the stream has started.
|
||||||
|
/// </summary>
|
||||||
|
public bool Started
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
return _streamStarted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the stream completed or was terminated.
|
||||||
|
/// </summary>
|
||||||
|
public bool Completed
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
return _streamCompleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AsyncStreamReply(
|
||||||
|
StreamMode streamMode,
|
||||||
|
Func<AsyncReply> pull,
|
||||||
|
Func<AsyncReply> terminate,
|
||||||
|
Func<AsyncReply> halt,
|
||||||
|
Func<AsyncReply> resume)
|
||||||
|
{
|
||||||
|
StreamMode = streamMode;
|
||||||
|
_pull = pull;
|
||||||
|
_terminate = terminate;
|
||||||
|
_halt = halt;
|
||||||
|
_resume = resume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests the next item of a pull stream.
|
||||||
|
/// </summary>
|
||||||
|
public AsyncReply Pull()
|
||||||
|
{
|
||||||
|
if (StreamMode != StreamMode.Pull)
|
||||||
|
throw new InvalidOperationException("Pull is only valid for a pull stream.");
|
||||||
|
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted)
|
||||||
|
return new AsyncReply(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _pull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Terminates the remote stream execution and releases its enumerator.
|
||||||
|
/// </summary>
|
||||||
|
public AsyncReply Terminate()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_terminationSent || _streamCompleted)
|
||||||
|
return new AsyncReply(null);
|
||||||
|
|
||||||
|
_terminationSent = true;
|
||||||
|
_streamCompleted = true;
|
||||||
|
_started.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnStreamCompleted();
|
||||||
|
|
||||||
|
var reply = _terminate();
|
||||||
|
reply.Error(TriggerStreamError);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Halts a pausable remote stream execution.
|
||||||
|
/// </summary>
|
||||||
|
public AsyncReply Halt()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted)
|
||||||
|
return new AsyncReply(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply = _halt();
|
||||||
|
reply.Error(TriggerStreamError);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resumes a halted remote stream execution.
|
||||||
|
/// </summary>
|
||||||
|
public AsyncReply Resume()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted)
|
||||||
|
return new AsyncReply(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply = _resume();
|
||||||
|
reply.Error(TriggerStreamError);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TriggerStreamStarted()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted || _streamException != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_streamStarted = true;
|
||||||
|
_started.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnStreamStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TriggerStreamCompleted()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_streamCompleted = true;
|
||||||
|
_started.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnStreamCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TriggerStreamError(AsyncException exception)
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
{
|
||||||
|
if (_streamCompleted || _streamException != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_streamException = exception;
|
||||||
|
_streamCompleted = true;
|
||||||
|
_started.TrySetException(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnStreamError(exception);
|
||||||
|
|
||||||
|
if (errorCallbacks != null)
|
||||||
|
base.TriggerError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void TriggerError(Exception exception)
|
||||||
|
{
|
||||||
|
TriggerStreamError(exception as AsyncException ?? new AsyncException(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Task WaitUntilStartedAsync() => _started.Task;
|
||||||
|
|
||||||
|
internal Exception GetStreamException()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
return _streamException;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsStreamCompleted()
|
||||||
|
{
|
||||||
|
lock (_streamLock)
|
||||||
|
return _streamCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnStreamStarted() { }
|
||||||
|
protected virtual void OnStreamCompleted() { }
|
||||||
|
protected virtual void OnStreamError(Exception exception) { }
|
||||||
|
|
||||||
|
protected static TaskCompletionSource<bool> NewCompletionSource()
|
||||||
|
=> new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A typed remote stream that can be consumed with <c>await foreach</c>.
|
||||||
|
/// Pull streams request exactly one remote item for each <c>MoveNextAsync</c> call.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AsyncStreamReply<T> : AsyncStreamReply, IAsyncEnumerable<T>
|
||||||
|
{
|
||||||
|
readonly object _itemsLock = new object();
|
||||||
|
readonly Queue<T> _items = new Queue<T>();
|
||||||
|
|
||||||
|
TaskCompletionSource<bool> _itemAvailable;
|
||||||
|
bool _enumeratorCreated;
|
||||||
|
bool _movePending;
|
||||||
|
|
||||||
|
internal AsyncStreamReply(
|
||||||
|
StreamMode streamMode,
|
||||||
|
Func<AsyncReply> pull,
|
||||||
|
Func<AsyncReply> terminate,
|
||||||
|
Func<AsyncReply> halt,
|
||||||
|
Func<AsyncReply> resume)
|
||||||
|
: base(streamMode, pull, terminate, halt, resume)
|
||||||
|
{
|
||||||
|
Chunk(value => ReceiveItem((T)RuntimeCaster.Cast(value, typeof(T))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
if (_enumeratorCreated)
|
||||||
|
throw new InvalidOperationException("A remote stream can only be enumerated once.");
|
||||||
|
|
||||||
|
_enumeratorCreated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Enumerator(this, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReceiveItem(T item)
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> itemAvailable;
|
||||||
|
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
if (IsStreamCompleted() || GetStreamException() != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_items.Enqueue(item);
|
||||||
|
itemAvailable = _itemAvailable;
|
||||||
|
_itemAvailable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemAvailable?.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStreamCompleted()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> itemAvailable;
|
||||||
|
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
itemAvailable = _itemAvailable;
|
||||||
|
_itemAvailable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemAvailable?.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStreamError(Exception exception)
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> itemAvailable;
|
||||||
|
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
itemAvailable = _itemAvailable;
|
||||||
|
_itemAvailable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemAvailable?.TrySetException(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<(bool HasValue, T Value)> MoveNextAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
if (_movePending)
|
||||||
|
throw new InvalidOperationException("Concurrent MoveNextAsync calls are not supported.");
|
||||||
|
|
||||||
|
_movePending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await WaitUntilStartedAsync();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> itemAvailable;
|
||||||
|
|
||||||
|
lock (_itemsLock)
|
||||||
|
{
|
||||||
|
var exception = GetStreamException();
|
||||||
|
if (exception != null)
|
||||||
|
throw exception;
|
||||||
|
|
||||||
|
if (_items.Count > 0)
|
||||||
|
return (true, _items.Dequeue());
|
||||||
|
|
||||||
|
if (IsStreamCompleted())
|
||||||
|
return (false, default);
|
||||||
|
|
||||||
|
_itemAvailable = NewCompletionSource();
|
||||||
|
itemAvailable = _itemAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StreamMode == StreamMode.Pull)
|
||||||
|
{
|
||||||
|
var pull = Pull();
|
||||||
|
_ = pull.Error(TriggerStreamError);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (cancellationToken.Register(() =>
|
||||||
|
{
|
||||||
|
itemAvailable.TrySetCanceled();
|
||||||
|
_ = Terminate();
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
await itemAvailable.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_itemsLock)
|
||||||
|
_movePending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
if (IsStreamCompleted())
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Enumerator : IAsyncEnumerator<T>
|
||||||
|
{
|
||||||
|
readonly AsyncStreamReply<T> _owner;
|
||||||
|
readonly CancellationToken _cancellationToken;
|
||||||
|
|
||||||
|
public T Current { get; private set; }
|
||||||
|
|
||||||
|
internal Enumerator(AsyncStreamReply<T> owner, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> MoveNextAsync()
|
||||||
|
{
|
||||||
|
var result = await _owner.MoveNextAsync(_cancellationToken);
|
||||||
|
Current = result.Value;
|
||||||
|
return result.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync() => new ValueTask(_owner.DisposeAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,5 +46,6 @@ public enum ExceptionCode : ushort
|
|||||||
Timeout,
|
Timeout,
|
||||||
NotSupported,
|
NotSupported,
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
NotAllowed
|
NotAllowed,
|
||||||
|
RateLimitExceeded
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,47 @@
|
|||||||
using Esiur.Protocol;
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Protocol;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Esiur.Core
|
namespace Esiur.Core
|
||||||
{
|
{
|
||||||
public class InvocationContext
|
public class InvocationContext
|
||||||
{
|
{
|
||||||
private uint CallbackId;
|
readonly uint CallbackId;
|
||||||
|
readonly object _stateLock = new object();
|
||||||
|
readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
|
||||||
|
readonly SemaphoreSlim _moveLock = new SemaphoreSlim(1, 1);
|
||||||
|
TaskCompletionSource<bool> _resumeSignal = CompletedSignal();
|
||||||
|
|
||||||
internal bool Ended;
|
IAsyncEnumeratorAdapter _asyncEnumerator;
|
||||||
|
IEnumerator _enumerator;
|
||||||
|
bool _halted;
|
||||||
|
|
||||||
|
internal StreamMode StreamMode { get; private set; }
|
||||||
|
internal bool Pausable { get; private set; }
|
||||||
|
|
||||||
|
internal volatile bool Ended;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a token that is cancelled when the caller terminates this execution.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken CancellationToken => _cancellation.Token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether a pausable execution is currently halted.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHalted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
return _halted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Chunk(object value)
|
public void Chunk(object value)
|
||||||
{
|
{
|
||||||
@@ -19,8 +51,8 @@ namespace Esiur.Core
|
|||||||
Connection.SendChunk(CallbackId, value);
|
Connection.SendChunk(CallbackId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Progress(uint value, uint max) {
|
public void Progress(uint value, uint max)
|
||||||
|
{
|
||||||
if (Ended)
|
if (Ended)
|
||||||
throw new Exception("Execution has ended.");
|
throw new Exception("Execution has ended.");
|
||||||
|
|
||||||
@@ -29,7 +61,6 @@ namespace Esiur.Core
|
|||||||
|
|
||||||
public void Warning(byte level, string message)
|
public void Warning(byte level, string message)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (Ended)
|
if (Ended)
|
||||||
throw new Exception("Execution has ended.");
|
throw new Exception("Execution has ended.");
|
||||||
|
|
||||||
@@ -38,12 +69,192 @@ namespace Esiur.Core
|
|||||||
|
|
||||||
public EpConnection Connection { get; internal set; }
|
public EpConnection Connection { get; internal set; }
|
||||||
|
|
||||||
|
|
||||||
internal InvocationContext(EpConnection connection, uint callbackId)
|
internal InvocationContext(EpConnection connection, uint callbackId)
|
||||||
{
|
{
|
||||||
Connection = connection;
|
Connection = connection;
|
||||||
CallbackId = callbackId;
|
CallbackId = callbackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void InitializeStream(StreamMode streamMode, bool pausable)
|
||||||
|
{
|
||||||
|
StreamMode = streamMode;
|
||||||
|
Pausable = pausable;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool SetAsyncEnumerable(object enumerable)
|
||||||
|
{
|
||||||
|
var asyncEnumerableInterface = enumerable.GetType()
|
||||||
|
.GetInterfaces()
|
||||||
|
.Concat(new[] { enumerable.GetType() })
|
||||||
|
.FirstOrDefault(x => x.IsGenericType &&
|
||||||
|
x.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>));
|
||||||
|
|
||||||
|
if (asyncEnumerableInterface == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var itemType = asyncEnumerableInterface.GetGenericArguments()[0];
|
||||||
|
var adapterType = typeof(AsyncEnumeratorAdapter<>).MakeGenericType(itemType);
|
||||||
|
_asyncEnumerator = (IAsyncEnumeratorAdapter)Activator.CreateInstance(
|
||||||
|
adapterType, enumerable, _cancellation.Token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetEnumerable(IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
_enumerator = enumerable.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<(bool HasValue, object Value)> PullAsync()
|
||||||
|
{
|
||||||
|
if (StreamMode != StreamMode.Pull || _asyncEnumerator == null)
|
||||||
|
throw new InvalidOperationException("Execution is not an asynchronous pull stream.");
|
||||||
|
|
||||||
|
await _moveLock.WaitAsync(_cancellation.Token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WaitWhileHaltedAsync();
|
||||||
|
_cancellation.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var hasValue = await _asyncEnumerator.MoveNextAsync();
|
||||||
|
return (hasValue, hasValue ? _asyncEnumerator.Current : null);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moveLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<(bool HasValue, object Value)> MoveNextAsync()
|
||||||
|
{
|
||||||
|
if (_enumerator == null)
|
||||||
|
throw new InvalidOperationException("Execution is not a synchronous stream.");
|
||||||
|
|
||||||
|
await WaitWhileHaltedAsync();
|
||||||
|
_cancellation.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var hasValue = _enumerator.MoveNext();
|
||||||
|
return (hasValue, hasValue ? _enumerator.Current : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Halt()
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Ended || !Pausable || _halted)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_halted = true;
|
||||||
|
_resumeSignal = NewSignal();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Resume()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> resumeSignal;
|
||||||
|
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Ended || !Pausable || !_halted)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_halted = false;
|
||||||
|
resumeSignal = _resumeSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeSignal.TrySetResult(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously waits until a halted execution is resumed.
|
||||||
|
/// </summary>
|
||||||
|
public async Task WaitWhileHaltedAsync()
|
||||||
|
{
|
||||||
|
Task resumeTask;
|
||||||
|
|
||||||
|
lock (_stateLock)
|
||||||
|
resumeTask = _resumeSignal.Task;
|
||||||
|
|
||||||
|
await resumeTask;
|
||||||
|
_cancellation.Token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task TerminateAsync()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> resumeSignal;
|
||||||
|
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Ended)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Ended = true;
|
||||||
|
_halted = false;
|
||||||
|
resumeSignal = _resumeSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeSignal.TrySetResult(true);
|
||||||
|
_cancellation.Cancel();
|
||||||
|
|
||||||
|
if (_asyncEnumerator != null)
|
||||||
|
await _asyncEnumerator.DisposeAsync();
|
||||||
|
|
||||||
|
(_enumerator as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task EndAsync()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> resumeSignal;
|
||||||
|
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Ended)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Ended = true;
|
||||||
|
_halted = false;
|
||||||
|
resumeSignal = _resumeSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeSignal.TrySetResult(true);
|
||||||
|
|
||||||
|
if (_asyncEnumerator != null)
|
||||||
|
await _asyncEnumerator.DisposeAsync();
|
||||||
|
|
||||||
|
(_enumerator as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static TaskCompletionSource<bool> NewSignal()
|
||||||
|
=> new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
static TaskCompletionSource<bool> CompletedSignal()
|
||||||
|
{
|
||||||
|
var signal = NewSignal();
|
||||||
|
signal.SetResult(true);
|
||||||
|
return signal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IAsyncEnumeratorAdapter
|
||||||
|
{
|
||||||
|
object Current { get; }
|
||||||
|
ValueTask<bool> MoveNextAsync();
|
||||||
|
ValueTask DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class AsyncEnumeratorAdapter<T> : IAsyncEnumeratorAdapter
|
||||||
|
{
|
||||||
|
readonly IAsyncEnumerator<T> _enumerator;
|
||||||
|
|
||||||
|
public object Current => _enumerator.Current;
|
||||||
|
|
||||||
|
public AsyncEnumeratorAdapter(IAsyncEnumerable<T> enumerable, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> MoveNextAsync() => _enumerator.MoveNextAsync();
|
||||||
|
public ValueTask DisposeAsync() => _enumerator.DisposeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ public class ConstantDef : MemberDef
|
|||||||
{
|
{
|
||||||
public object Value { get; set; }
|
public object Value { get; set; }
|
||||||
|
|
||||||
public Map<string, string> Annotations { get; set; }
|
|
||||||
public Tru ValueType { get; set; }
|
public Tru ValueType { get; set; }
|
||||||
|
|
||||||
public FieldInfo FieldInfo { get; set; }
|
public FieldInfo FieldInfo { get; set; }
|
||||||
@@ -128,7 +127,7 @@ public class ConstantDef : MemberDef
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return new ConstantDef()
|
return DefinitionAttributeReader.Apply(ci, new ConstantDef()
|
||||||
{
|
{
|
||||||
Name = customName,
|
Name = customName,
|
||||||
Index = index,
|
Index = index,
|
||||||
@@ -137,7 +136,7 @@ public class ConstantDef : MemberDef
|
|||||||
Value = value,
|
Value = value,
|
||||||
FieldInfo = ci,
|
FieldInfo = ci,
|
||||||
Annotations = annotations,
|
Annotations = annotations,
|
||||||
};
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Esiur.Resource;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Esiur.Data.Types;
|
||||||
|
|
||||||
|
internal static class DefinitionAttributeReader
|
||||||
|
{
|
||||||
|
internal static void Apply(Type source, TypeDef target)
|
||||||
|
{
|
||||||
|
target.Usage = source.GetCustomAttribute<UsageAttribute>(true)?.Value;
|
||||||
|
target.Description = source.GetCustomAttribute<DescriptionAttribute>(true)?.Value;
|
||||||
|
target.Example = source.GetCustomAttributes<ExampleAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.FirstOrDefault();
|
||||||
|
target.Category = source.GetCustomAttribute<CategoryAttribute>(true)?.Value;
|
||||||
|
target.Since = source.GetCustomAttribute<SinceAttribute>(true)?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static T Apply<T>(MemberInfo source, T target) where T : MemberDef
|
||||||
|
{
|
||||||
|
target.Description = source.GetCustomAttribute<DescriptionAttribute>(true)?.Value;
|
||||||
|
target.Usage = source.GetCustomAttribute<UsageAttribute>(true)?.Value;
|
||||||
|
|
||||||
|
var examples = source.GetCustomAttributes<ExampleAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
target.Examples = examples.Count == 0 ? null : examples;
|
||||||
|
|
||||||
|
var tags = source.GetCustomAttribute<TagsAttribute>(true)?.Values;
|
||||||
|
target.Tags = tags == null || tags.Length == 0 ? null : tags.ToList();
|
||||||
|
target.Unit = source.GetCustomAttribute<UnitAttribute>(true)?.Value;
|
||||||
|
target.Minimum = source.GetCustomAttribute<MinimumAttribute>(true)?.Value;
|
||||||
|
target.Maximum = source.GetCustomAttribute<MaximumAttribute>(true)?.Value;
|
||||||
|
|
||||||
|
var allowedValues = source.GetCustomAttributes<AllowedValueAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
target.AllowedValues = allowedValues.Count == 0 ? null : allowedValues;
|
||||||
|
|
||||||
|
target.Pattern = source.GetCustomAttribute<PatternAttribute>(true)?.Value;
|
||||||
|
target.Format = source.GetCustomAttribute<FormatAttribute>(true)?.Value;
|
||||||
|
|
||||||
|
var preconditions = source.GetCustomAttributes<PreconditionAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
target.Preconditions = preconditions.Count == 0 ? null : preconditions;
|
||||||
|
|
||||||
|
var postconditions = source.GetCustomAttributes<PostconditionAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
target.Postconditions = postconditions.Count == 0 ? null : postconditions;
|
||||||
|
|
||||||
|
target.Effects = source.GetCustomAttribute<EffectsAttribute>(true)?.Value
|
||||||
|
?? OperationEffects.None;
|
||||||
|
|
||||||
|
var warnings = source.GetCustomAttributes<WarningAttribute>(true)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
target.Warnings = warnings.Count == 0 ? null : warnings;
|
||||||
|
|
||||||
|
var relatedMembers = source.GetCustomAttribute<RelatedMembersAttribute>(true)?.Indexes;
|
||||||
|
target.RelatedMembers = relatedMembers == null || relatedMembers.Length == 0
|
||||||
|
? null
|
||||||
|
: relatedMembers.ToList();
|
||||||
|
|
||||||
|
var obsolete = source.GetCustomAttribute<ObsoleteAttribute>(true);
|
||||||
|
target.Deprecated = obsolete != null;
|
||||||
|
target.DeprecationMessage = obsolete?.Message;
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
|
|||||||
public class EventDef : MemberDef
|
public class EventDef : MemberDef
|
||||||
{
|
{
|
||||||
|
|
||||||
public Map<string, string> Annotations
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Name}: {ArgumentType}";
|
return $"{Name}: {ArgumentType}";
|
||||||
@@ -35,7 +29,9 @@ public class EventDef : MemberDef
|
|||||||
set => AutoDelivered = !value;
|
set => AutoDelivered = !value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Deprecated { get; set; }
|
public bool Historical { get; set; }
|
||||||
|
|
||||||
|
public OrderingControl OrderingControl { get; set; }
|
||||||
|
|
||||||
public EventInfo EventInfo { get; set; }
|
public EventInfo EventInfo { get; set; }
|
||||||
|
|
||||||
@@ -166,7 +162,9 @@ public class EventDef : MemberDef
|
|||||||
throw new Exception($"Unsupported type `{argType}` in event `{type.Name}.{ei.Name}`");
|
throw new Exception($"Unsupported type `{argType}` in event `{type.Name}.{ei.Name}`");
|
||||||
|
|
||||||
var annotationAttrs = ei.GetCustomAttributes<AnnotationAttribute>(true);
|
var annotationAttrs = ei.GetCustomAttributes<AnnotationAttribute>(true);
|
||||||
var autoDeliveredAttr = ei.GetCustomAttribute<AutoDeliveredAttribute>(true);
|
var autoDeliveryAttr = ei.GetCustomAttribute<AutoDeliveryAttribute>(true);
|
||||||
|
var historicalAttr = ei.GetCustomAttribute<HistoricalAttribute>(true);
|
||||||
|
var orderingAttr = ei.GetCustomAttribute<OrderingAttribute>(true);
|
||||||
|
|
||||||
//evtType.Nullable = new NullabilityInfoContext().Create(ei).ReadState is NullabilityState.Nullable;
|
//evtType.Nullable = new NullabilityInfoContext().Create(ei).ReadState is NullabilityState.Nullable;
|
||||||
|
|
||||||
@@ -209,7 +207,7 @@ public class EventDef : MemberDef
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new EventDef()
|
return DefinitionAttributeReader.Apply(ei, new EventDef()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
ArgumentType = evtType,
|
ArgumentType = evtType,
|
||||||
@@ -217,8 +215,10 @@ public class EventDef : MemberDef
|
|||||||
Inherited = ei.DeclaringType != type,
|
Inherited = ei.DeclaringType != type,
|
||||||
Annotations = annotations,
|
Annotations = annotations,
|
||||||
EventInfo = ei,
|
EventInfo = ei,
|
||||||
AutoDelivered = autoDeliveredAttr != null
|
AutoDelivered = autoDeliveryAttr != null,
|
||||||
};
|
Historical = historicalAttr != null,
|
||||||
|
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
|
|||||||
public class FunctionDef : MemberDef
|
public class FunctionDef : MemberDef
|
||||||
{
|
{
|
||||||
|
|
||||||
public Map<string, string> Annotations
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
//public bool IsVoid
|
//public bool IsVoid
|
||||||
//{
|
//{
|
||||||
// get;
|
// get;
|
||||||
@@ -33,7 +27,7 @@ public class FunctionDef : MemberDef
|
|||||||
public bool ReadOnly { get; set; }
|
public bool ReadOnly { get; set; }
|
||||||
public bool Idempotent { get; set; }
|
public bool Idempotent { get; set; }
|
||||||
public bool Cancellable { get; set; }
|
public bool Cancellable { get; set; }
|
||||||
public bool Deprecated { get; set; }
|
public bool Pausable { get; set; }
|
||||||
|
|
||||||
//public FunctionDefFlags Flags { get; set; }
|
//public FunctionDefFlags Flags { get; set; }
|
||||||
public StreamMode StreamMode { get; set; }
|
public StreamMode StreamMode { get; set; }
|
||||||
@@ -175,28 +169,61 @@ public class FunctionDef : MemberDef
|
|||||||
{
|
{
|
||||||
|
|
||||||
var genericRtType = mi.ReturnType.IsGenericType ? mi.ReturnType.GetGenericTypeDefinition() : null;
|
var genericRtType = mi.ReturnType.IsGenericType ? mi.ReturnType.GetGenericTypeDefinition() : null;
|
||||||
|
var streamAttribute = mi.GetCustomAttribute<StreamAttribute>(true);
|
||||||
|
var streamMode = StreamMode.None;
|
||||||
|
var pausable = false;
|
||||||
Tru rtType;
|
Tru rtType;
|
||||||
|
|
||||||
if (genericRtType == typeof(AsyncReply<>))
|
if (streamAttribute != null &&
|
||||||
|
genericRtType != typeof(AsyncReply<>) &&
|
||||||
|
genericRtType != typeof(AsyncStreamReply<>))
|
||||||
|
throw new Exception($"Method `{type.Name}.{mi.Name}` uses StreamAttribute and must return AsyncReply<T> or AsyncStreamReply<T>.");
|
||||||
|
|
||||||
|
if (genericRtType == typeof(IAsyncEnumerable<>))
|
||||||
{
|
{
|
||||||
|
streamMode = StreamMode.Pull;
|
||||||
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
||||||
}
|
}
|
||||||
else if (genericRtType == typeof(Task<>))
|
else if (genericRtType == typeof(IEnumerable<>))
|
||||||
{
|
{
|
||||||
|
streamMode = StreamMode.Push;
|
||||||
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
||||||
}
|
}
|
||||||
else if (genericRtType == typeof(IEnumerable<>) || genericRtType == typeof(IAsyncEnumerable<>))
|
else if (genericRtType == typeof(AsyncStreamReply<>))
|
||||||
|
{
|
||||||
|
if (streamAttribute == null)
|
||||||
|
throw new Exception($"Method `{type.Name}.{mi.Name}` returning AsyncStreamReply<T> must declare StreamAttribute.");
|
||||||
|
|
||||||
|
streamMode = streamAttribute.Mode;
|
||||||
|
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
||||||
|
}
|
||||||
|
else if (streamAttribute != null)
|
||||||
|
{
|
||||||
|
streamMode = streamAttribute.Mode;
|
||||||
|
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
||||||
|
}
|
||||||
|
else if (genericRtType == typeof(AsyncReply<>) || genericRtType == typeof(Task<>))
|
||||||
{
|
{
|
||||||
// get export
|
|
||||||
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
rtType = Tru.FromType(mi.ReturnType.GetGenericArguments()[0], warehouse);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (mi.ReturnType == typeof(Task))
|
rtType = mi.ReturnType == typeof(Task)
|
||||||
rtType = Tru.FromType(null, warehouse);
|
? Tru.FromType(null, warehouse)
|
||||||
else
|
: Tru.FromType(mi.ReturnType, warehouse);
|
||||||
rtType = Tru.FromType(mi.ReturnType, warehouse);
|
}
|
||||||
|
|
||||||
|
if (streamAttribute != null)
|
||||||
|
{
|
||||||
|
if (streamAttribute.Mode != StreamMode.Push && streamAttribute.Mode != StreamMode.Pull)
|
||||||
|
throw new Exception($"Stream method `{type.Name}.{mi.Name}` must use Push or Pull mode.");
|
||||||
|
|
||||||
|
if (streamMode != streamAttribute.Mode)
|
||||||
|
throw new Exception($"Stream mode `{streamAttribute.Mode}` conflicts with return type `{mi.ReturnType}` in method `{type.Name}.{mi.Name}`.");
|
||||||
|
|
||||||
|
pausable = streamAttribute.Pausable;
|
||||||
|
if (pausable && streamMode != StreamMode.Push)
|
||||||
|
throw new Exception($"Only push stream method `{type.Name}.{mi.Name}` can be pausable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rtType == null)
|
if (rtType == null)
|
||||||
@@ -237,7 +264,12 @@ public class FunctionDef : MemberDef
|
|||||||
//var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List<byte>();
|
//var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List<byte>();
|
||||||
//var rtFlags = ((byte[])rtNullableAttr?.NullableFlags ?? new byte[0]).ToList();
|
//var rtFlags = ((byte[])rtNullableAttr?.NullableFlags ?? new byte[0]).ToList();
|
||||||
|
|
||||||
if (rtNullableAttrFlags.Count > 0 && genericRtType == typeof(AsyncReply<>))
|
if (rtNullableAttrFlags.Count > 0 &&
|
||||||
|
(genericRtType == typeof(AsyncReply<>) ||
|
||||||
|
genericRtType == typeof(Task<>) ||
|
||||||
|
genericRtType == typeof(IEnumerable<>) ||
|
||||||
|
genericRtType == typeof(IAsyncEnumerable<>) ||
|
||||||
|
genericRtType == typeof(AsyncStreamReply<>)))
|
||||||
rtNullableAttrFlags.RemoveAt(0);
|
rtNullableAttrFlags.RemoveAt(0);
|
||||||
|
|
||||||
if (rtNullableContextAttrFlag == 2)
|
if (rtNullableContextAttrFlag == 2)
|
||||||
@@ -333,7 +365,7 @@ public class FunctionDef : MemberDef
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FunctionDef()
|
return DefinitionAttributeReader.Apply(mi, new FunctionDef()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Index = index,
|
Index = index,
|
||||||
@@ -342,8 +374,14 @@ public class FunctionDef : MemberDef
|
|||||||
ReturnType = rtType,
|
ReturnType = rtType,
|
||||||
Arguments = arguments,
|
Arguments = arguments,
|
||||||
MethodInfo = mi,
|
MethodInfo = mi,
|
||||||
Annotations = annotations
|
Annotations = annotations,
|
||||||
};
|
ReadOnly = mi.GetCustomAttribute<ReadOnlyAttribute>(true) != null,
|
||||||
|
Idempotent = mi.GetCustomAttribute<IdempotentAttribute>(true) != null,
|
||||||
|
Cancellable = mi.GetCustomAttribute<CancellableAttribute>(true) != null,
|
||||||
|
RatePolicyName = mi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
|
||||||
|
StreamMode = streamMode,
|
||||||
|
Pausable = pausable,
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ namespace Esiur.Data.Types
|
|||||||
ReadOnly = 0x08,
|
ReadOnly = 0x08,
|
||||||
Idempotent = 0x10,
|
Idempotent = 0x10,
|
||||||
Cancellable = 0x20,
|
Cancellable = 0x20,
|
||||||
|
Pausable = 0x40,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ public class LocalTypeDef:TypeDef
|
|||||||
if (genericType == typeof(List<>)
|
if (genericType == typeof(List<>)
|
||||||
|| genericType == typeof(PropertyContext<>)
|
|| genericType == typeof(PropertyContext<>)
|
||||||
|| genericType == typeof(AsyncReply<>)
|
|| genericType == typeof(AsyncReply<>)
|
||||||
|
|| genericType == typeof(Task<>)
|
||||||
|
|| genericType == typeof(IEnumerable<>)
|
||||||
|
|| genericType == typeof(IAsyncEnumerable<>)
|
||||||
|| genericType == typeof(ResourceLink<>))
|
|| genericType == typeof(ResourceLink<>))
|
||||||
{
|
{
|
||||||
return GetDistributedTypes(genericTypeArgs[0]);
|
return GetDistributedTypes(genericTypeArgs[0]);
|
||||||
@@ -273,6 +276,8 @@ public class LocalTypeDef:TypeDef
|
|||||||
|
|
||||||
_typeName = GetTypeName(type);
|
_typeName = GetTypeName(type);
|
||||||
|
|
||||||
|
DefinitionAttributeReader.Apply(type, this);
|
||||||
|
|
||||||
|
|
||||||
warehouse.TryRegisterLocalTypeDef(this);
|
warehouse.TryRegisterLocalTypeDef(this);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ public class MemberDef
|
|||||||
|
|
||||||
public bool Inherited { get; set; }
|
public bool Inherited { get; set; }
|
||||||
|
|
||||||
|
public bool Deprecated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the server-side Warehouse rate policy applied to this member.
|
||||||
|
/// This is local execution metadata and is not sent to remote clients.
|
||||||
|
/// </summary>
|
||||||
|
public string? RatePolicyName { get; set; }
|
||||||
|
|
||||||
public TypeDef Definition { get; set; } = null!;
|
public TypeDef Definition { get; set; } = null!;
|
||||||
|
|
||||||
// Human-readable metadata
|
// Human-readable metadata
|
||||||
@@ -54,6 +62,8 @@ public class MemberDef
|
|||||||
// Compatibility guidance
|
// Compatibility guidance
|
||||||
public string? DeprecationMessage { get; set; }
|
public string? DeprecationMessage { get; set; }
|
||||||
|
|
||||||
|
public Map<string, string>? Annotations { get; set; }
|
||||||
|
|
||||||
public string Fullname =>
|
public string Fullname =>
|
||||||
Definition is null || string.IsNullOrEmpty(Definition.Name)
|
Definition is null || string.IsNullOrEmpty(Definition.Name)
|
||||||
? Name
|
? Name
|
||||||
|
|||||||
@@ -34,5 +34,8 @@ namespace Esiur.Data.Types
|
|||||||
|
|
||||||
// Compatibility guidance
|
// Compatibility guidance
|
||||||
DeprecationMessage = 0x2F, // string
|
DeprecationMessage = 0x2F, // string
|
||||||
|
|
||||||
|
// Extensible application metadata
|
||||||
|
Annotations = 0x30, // Map<string, string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,4 +68,7 @@ public class MemberDefInfo : IndexedStructure
|
|||||||
|
|
||||||
[Index((int)MemberDefField.DeprecationMessage)]
|
[Index((int)MemberDefField.DeprecationMessage)]
|
||||||
public string? DeprecationMessage { get; set; }
|
public string? DeprecationMessage { get; set; }
|
||||||
|
|
||||||
|
[Index((int)MemberDefField.Annotations)]
|
||||||
|
public Map<string, string>? Annotations { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ namespace Esiur.Data.Types;
|
|||||||
|
|
||||||
public class PropertyDef : MemberDef
|
public class PropertyDef : MemberDef
|
||||||
{
|
{
|
||||||
public Map<string, string> Annotations { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public PropertyInfo PropertyInfo
|
public PropertyInfo PropertyInfo
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -52,10 +47,15 @@ public class PropertyDef : MemberDef
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool Constant { get; set; }
|
public bool Constant { get; set; }
|
||||||
|
public bool Volatile { get; set; }
|
||||||
//public bool IsNullable { get; set; }
|
//public bool IsNullable { get; set; }
|
||||||
|
|
||||||
public bool Historical { get; set; }
|
public bool Historical { get; set; }
|
||||||
|
|
||||||
|
public OrderingControl OrderingControl { get; set; }
|
||||||
|
|
||||||
|
public object? DefaultValue { get; set; }
|
||||||
|
|
||||||
public bool HasHistory
|
public bool HasHistory
|
||||||
{
|
{
|
||||||
get => Historical;
|
get => Historical;
|
||||||
@@ -258,6 +258,10 @@ public class PropertyDef : MemberDef
|
|||||||
|
|
||||||
var annotationAttrs = pi.GetCustomAttributes<AnnotationAttribute>(true);
|
var annotationAttrs = pi.GetCustomAttributes<AnnotationAttribute>(true);
|
||||||
var historicalAttr = pi.GetCustomAttribute<HistoricalAttribute>(true);
|
var historicalAttr = pi.GetCustomAttribute<HistoricalAttribute>(true);
|
||||||
|
var readOnlyAttr = pi.GetCustomAttribute<ReadOnlyAttribute>(true);
|
||||||
|
var volatileAttr = pi.GetCustomAttribute<VolatileAttribute>(true);
|
||||||
|
var orderingAttr = pi.GetCustomAttribute<OrderingAttribute>(true);
|
||||||
|
var defaultValueAttr = pi.GetCustomAttribute<System.ComponentModel.DefaultValueAttribute>(true);
|
||||||
|
|
||||||
//var nullabilityContext = new NullabilityInfoContext();
|
//var nullabilityContext = new NullabilityInfoContext();
|
||||||
//propType.Nullable = nullabilityContext.Create(pi).ReadState is NullabilityState.Nullable;
|
//propType.Nullable = nullabilityContext.Create(pi).ReadState is NullabilityState.Nullable;
|
||||||
@@ -306,16 +310,21 @@ public class PropertyDef : MemberDef
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new PropertyDef()
|
return DefinitionAttributeReader.Apply(pi, new PropertyDef()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Index = index,
|
Index = index,
|
||||||
Inherited = pi.DeclaringType != type,
|
Inherited = pi.DeclaringType != type,
|
||||||
ValueType = propType,
|
ValueType = propType,
|
||||||
PropertyInfo = pi,
|
PropertyInfo = pi,
|
||||||
Historical = historicalAttr == null,
|
RatePolicyName = pi.GetCustomAttribute<RateControlAttribute>(true)?.PolicyName,
|
||||||
|
ReadOnly = readOnlyAttr != null,
|
||||||
|
Volatile = volatileAttr != null,
|
||||||
|
Historical = historicalAttr != null,
|
||||||
|
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
|
||||||
|
DefaultValue = defaultValueAttr?.Value,
|
||||||
Annotations = annotations,
|
Annotations = annotations,
|
||||||
};
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -325,7 +334,7 @@ public class PropertyDef : MemberDef
|
|||||||
PropertyPermission permission, TypeDef typeDef)
|
PropertyPermission permission, TypeDef typeDef)
|
||||||
{
|
{
|
||||||
var definition = MakePropertyDef(warehouse, type, pi, name, index, typeDef);
|
var definition = MakePropertyDef(warehouse, type, pi, name, index, typeDef);
|
||||||
definition.Permission = permission;
|
definition.ReadOnly = definition.ReadOnly || permission == PropertyPermission.Read;
|
||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,11 @@ public class RemoteTypeDef:TypeDef
|
|||||||
: $"{info.Namespace}.{info.Name}";
|
: $"{info.Namespace}.{info.Name}";
|
||||||
definition._typeDefKind = info.Kind;
|
definition._typeDefKind = info.Kind;
|
||||||
definition._version = info.Version;
|
definition._version = info.Version;
|
||||||
|
definition.Usage = info.Usage;
|
||||||
|
definition.Description = info.Description;
|
||||||
|
definition.Example = info.Example;
|
||||||
|
definition.Category = info.Category;
|
||||||
|
definition.Since = info.Since;
|
||||||
definition.Annotations = info.Annotations;
|
definition.Annotations = info.Annotations;
|
||||||
|
|
||||||
definition._properties.Clear();
|
definition._properties.Clear();
|
||||||
@@ -218,6 +223,7 @@ public class RemoteTypeDef:TypeDef
|
|||||||
member.Index = info.Index;
|
member.Index = info.Index;
|
||||||
member.Name = info.Name;
|
member.Name = info.Name;
|
||||||
member.Inherited = (info.Flags & (byte)MemberDefFlags.Inherited) != 0;
|
member.Inherited = (info.Flags & (byte)MemberDefFlags.Inherited) != 0;
|
||||||
|
member.Deprecated = (info.Flags & (byte)MemberDefFlags.Deprecated) != 0;
|
||||||
member.Description = info.Description;
|
member.Description = info.Description;
|
||||||
member.Usage = info.Usage;
|
member.Usage = info.Usage;
|
||||||
member.Examples = info.Examples;
|
member.Examples = info.Examples;
|
||||||
@@ -234,6 +240,7 @@ public class RemoteTypeDef:TypeDef
|
|||||||
member.Warnings = info.Warnings;
|
member.Warnings = info.Warnings;
|
||||||
member.RelatedMembers = info.RelatedMembers;
|
member.RelatedMembers = info.RelatedMembers;
|
||||||
member.DeprecationMessage = info.DeprecationMessage;
|
member.DeprecationMessage = info.DeprecationMessage;
|
||||||
|
member.Annotations = info.Annotations;
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +252,10 @@ public class RemoteTypeDef:TypeDef
|
|||||||
ValueType = info.ValueType,
|
ValueType = info.ValueType,
|
||||||
ReadOnly = flags.HasFlag(PropertyDefFlags.ReadOnly),
|
ReadOnly = flags.HasFlag(PropertyDefFlags.ReadOnly),
|
||||||
Constant = flags.HasFlag(PropertyDefFlags.Constant),
|
Constant = flags.HasFlag(PropertyDefFlags.Constant),
|
||||||
|
Volatile = flags.HasFlag(PropertyDefFlags.Volatile),
|
||||||
Historical = flags.HasFlag(PropertyDefFlags.Historical) || info.HistoryControl != 0,
|
Historical = flags.HasFlag(PropertyDefFlags.Historical) || info.HistoryControl != 0,
|
||||||
|
OrderingControl = info.OrderingControl,
|
||||||
|
DefaultValue = info.DefaultValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +270,7 @@ public class RemoteTypeDef:TypeDef
|
|||||||
ReadOnly = flags.HasFlag(FunctionDefFlags.ReadOnly),
|
ReadOnly = flags.HasFlag(FunctionDefFlags.ReadOnly),
|
||||||
Idempotent = flags.HasFlag(FunctionDefFlags.Idempotent),
|
Idempotent = flags.HasFlag(FunctionDefFlags.Idempotent),
|
||||||
Cancellable = flags.HasFlag(FunctionDefFlags.Cancellable),
|
Cancellable = flags.HasFlag(FunctionDefFlags.Cancellable),
|
||||||
Deprecated = flags.HasFlag(FunctionDefFlags.Deprecated),
|
Pausable = flags.HasFlag(FunctionDefFlags.Pausable),
|
||||||
Arguments = info.Arguments?.Select(ToArgument).ToArray() ?? Array.Empty<ArgumentDef>(),
|
Arguments = info.Arguments?.Select(ToArgument).ToArray() ?? Array.Empty<ArgumentDef>(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -275,6 +285,7 @@ public class RemoteTypeDef:TypeDef
|
|||||||
Optional = flags.HasFlag(ArgumentDefFlags.Optional),
|
Optional = flags.HasFlag(ArgumentDefFlags.Optional),
|
||||||
Type = info.ValueType,
|
Type = info.ValueType,
|
||||||
DefaultValue = info.DefaultValue,
|
DefaultValue = info.DefaultValue,
|
||||||
|
Annotations = info.Annotations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +296,8 @@ public class RemoteTypeDef:TypeDef
|
|||||||
{
|
{
|
||||||
ArgumentType = info.ArgumentType,
|
ArgumentType = info.ArgumentType,
|
||||||
AutoDelivered = flags.HasFlag(EventDefFlags.AutoDelivered),
|
AutoDelivered = flags.HasFlag(EventDefFlags.AutoDelivered),
|
||||||
Deprecated = flags.HasFlag(EventDefFlags.Deprecated),
|
Historical = flags.HasFlag(EventDefFlags.Historical) || info.HistoryControl != 0,
|
||||||
|
OrderingControl = info.OrderingControl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ public class TypeDef
|
|||||||
|
|
||||||
public Map<string, string> Annotations { get; set; }
|
public Map<string, string> Annotations { get; set; }
|
||||||
|
|
||||||
|
public string? Usage { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public object? Example { get; set; }
|
||||||
|
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
public string? Since { get; set; }
|
||||||
|
|
||||||
protected string _typeName;
|
protected string _typeName;
|
||||||
protected List<FunctionDef> _functions = new List<FunctionDef>();
|
protected List<FunctionDef> _functions = new List<FunctionDef>();
|
||||||
protected List<EventDef> _events = new List<EventDef>();
|
protected List<EventDef> _events = new List<EventDef>();
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ public class TypeDefInfo : IndexedStructure
|
|||||||
Name = definition.Name,
|
Name = definition.Name,
|
||||||
Kind = definition.Kind,
|
Kind = definition.Kind,
|
||||||
Parent = definition.ParentTypeId,
|
Parent = definition.ParentTypeId,
|
||||||
|
Usage = definition.Usage,
|
||||||
|
Description = definition.Description,
|
||||||
|
Example = definition.Example,
|
||||||
|
Category = definition.Category,
|
||||||
|
Since = definition.Since,
|
||||||
Annotations = definition.Annotations,
|
Annotations = definition.Annotations,
|
||||||
Properties = definition.Properties.Select(FromProperty).ToList(),
|
Properties = definition.Properties.Select(FromProperty).ToList(),
|
||||||
Functions = definition.Functions.Select(FromFunction).ToList(),
|
Functions = definition.Functions.Select(FromFunction).ToList(),
|
||||||
@@ -94,21 +99,26 @@ public class TypeDefInfo : IndexedStructure
|
|||||||
target.Warnings = source.Warnings;
|
target.Warnings = source.Warnings;
|
||||||
target.RelatedMembers = source.RelatedMembers;
|
target.RelatedMembers = source.RelatedMembers;
|
||||||
target.DeprecationMessage = source.DeprecationMessage;
|
target.DeprecationMessage = source.DeprecationMessage;
|
||||||
|
target.Annotations = source.Annotations;
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PropertyDefInfo FromProperty(PropertyDef source)
|
private static PropertyDefInfo FromProperty(PropertyDef source)
|
||||||
{
|
{
|
||||||
var flags = source.Inherited ? PropertyDefFlags.Inherited : PropertyDefFlags.None;
|
var flags = source.Inherited ? PropertyDefFlags.Inherited : PropertyDefFlags.None;
|
||||||
|
if (source.Deprecated) flags |= PropertyDefFlags.Deprecated;
|
||||||
if (source.ReadOnly) flags |= PropertyDefFlags.ReadOnly;
|
if (source.ReadOnly) flags |= PropertyDefFlags.ReadOnly;
|
||||||
if (source.Constant) flags |= PropertyDefFlags.Constant;
|
if (source.Constant) flags |= PropertyDefFlags.Constant;
|
||||||
|
if (source.Volatile) flags |= PropertyDefFlags.Volatile;
|
||||||
if (source.Historical) flags |= PropertyDefFlags.Historical;
|
if (source.Historical) flags |= PropertyDefFlags.Historical;
|
||||||
|
|
||||||
return CopyMember(source, new PropertyDefInfo
|
return CopyMember(source, new PropertyDefInfo
|
||||||
{
|
{
|
||||||
Flags = (byte)flags,
|
Flags = (byte)flags,
|
||||||
ValueType = source.ValueType,
|
ValueType = source.ValueType,
|
||||||
|
OrderingControl = source.OrderingControl,
|
||||||
HistoryControl = source.Historical ? (byte)1 : (byte)0,
|
HistoryControl = source.Historical ? (byte)1 : (byte)0,
|
||||||
|
DefaultValue = source.DefaultValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +130,7 @@ public class TypeDefInfo : IndexedStructure
|
|||||||
if (source.ReadOnly) flags |= FunctionDefFlags.ReadOnly;
|
if (source.ReadOnly) flags |= FunctionDefFlags.ReadOnly;
|
||||||
if (source.Idempotent) flags |= FunctionDefFlags.Idempotent;
|
if (source.Idempotent) flags |= FunctionDefFlags.Idempotent;
|
||||||
if (source.Cancellable) flags |= FunctionDefFlags.Cancellable;
|
if (source.Cancellable) flags |= FunctionDefFlags.Cancellable;
|
||||||
|
if (source.Pausable) flags |= FunctionDefFlags.Pausable;
|
||||||
|
|
||||||
return CopyMember(source, new FunctionDefInfo
|
return CopyMember(source, new FunctionDefInfo
|
||||||
{
|
{
|
||||||
@@ -139,6 +150,7 @@ public class TypeDefInfo : IndexedStructure
|
|||||||
Flags = source.Optional ? (byte)ArgumentDefFlags.Optional : (byte)ArgumentDefFlags.None,
|
Flags = source.Optional ? (byte)ArgumentDefFlags.Optional : (byte)ArgumentDefFlags.None,
|
||||||
ValueType = source.Type,
|
ValueType = source.Type,
|
||||||
DefaultValue = source.DefaultValue,
|
DefaultValue = source.DefaultValue,
|
||||||
|
Annotations = source.Annotations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,17 +159,21 @@ public class TypeDefInfo : IndexedStructure
|
|||||||
var flags = source.Inherited ? EventDefFlags.Inherited : EventDefFlags.None;
|
var flags = source.Inherited ? EventDefFlags.Inherited : EventDefFlags.None;
|
||||||
if (source.Deprecated) flags |= EventDefFlags.Deprecated;
|
if (source.Deprecated) flags |= EventDefFlags.Deprecated;
|
||||||
if (source.AutoDelivered) flags |= EventDefFlags.AutoDelivered;
|
if (source.AutoDelivered) flags |= EventDefFlags.AutoDelivered;
|
||||||
|
if (source.Historical) flags |= EventDefFlags.Historical;
|
||||||
|
|
||||||
return CopyMember(source, new EventDefInfo
|
return CopyMember(source, new EventDefInfo
|
||||||
{
|
{
|
||||||
Flags = (byte)flags,
|
Flags = (byte)flags,
|
||||||
ArgumentType = source.ArgumentType,
|
ArgumentType = source.ArgumentType,
|
||||||
|
OrderingControl = source.OrderingControl,
|
||||||
|
HistoryControl = source.Historical ? (byte)1 : (byte)0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConstantDefInfo FromConstant(ConstantDef source)
|
private static ConstantDefInfo FromConstant(ConstantDef source)
|
||||||
{
|
{
|
||||||
var flags = source.Inherited ? ConstantDefFlags.Inherited : ConstantDefFlags.None;
|
var flags = source.Inherited ? ConstantDefFlags.Inherited : ConstantDefFlags.None;
|
||||||
|
if (source.Deprecated) flags |= ConstantDefFlags.Deprecated;
|
||||||
return CopyMember(source, new ConstantDefInfo
|
return CopyMember(source, new ConstantDefInfo
|
||||||
{
|
{
|
||||||
Flags = (byte)flags,
|
Flags = (byte)flags,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
|||||||
|
|
||||||
if (s == null)
|
if (s == null)
|
||||||
{
|
{
|
||||||
Global.Log("NetworkServer", LogType.Error, "sock == null");
|
//Global.Log("NetworkServer", LogType.Error, "sock == null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace Esiur.Net.Packets
|
|||||||
PullStream = 0x1C,
|
PullStream = 0x1C,
|
||||||
TerminateExecution = 0x1D,
|
TerminateExecution = 0x1D,
|
||||||
HaltExecution = 0x1E,
|
HaltExecution = 0x1E,
|
||||||
|
ResumeExecution = 0x1F,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,6 +454,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
|
|
||||||
public override void Destroy()
|
public override void Destroy()
|
||||||
{
|
{
|
||||||
|
TerminateInvocations();
|
||||||
UnsubscribeAll();
|
UnsubscribeAll();
|
||||||
this.OnReady = null;
|
this.OnReady = null;
|
||||||
this.OnError = null;
|
this.OnError = null;
|
||||||
@@ -477,7 +478,9 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
{
|
{
|
||||||
offset += (uint)rt;
|
offset += (uint)rt;
|
||||||
|
|
||||||
if (_packet.Tdu == null)
|
if (_packet.Tdu == null &&
|
||||||
|
_packet.Method != EpPacketMethod.Reply &&
|
||||||
|
_packet.Method != EpPacketMethod.Extension)
|
||||||
return offset;
|
return offset;
|
||||||
|
|
||||||
#if VERBOSE
|
#if VERBOSE
|
||||||
@@ -514,6 +517,9 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
}
|
}
|
||||||
else if (_packet.Method == EpPacketMethod.Request)
|
else if (_packet.Method == EpPacketMethod.Request)
|
||||||
{
|
{
|
||||||
|
if (IsRateControlBlocked)
|
||||||
|
return offset;
|
||||||
|
|
||||||
var dt = _packet.Tdu.Value;
|
var dt = _packet.Tdu.Value;
|
||||||
|
|
||||||
switch (_packet.Request)
|
switch (_packet.Request)
|
||||||
@@ -582,37 +588,58 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
case EpPacketRequest.StaticCall:
|
case EpPacketRequest.StaticCall:
|
||||||
EpRequestStaticCall(_packet.CallbackId, dt);
|
EpRequestStaticCall(_packet.CallbackId, dt);
|
||||||
break;
|
break;
|
||||||
|
case EpPacketRequest.PullStream:
|
||||||
|
EpRequestPullStream(_packet.CallbackId, dt);
|
||||||
|
break;
|
||||||
|
case EpPacketRequest.TerminateExecution:
|
||||||
|
EpRequestTerminateExecution(_packet.CallbackId, dt);
|
||||||
|
break;
|
||||||
|
case EpPacketRequest.HaltExecution:
|
||||||
|
EpRequestHaltExecution(_packet.CallbackId, dt);
|
||||||
|
break;
|
||||||
|
case EpPacketRequest.ResumeExecution:
|
||||||
|
EpRequestResumeExecution(_packet.CallbackId, dt);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_packet.Method == EpPacketMethod.Reply)
|
else if (_packet.Method == EpPacketMethod.Reply)
|
||||||
{
|
{
|
||||||
var dt = _packet.Tdu.Value;
|
var dt = _packet.Tdu;
|
||||||
|
|
||||||
switch (_packet.Reply)
|
switch (_packet.Reply)
|
||||||
{
|
{
|
||||||
case EpPacketReply.Completed:
|
case EpPacketReply.Completed:
|
||||||
EpReplyCompleted(_packet.CallbackId, dt);
|
EpReplyCompleted(_packet.CallbackId, dt);
|
||||||
break;
|
break;
|
||||||
|
case EpPacketReply.Stream:
|
||||||
|
EpReplyStream(_packet.CallbackId);
|
||||||
|
break;
|
||||||
case EpPacketReply.Propagated:
|
case EpPacketReply.Propagated:
|
||||||
EpReplyPropagated(_packet.CallbackId, dt);
|
if (dt.HasValue)
|
||||||
|
EpReplyPropagated(_packet.CallbackId, dt.Value);
|
||||||
break;
|
break;
|
||||||
case EpPacketReply.PermissionError:
|
case EpPacketReply.PermissionError:
|
||||||
EpReplyError(_packet.CallbackId, dt, ErrorType.Management);
|
if (dt.HasValue)
|
||||||
|
EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Management);
|
||||||
break;
|
break;
|
||||||
case EpPacketReply.ExecutionError:
|
case EpPacketReply.ExecutionError:
|
||||||
EpReplyError(_packet.CallbackId, dt, ErrorType.Exception);
|
if (dt.HasValue)
|
||||||
|
EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Exception);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EpPacketReply.Progress:
|
case EpPacketReply.Progress:
|
||||||
EpReplyProgress(_packet.CallbackId, dt);
|
if (dt.HasValue)
|
||||||
|
EpReplyProgress(_packet.CallbackId, dt.Value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EpPacketReply.Chunk:
|
case EpPacketReply.Chunk:
|
||||||
EpReplyChunk(_packet.CallbackId, dt);
|
if (dt.HasValue)
|
||||||
|
EpReplyChunk(_packet.CallbackId, dt.Value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EpPacketReply.Warning:
|
case EpPacketReply.Warning:
|
||||||
EpReplyWarning(_packet.Extension, dt);
|
if (dt.HasValue)
|
||||||
|
EpReplyWarning(_packet.CallbackId, dt.Value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2102,6 +2129,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
protected override void Disconnected()
|
protected override void Disconnected()
|
||||||
{
|
{
|
||||||
// clean up
|
// clean up
|
||||||
|
TerminateInvocations();
|
||||||
_authenticated = false;
|
_authenticated = false;
|
||||||
Status = EpConnectionStatus.Closed;
|
Status = EpConnectionStatus.Closed;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ using Esiur.Net.Packets;
|
|||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.RateLimiting;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
@@ -47,6 +48,140 @@ namespace Esiur.Protocol;
|
|||||||
|
|
||||||
partial class EpConnection
|
partial class EpConnection
|
||||||
{
|
{
|
||||||
|
readonly object _rateControlDenialsLock = new object();
|
||||||
|
DateTime _rateControlDenialWindowStarted;
|
||||||
|
int _rateControlDenials;
|
||||||
|
int _rateControlBlocked;
|
||||||
|
|
||||||
|
internal bool IsRateControlBlocked => Volatile.Read(ref _rateControlBlocked) != 0;
|
||||||
|
|
||||||
|
bool TryApplyRateControl(
|
||||||
|
MemberDef member,
|
||||||
|
IResource? resource,
|
||||||
|
ActionType action,
|
||||||
|
uint callback,
|
||||||
|
out TimeSpan delay)
|
||||||
|
{
|
||||||
|
delay = TimeSpan.Zero;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(member.RatePolicyName))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (IsRateControlBlocked)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var warehouse = Instance.Warehouse;
|
||||||
|
var policy = warehouse.TryGetRatePolicy(member.RatePolicyName);
|
||||||
|
|
||||||
|
if (policy == null)
|
||||||
|
{
|
||||||
|
DenyRateControlledRequest(
|
||||||
|
callback,
|
||||||
|
$"Rate policy `{member.RatePolicyName}` is not registered.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = new RateControlContext(
|
||||||
|
warehouse,
|
||||||
|
this,
|
||||||
|
_session,
|
||||||
|
resource,
|
||||||
|
member,
|
||||||
|
action);
|
||||||
|
|
||||||
|
if (policy.Applicable(context) == Ruling.Denied)
|
||||||
|
{
|
||||||
|
DenyRateControlledRequest(
|
||||||
|
callback,
|
||||||
|
$"Rate policy `{member.RatePolicyName}` denied `{member.Fullname}`.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = context.Delay > TimeSpan.Zero ? context.Delay : TimeSpan.Zero;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
DenyRateControlledRequest(
|
||||||
|
callback,
|
||||||
|
$"Rate policy `{member.RatePolicyName}` failed: {exception.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DenyRateControlledRequest(uint callback, string message)
|
||||||
|
{
|
||||||
|
var configuration = Instance.Warehouse.Configuration.RateControl;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var shouldBlock = false;
|
||||||
|
|
||||||
|
lock (_rateControlDenialsLock)
|
||||||
|
{
|
||||||
|
var window = configuration.DenialWindow > TimeSpan.Zero
|
||||||
|
? configuration.DenialWindow
|
||||||
|
: TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
if (_rateControlDenialWindowStarted == default ||
|
||||||
|
now - _rateControlDenialWindowStarted > window)
|
||||||
|
{
|
||||||
|
_rateControlDenialWindowStarted = now;
|
||||||
|
_rateControlDenials = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rateControlDenials++;
|
||||||
|
shouldBlock = configuration.DenialsBeforeConnectionBlock > 0 &&
|
||||||
|
_rateControlDenials >= configuration.DenialsBeforeConnectionBlock &&
|
||||||
|
Interlocked.Exchange(ref _rateControlBlocked, 1) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendError(
|
||||||
|
ErrorType.Management,
|
||||||
|
callback,
|
||||||
|
(ushort)ExceptionCode.RateLimitExceeded,
|
||||||
|
message);
|
||||||
|
|
||||||
|
if (shouldBlock)
|
||||||
|
_ = CloseRateControlledConnectionAsync(configuration.ConnectionBlockDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task CloseRateControlledConnectionAsync(TimeSpan delay)
|
||||||
|
{
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExecuteRateControlled(uint callback, TimeSpan delay, Action action)
|
||||||
|
{
|
||||||
|
if (delay <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ExecuteRateControlledAsync(callback, delay, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ExecuteRateControlledAsync(uint callback, TimeSpan delay, Action action)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!IsConnected || IsRateControlBlocked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.GeneralFailure, exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KeyList<ulong, RemoteTypeDef> _neededTypeDefs = new KeyList<ulong, RemoteTypeDef>();
|
KeyList<ulong, RemoteTypeDef> _neededTypeDefs = new KeyList<ulong, RemoteTypeDef>();
|
||||||
KeyList<ulong, RemoteTypeDef> _cachedTypeDefs = new KeyList<ulong, RemoteTypeDef>();
|
KeyList<ulong, RemoteTypeDef> _cachedTypeDefs = new KeyList<ulong, RemoteTypeDef>();
|
||||||
KeyList<ulong, FetchRequestInfo<RemoteTypeDef, ulong>> _typeDefRequests = new KeyList<ulong, FetchRequestInfo<RemoteTypeDef, ulong>>();
|
KeyList<ulong, FetchRequestInfo<RemoteTypeDef, ulong>> _typeDefRequests = new KeyList<ulong, FetchRequestInfo<RemoteTypeDef, ulong>>();
|
||||||
@@ -94,6 +229,9 @@ partial class EpConnection
|
|||||||
|
|
||||||
KeyList<uint, AsyncReply> _requests = new KeyList<uint, AsyncReply>();
|
KeyList<uint, AsyncReply> _requests = new KeyList<uint, AsyncReply>();
|
||||||
|
|
||||||
|
readonly object _invocationsLock = new object();
|
||||||
|
readonly Dictionary<uint, InvocationContext> _invocations = new Dictionary<uint, InvocationContext>();
|
||||||
|
|
||||||
volatile int _callbackCounter = 0;
|
volatile int _callbackCounter = 0;
|
||||||
|
|
||||||
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
|
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
|
||||||
@@ -125,18 +263,25 @@ partial class EpConnection
|
|||||||
//callbackCounter++; // avoid thread racing
|
//callbackCounter++; // avoid thread racing
|
||||||
_requests.Add(c, reply);
|
_requests.Add(c, reply);
|
||||||
|
|
||||||
|
SendRequestPacket(action, c, args);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendRequestPacket(EpPacketRequest action, uint callbackId, object[] args)
|
||||||
|
{
|
||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x40 | (byte)action))
|
bl.AddUInt8((byte)(0x40 | (byte)action))
|
||||||
.AddUInt32(c);
|
.AddUInt32(callbackId);
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
if (args.Length == 1)
|
if (args.Length == 1)
|
||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x60 | (byte)action))
|
bl.AddUInt8((byte)(0x60 | (byte)action))
|
||||||
.AddUInt32(c)
|
.AddUInt32(callbackId)
|
||||||
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
|
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
@@ -144,11 +289,39 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x60 | (byte)action))
|
bl.AddUInt8((byte)(0x60 | (byte)action))
|
||||||
.AddUInt32(c)
|
.AddUInt32(callbackId)
|
||||||
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncStreamReply SendStreamRequest(StreamMode streamMode, EpPacketRequest action, params object[] args)
|
||||||
|
{
|
||||||
|
var callbackId = (uint)Interlocked.Increment(ref _callbackCounter);
|
||||||
|
var reply = new AsyncStreamReply(
|
||||||
|
streamMode,
|
||||||
|
() => SendRequest(EpPacketRequest.PullStream, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.TerminateExecution, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.HaltExecution, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
||||||
|
|
||||||
|
_requests.Add(callbackId, reply);
|
||||||
|
SendRequestPacket(action, callbackId, args);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncStreamReply<T> SendStreamRequest<T>(StreamMode streamMode, EpPacketRequest action, params object[] args)
|
||||||
|
{
|
||||||
|
var callbackId = (uint)Interlocked.Increment(ref _callbackCounter);
|
||||||
|
var reply = new AsyncStreamReply<T>(
|
||||||
|
streamMode,
|
||||||
|
() => SendRequest(EpPacketRequest.PullStream, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.TerminateExecution, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.HaltExecution, callbackId),
|
||||||
|
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
|
||||||
|
|
||||||
|
_requests.Add(callbackId, reply);
|
||||||
|
SendRequestPacket(action, callbackId, args);
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +477,16 @@ partial class EpConnection
|
|||||||
return SendRequest(EpPacketRequest.StaticCall, typeId, index, parameters);
|
return SendRequest(EpPacketRequest.StaticCall, typeId, index, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AsyncStreamReply<T> StaticStreamCall<T>(ulong typeId, byte index, object parameters, StreamMode streamMode)
|
||||||
|
{
|
||||||
|
return SendStreamRequest<T>(streamMode, EpPacketRequest.StaticCall, typeId, index, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncStreamReply StaticStreamCall(ulong typeId, byte index, object parameters, StreamMode streamMode)
|
||||||
|
{
|
||||||
|
return SendStreamRequest(streamMode, EpPacketRequest.StaticCall, typeId, index, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
public AsyncReply Call(string procedureCall, params object[] parameters)
|
public AsyncReply Call(string procedureCall, params object[] parameters)
|
||||||
{
|
{
|
||||||
//var args = new Map<byte, object>();
|
//var args = new Map<byte, object>();
|
||||||
@@ -324,6 +507,16 @@ partial class EpConnection
|
|||||||
return SendRequest(EpPacketRequest.InvokeFunction, instanceId, index, parameters);
|
return SendRequest(EpPacketRequest.InvokeFunction, instanceId, index, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal AsyncStreamReply SendStreamInvoke(uint instanceId, byte index, object parameters, StreamMode streamMode)
|
||||||
|
{
|
||||||
|
return SendStreamRequest(streamMode, EpPacketRequest.InvokeFunction, instanceId, index, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AsyncStreamReply<T> SendStreamInvoke<T>(uint instanceId, byte index, object parameters, StreamMode streamMode)
|
||||||
|
{
|
||||||
|
return SendStreamRequest<T>(streamMode, EpPacketRequest.InvokeFunction, instanceId, index, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
internal AsyncReply SendSetProperty(uint instanceId, byte index, object value)
|
internal AsyncReply SendSetProperty(uint instanceId, byte index, object value)
|
||||||
{
|
{
|
||||||
return SendRequest(EpPacketRequest.SetProperty, instanceId, index, value);
|
return SendRequest(EpPacketRequest.SetProperty, instanceId, index, value);
|
||||||
@@ -383,7 +576,7 @@ partial class EpConnection
|
|||||||
SendReply(EpPacketReply.Chunk, callbackId, chunk);
|
SendReply(EpPacketReply.Chunk, callbackId, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpReplyCompleted(uint callbackId, PlainTdu tdu)
|
void EpReplyCompleted(uint callbackId, PlainTdu? tdu)
|
||||||
{
|
{
|
||||||
var req = _requests.Take(callbackId);
|
var req = _requests.Take(callbackId);
|
||||||
|
|
||||||
@@ -395,7 +588,19 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pr = Codec.Parse(tdu, this, null);
|
if (req is AsyncStreamReply streamReply)
|
||||||
|
{
|
||||||
|
streamReply.TriggerStreamCompleted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tdu == null)
|
||||||
|
{
|
||||||
|
req.Trigger(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pr = Codec.Parse(tdu.Value, this, null);
|
||||||
|
|
||||||
if (pr is AsyncReply asyncReply)
|
if (pr is AsyncReply asyncReply)
|
||||||
{
|
{
|
||||||
@@ -421,6 +626,12 @@ partial class EpConnection
|
|||||||
//}).Error(req.TriggerError);
|
//}).Error(req.TriggerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpReplyStream(uint callbackId)
|
||||||
|
{
|
||||||
|
if (_requests[callbackId] is AsyncStreamReply streamReply)
|
||||||
|
streamReply.TriggerStreamStarted();
|
||||||
|
}
|
||||||
|
|
||||||
void EpExtensionAction(byte actionId, PlainTdu? tdu)
|
void EpExtensionAction(byte actionId, PlainTdu? tdu)
|
||||||
{
|
{
|
||||||
// nothing is supported now
|
// nothing is supported now
|
||||||
@@ -1419,6 +1630,22 @@ partial class EpConnection
|
|||||||
|
|
||||||
|
|
||||||
void InvokeFunction(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
|
void InvokeFunction(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
|
||||||
|
{
|
||||||
|
if (!TryApplyRateControl(
|
||||||
|
ft,
|
||||||
|
target as IResource,
|
||||||
|
ActionType.Execute,
|
||||||
|
callback,
|
||||||
|
out var delay))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ExecuteRateControlled(
|
||||||
|
callback,
|
||||||
|
delay,
|
||||||
|
() => InvokeFunctionCore(ft, callback, arguments, actionType, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvokeFunctionCore(FunctionDef ft, uint callback, object arguments, EpPacketRequest actionType, object target = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
// cast arguments
|
// cast arguments
|
||||||
@@ -1559,59 +1786,44 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rt is IAsyncEnumerable<object>)
|
if (ft.StreamMode != StreamMode.None)
|
||||||
{
|
{
|
||||||
var enu = rt as IAsyncEnumerable<object>;
|
context ??= new InvocationContext(this, callback);
|
||||||
var enumerator = enu.GetAsyncEnumerator();
|
context.InitializeStream(ft.StreamMode, ft.Pausable);
|
||||||
Task.Run(async () =>
|
|
||||||
|
if (ft.StreamMode == StreamMode.Pull && context.SetAsyncEnumerable(rt))
|
||||||
{
|
{
|
||||||
try
|
RegisterInvocation(callback, context);
|
||||||
{
|
SendReply(EpPacketReply.Stream, callback);
|
||||||
while (await enumerator.MoveNextAsync())
|
}
|
||||||
|
else if (ft.StreamMode == StreamMode.Push && rt is System.Collections.IEnumerable enumerable)
|
||||||
|
{
|
||||||
|
context.SetEnumerable(enumerable);
|
||||||
|
RegisterInvocation(callback, context);
|
||||||
|
SendReply(EpPacketReply.Stream, callback);
|
||||||
|
PumpEnumerable(callback, context);
|
||||||
|
}
|
||||||
|
else if (ft.StreamMode == StreamMode.Push && rt is AsyncReply streamSource)
|
||||||
|
{
|
||||||
|
RegisterInvocation(callback, context);
|
||||||
|
SendReply(EpPacketReply.Stream, callback);
|
||||||
|
|
||||||
|
streamSource.Then(_ => CompleteInvocation(callback, context))
|
||||||
|
.Error(ex => FailInvocation(callback, context, ex))
|
||||||
|
.Progress((pt, pv, pm) => SendProgress(callback, pv, pm))
|
||||||
|
.Chunk(value =>
|
||||||
{
|
{
|
||||||
var v = enumerator.Current;
|
if (!context.Ended)
|
||||||
SendChunk(callback, v);
|
context.Chunk(value);
|
||||||
}
|
})
|
||||||
|
.Warning((level, message) => SendWarning(callback, level, message));
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
|
||||||
|
|
||||||
if (context != null)
|
|
||||||
context.Ended = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (context != null)
|
|
||||||
context.Ended = true;
|
|
||||||
|
|
||||||
var (code, msg) = SummerizeException(ex);
|
|
||||||
SendError(ErrorType.Exception, callback, code, msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (rt is System.Collections.IEnumerable && !(rt is Array || rt is Map<string, object> || rt is string))
|
|
||||||
{
|
|
||||||
var enu = rt as System.Collections.IEnumerable;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var v in enu)
|
|
||||||
SendChunk(callback, v);
|
|
||||||
|
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
|
||||||
|
|
||||||
if (context != null)
|
|
||||||
context.Ended = true;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
if (context != null)
|
context.Ended = true;
|
||||||
context.Ended = true;
|
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.NotSupported,
|
||||||
|
$"Stream mode `{ft.StreamMode}` is not compatible with `{rt?.GetType()}`.");
|
||||||
var (code, msg) = SummerizeException(ex);
|
|
||||||
SendError(ErrorType.Exception, callback, code, msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (rt is Task)
|
else if (rt is Task)
|
||||||
{
|
{
|
||||||
@@ -1663,6 +1875,217 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RegisterInvocation(uint callback, InvocationContext context)
|
||||||
|
{
|
||||||
|
lock (_invocationsLock)
|
||||||
|
_invocations[callback] = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
InvocationContext GetInvocation(uint callback)
|
||||||
|
{
|
||||||
|
lock (_invocationsLock)
|
||||||
|
return _invocations.TryGetValue(callback, out var context) ? context : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TerminateInvocations()
|
||||||
|
{
|
||||||
|
InvocationContext[] contexts;
|
||||||
|
|
||||||
|
lock (_invocationsLock)
|
||||||
|
{
|
||||||
|
contexts = _invocations.Values.ToArray();
|
||||||
|
_invocations.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var context in contexts)
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.TerminateAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// The connection is already closing; disposal is best effort.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TakeInvocation(uint callback, InvocationContext expected, out InvocationContext context)
|
||||||
|
{
|
||||||
|
lock (_invocationsLock)
|
||||||
|
{
|
||||||
|
if (!_invocations.TryGetValue(callback, out context) ||
|
||||||
|
(expected != null && !ReferenceEquals(expected, context)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_invocations.Remove(callback);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async void CompleteInvocation(uint callback, InvocationContext expected)
|
||||||
|
{
|
||||||
|
if (!TakeInvocation(callback, expected, out var context))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.EndAsync();
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var (code, message) = SummerizeException(ex);
|
||||||
|
SendError(ErrorType.Exception, callback, code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async void FailInvocation(uint callback, InvocationContext expected, Exception exception)
|
||||||
|
{
|
||||||
|
if (!TakeInvocation(callback, expected, out var context))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.TerminateAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Preserve the exception that ended the stream.
|
||||||
|
}
|
||||||
|
|
||||||
|
var (code, message) = SummerizeException(exception);
|
||||||
|
SendError(ErrorType.Exception, callback, code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PumpEnumerable(uint callback, InvocationContext context)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var next = await context.MoveNextAsync();
|
||||||
|
if (!next.HasValue)
|
||||||
|
{
|
||||||
|
CompleteInvocation(callback, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Chunk(next.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// TerminateExecution owns completion when it cancels the pump.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
FailInvocation(callback, context, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uint ParseExecutionCallback(PlainTdu tdu)
|
||||||
|
=> Convert.ToUInt32(Codec.ParseSync(tdu, Instance.Warehouse));
|
||||||
|
|
||||||
|
void EpRequestPullStream(uint callback, PlainTdu tdu)
|
||||||
|
{
|
||||||
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
|
var context = GetInvocation(executionCallback);
|
||||||
|
|
||||||
|
if (context == null || context.StreamMode != StreamMode.Pull)
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
|
"The target execution is not an active pull stream.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var next = await context.PullAsync();
|
||||||
|
if (next.HasValue)
|
||||||
|
SendChunk(executionCallback, next.Value);
|
||||||
|
else
|
||||||
|
CompleteInvocation(executionCallback, context);
|
||||||
|
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
FailInvocation(executionCallback, context, ex);
|
||||||
|
var (code, message) = SummerizeException(ex);
|
||||||
|
SendError(ErrorType.Exception, callback, code, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpRequestTerminateExecution(uint callback, PlainTdu tdu)
|
||||||
|
{
|
||||||
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
|
|
||||||
|
if (!TakeInvocation(executionCallback, null, out var context))
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
|
"The target execution is not active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.TerminateAsync();
|
||||||
|
SendReply(EpPacketReply.Completed, executionCallback);
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var (code, message) = SummerizeException(ex);
|
||||||
|
SendError(ErrorType.Exception, executionCallback, code, message);
|
||||||
|
SendError(ErrorType.Exception, callback, code, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpRequestHaltExecution(uint callback, PlainTdu tdu)
|
||||||
|
{
|
||||||
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
|
var context = GetInvocation(executionCallback);
|
||||||
|
|
||||||
|
if (context == null || !context.Halt())
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
|
"The target execution is not pausable or is already halted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpRequestResumeExecution(uint callback, PlainTdu tdu)
|
||||||
|
{
|
||||||
|
var executionCallback = ParseExecutionCallback(tdu);
|
||||||
|
var context = GetInvocation(executionCallback);
|
||||||
|
|
||||||
|
if (context == null || !context.Resume())
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.NotAllowed,
|
||||||
|
"The target execution is not pausable or is not halted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
|
||||||
void EpRequestSubscribe(uint callback, PlainTdu tdu)
|
void EpRequestSubscribe(uint callback, PlainTdu tdu)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -1683,7 +2106,7 @@ partial class EpConnection
|
|||||||
|
|
||||||
var et = r.Instance.Definition.GetEventDefByIndex(index);
|
var et = r.Instance.Definition.GetEventDefByIndex(index);
|
||||||
|
|
||||||
if (et != null)
|
if (et == null)
|
||||||
{
|
{
|
||||||
// et not found
|
// et not found
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
|
||||||
@@ -1789,7 +2212,7 @@ partial class EpConnection
|
|||||||
var (offset, length, args) = DataDeserializer.LimitedCountListParser(tdu.Data, tdu.PayloadOffset,
|
var (offset, length, args) = DataDeserializer.LimitedCountListParser(tdu.Data, tdu.PayloadOffset,
|
||||||
tdu.PayloadLength, Instance.Warehouse, 2);
|
tdu.PayloadLength, Instance.Warehouse, 2);
|
||||||
|
|
||||||
var rid = (uint)args[0];
|
var rid = Convert.ToUInt32(args[0]);
|
||||||
var index = (byte)args[1];
|
var index = (byte)args[1];
|
||||||
|
|
||||||
// un hold the socket to send data immediately
|
// un hold the socket to send data immediately
|
||||||
@@ -1806,13 +2229,27 @@ partial class EpConnection
|
|||||||
|
|
||||||
var pt = r.Instance.Definition.GetPropertyDefByIndex(index);
|
var pt = r.Instance.Definition.GetPropertyDefByIndex(index);
|
||||||
|
|
||||||
if (pt != null)
|
if (pt == null)
|
||||||
{
|
{
|
||||||
// property not found
|
// property not found
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.PropertyNotFound);
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.PropertyNotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.Instance.Applicable(_session, ActionType.SetProperty, pt, this) == Ruling.Denied)
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.SetPropertyDenied);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryApplyRateControl(
|
||||||
|
pt,
|
||||||
|
r,
|
||||||
|
ActionType.SetProperty,
|
||||||
|
callback,
|
||||||
|
out var rateControlDelay))
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
if (r is IDynamicResource dynamicResource)
|
if (r is IDynamicResource dynamicResource)
|
||||||
{
|
{
|
||||||
@@ -1823,25 +2260,27 @@ partial class EpConnection
|
|||||||
asyncReply.Then((value) =>
|
asyncReply.Then((value) =>
|
||||||
{
|
{
|
||||||
// propagation
|
// propagation
|
||||||
dynamicResource.SetResourcePropertyAsync(index, value).Then((x) =>
|
ExecuteRateControlled(callback, rateControlDelay, () =>
|
||||||
{
|
dynamicResource.SetResourcePropertyAsync(index, value).Then((x) =>
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
{
|
||||||
}).Error(x =>
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
{
|
}).Error(x =>
|
||||||
SendError(x.Type, callback, (ushort)x.Code, x.Message);
|
{
|
||||||
});
|
SendError(x.Type, callback, (ushort)x.Code, x.Message);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// propagation
|
// propagation
|
||||||
dynamicResource.SetResourcePropertyAsync(index, pr.Value).Then((x) =>
|
ExecuteRateControlled(callback, rateControlDelay, () =>
|
||||||
{
|
dynamicResource.SetResourcePropertyAsync(index, pr.Value).Then((x) =>
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
{
|
||||||
}).Error(x =>
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
{
|
}).Error(x =>
|
||||||
SendError(x.Type, callback, (ushort)x.Code, x.Message);
|
{
|
||||||
});
|
SendError(x.Type, callback, (ushort)x.Code, x.Message);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError)); ;
|
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError)); ;
|
||||||
|
|
||||||
@@ -1856,12 +2295,6 @@ partial class EpConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.Instance.Applicable(_session, ActionType.SetProperty, pt, this) == Ruling.Denied)
|
|
||||||
{
|
|
||||||
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.SetPropertyDenied);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pi.CanWrite)
|
if (!pi.CanWrite)
|
||||||
{
|
{
|
||||||
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ReadOnlyProperty);
|
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ReadOnlyProperty);
|
||||||
@@ -1876,6 +2309,36 @@ partial class EpConnection
|
|||||||
if (pr.Value is AsyncReply asyncReply)
|
if (pr.Value is AsyncReply asyncReply)
|
||||||
{
|
{
|
||||||
asyncReply.Then((value) =>
|
asyncReply.Then((value) =>
|
||||||
|
{
|
||||||
|
ExecuteRateControlled(callback, rateControlDelay, () =>
|
||||||
|
{
|
||||||
|
if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition()
|
||||||
|
== typeof(PropertyContext<>))
|
||||||
|
{
|
||||||
|
value = Activator.CreateInstance(pi.PropertyType, this, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = RuntimeCaster.Cast(value, pi.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pi.SetValue(r, value);
|
||||||
|
SendReply(EpPacketReply.Completed, callback);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SendError(ErrorType.Exception, callback, 0, ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = pr.Value;
|
||||||
|
|
||||||
|
ExecuteRateControlled(callback, rateControlDelay, () =>
|
||||||
{
|
{
|
||||||
if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition()
|
if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition()
|
||||||
== typeof(PropertyContext<>))
|
== typeof(PropertyContext<>))
|
||||||
@@ -1884,7 +2347,6 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// cast new value type to property type
|
|
||||||
value = RuntimeCaster.Cast(value, pi.PropertyType);
|
value = RuntimeCaster.Cast(value, pi.PropertyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1899,32 +2361,6 @@ partial class EpConnection
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var value = pr.Value;
|
|
||||||
|
|
||||||
if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition()
|
|
||||||
== typeof(PropertyContext<>))
|
|
||||||
{
|
|
||||||
value = Activator.CreateInstance(pi.PropertyType, this, value);
|
|
||||||
//value = new DistributedPropertyContext(this, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// cast new value type to property type
|
|
||||||
value = RuntimeCaster.Cast(value, pi.PropertyType);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pi.SetValue(r, value);
|
|
||||||
SendReply(EpPacketReply.Completed, callback);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
SendError(ErrorType.Exception, callback, 0, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
|
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -289,9 +289,33 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
throw new Exception("Function definition not found.");
|
throw new Exception("Function definition not found.");
|
||||||
|
|
||||||
if (ft.IsStatic)
|
if (ft.IsStatic)
|
||||||
return _connection.StaticCall(Instance.Definition.Id, index, args);
|
return ft.StreamMode == StreamMode.None
|
||||||
else
|
? _connection.StaticCall(Instance.Definition.Id, index, args)
|
||||||
return _connection.SendInvoke(_instanceId, index, args);
|
: _connection.StaticStreamCall(Instance.Definition.Id, index, args, ft.StreamMode);
|
||||||
|
|
||||||
|
return ft.StreamMode == StreamMode.None
|
||||||
|
? _connection.SendInvoke(_instanceId, index, args)
|
||||||
|
: _connection.SendStreamInvoke(_instanceId, index, args, ft.StreamMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncStreamReply<T> _InvokeStream<T>(byte index, object args)
|
||||||
|
{
|
||||||
|
if (_status == ResourceStatus.Destroyed)
|
||||||
|
throw new Exception("Trying to access a destroyed object.");
|
||||||
|
|
||||||
|
if (_status == ResourceStatus.Suspended)
|
||||||
|
throw new Exception("Trying to access a suspended object.");
|
||||||
|
|
||||||
|
var function = Instance.Definition.GetFunctionDefByIndex(index)
|
||||||
|
?? throw new Exception("Function definition not found.");
|
||||||
|
|
||||||
|
if (function.StreamMode == StreamMode.None)
|
||||||
|
throw new Exception("Function is not a stream.");
|
||||||
|
|
||||||
|
if (function.IsStatic)
|
||||||
|
return _connection.StaticStreamCall<T>(Instance.Definition.Id, index, args, function.StreamMode);
|
||||||
|
|
||||||
|
return _connection.SendStreamInvoke<T>(_instanceId, index, args, function.StreamMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AsyncReply Subscribe(EventDef et)
|
public AsyncReply Subscribe(EventDef et)
|
||||||
@@ -634,7 +658,8 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
// this only happens if the programmer forgot to emit in property setter
|
// this only happens if the programmer forgot to emit in property setter
|
||||||
_properties[index] = value;
|
_properties[index] = value;
|
||||||
reply.Trigger(null);
|
reply.Trigger(null);
|
||||||
});
|
})
|
||||||
|
.Error(reply.TriggerError);
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
|
|
||||||
@@ -655,4 +680,4 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
|
|||||||
{
|
{
|
||||||
Destroy();
|
Destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ namespace Esiur.Proxy
|
|||||||
|
|
||||||
if (!ci.HasHandle)
|
if (!ci.HasHandle)
|
||||||
{
|
{
|
||||||
code.AppendLine("public virtual AsyncReply<bool> Handle(ResourceOperation operation, IResourceContext context = null) => new AsyncReply<bool>(true);");
|
code.AppendLine("public virtual AsyncReply<bool> Handle(ResourceOperation operation, IResourceContext? context = null) => new AsyncReply<bool>(true);");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,4 +359,4 @@ namespace Esiur.Proxy
|
|||||||
public bool HasHandle;
|
public bool HasHandle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ public static class TypeDefGenerator
|
|||||||
|
|
||||||
var rt = new StringBuilder();
|
var rt = new StringBuilder();
|
||||||
|
|
||||||
rt.AppendLine("using System;\r\nusing Esiur.Resource;\r\nusing Esiur.Core;\r\nusing Esiur.Data;\r\nusing Esiur.Protocol;");
|
rt.AppendLine("using System;\r\nusing Esiur.Resource;\r\nusing Esiur.Core;\r\nusing Esiur.Data;\r\nusing Esiur.Data.Types;\r\nusing Esiur.Protocol;");
|
||||||
rt.AppendLine("#nullable enable");
|
rt.AppendLine("#nullable enable");
|
||||||
|
|
||||||
rt.AppendLine($"namespace {nameSpace} {{");
|
rt.AppendLine($"namespace {nameSpace} {{");
|
||||||
@@ -355,6 +355,8 @@ public static class TypeDefGenerator
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var rtTypeName = GetTypeName(f.ReturnType, typeDefs);
|
var rtTypeName = GetTypeName(f.ReturnType, typeDefs);
|
||||||
|
var isStream = f.StreamMode != StreamMode.None;
|
||||||
|
var replyType = isStream ? "AsyncStreamReply" : "AsyncReply";
|
||||||
|
|
||||||
var positionalArgs = f.Arguments.Where((x) => !x.Optional).ToArray();
|
var positionalArgs = f.Arguments.Where((x) => !x.Optional).ToArray();
|
||||||
var optionalArgs = f.Arguments.Where((x) => x.Optional).ToArray();
|
var optionalArgs = f.Arguments.Where((x) => x.Optional).ToArray();
|
||||||
@@ -367,10 +369,13 @@ public static class TypeDefGenerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isStream)
|
||||||
|
rt.AppendLine($"[Stream(StreamMode.{f.StreamMode}, Pausable = {f.Pausable.ToString().ToLowerInvariant()})]");
|
||||||
|
|
||||||
if (f.IsStatic)
|
if (f.IsStatic)
|
||||||
{
|
{
|
||||||
|
|
||||||
rt.Append($"[Export] public static AsyncReply<{rtTypeName}> {f.Name}(EpConnection connection");
|
rt.Append($"[Export] public static {replyType}<{rtTypeName}> {f.Name}(EpConnection connection");
|
||||||
|
|
||||||
if (positionalArgs.Length > 0)
|
if (positionalArgs.Length > 0)
|
||||||
rt.Append(", " +
|
rt.Append(", " +
|
||||||
@@ -383,7 +388,7 @@ public static class TypeDefGenerator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rt.Append($"[Export] public AsyncReply<{rtTypeName}> {f.Name}(");
|
rt.Append($"[Export] public {replyType}<{rtTypeName}> {f.Name}(");
|
||||||
|
|
||||||
if (positionalArgs.Length > 0)
|
if (positionalArgs.Length > 0)
|
||||||
rt.Append(
|
rt.Append(
|
||||||
@@ -409,6 +414,15 @@ public static class TypeDefGenerator
|
|||||||
$"if ({a.Name} != null) args[{a.Index}] = {a.Name};");
|
$"if ({a.Name} != null) args[{a.Index}] = {a.Name};");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isStream)
|
||||||
|
{
|
||||||
|
if (f.IsStatic)
|
||||||
|
rt.AppendLine($"return connection.StaticStreamCall<{rtTypeName}>({typeDef.Id}UL, {f.Index}, args, StreamMode.{f.StreamMode}); }}");
|
||||||
|
else
|
||||||
|
rt.AppendLine($"return _InvokeStream<{rtTypeName}>({f.Index}, args); }}");
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
rt.AppendLine($"var rt = new AsyncReply<{rtTypeName}>();");
|
rt.AppendLine($"var rt = new AsyncReply<{rtTypeName}>();");
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ namespace Esiur.Resource;
|
|||||||
AttributeTargets.Field,
|
AttributeTargets.Field,
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
Inherited = true)]
|
Inherited = true)]
|
||||||
public sealed class AutoDeliveredAttribute : Attribute
|
public class AutoDeliveryAttribute : Attribute
|
||||||
{
|
{
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using Esiur.Data.Types;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Esiur.Resource;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface |
|
||||||
|
AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property |
|
||||||
|
AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class DescriptionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public DescriptionAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface |
|
||||||
|
AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property |
|
||||||
|
AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class ExampleAttribute : Attribute
|
||||||
|
{
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public ExampleAttribute(object value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
|
||||||
|
AttributeTargets.Interface | AttributeTargets.Enum,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class CategoryAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public CategoryAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
|
||||||
|
AttributeTargets.Interface | AttributeTargets.Enum,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class SinceAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public SinceAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property |
|
||||||
|
AttributeTargets.Field | AttributeTargets.Event,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class TagsAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string[] Values { get; }
|
||||||
|
|
||||||
|
public TagsAttribute(params string[] values) => Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Method | AttributeTargets.Event,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class UnitAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public UnitAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class MinimumAttribute : Attribute
|
||||||
|
{
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public MinimumAttribute(object value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class MaximumAttribute : Attribute
|
||||||
|
{
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public MaximumAttribute(object value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class AllowedValueAttribute : Attribute
|
||||||
|
{
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public AllowedValueAttribute(object value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class PatternAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public PatternAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
|
||||||
|
AttributeTargets.Method | AttributeTargets.Event | AttributeTargets.Parameter,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class FormatAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public FormatAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method,
|
||||||
|
AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class PreconditionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public PreconditionAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method,
|
||||||
|
AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class PostconditionAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public PostconditionAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class EffectsAttribute : Attribute
|
||||||
|
{
|
||||||
|
public OperationEffects Value { get; }
|
||||||
|
|
||||||
|
public EffectsAttribute(OperationEffects value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property |
|
||||||
|
AttributeTargets.Field | AttributeTargets.Event,
|
||||||
|
AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class WarningAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public WarningAttribute(string value) => Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property |
|
||||||
|
AttributeTargets.Field | AttributeTargets.Event,
|
||||||
|
AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class RelatedMembersAttribute : Attribute
|
||||||
|
{
|
||||||
|
public byte[] Indexes { get; }
|
||||||
|
|
||||||
|
public RelatedMembersAttribute(params byte[] indexes) => Indexes = indexes;
|
||||||
|
}
|
||||||
@@ -5,12 +5,13 @@ using System.Text;
|
|||||||
namespace Esiur.Resource
|
namespace Esiur.Resource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that previous values of an exported property are retained
|
/// Indicates that previous property values or event occurrences are retained
|
||||||
/// and may be fetched remotely.
|
/// and may be fetched remotely.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(
|
[AttributeUsage(
|
||||||
AttributeTargets.Property |
|
AttributeTargets.Property |
|
||||||
AttributeTargets.Field,
|
AttributeTargets.Field |
|
||||||
|
AttributeTargets.Event,
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
Inherited = true)]
|
Inherited = true)]
|
||||||
public sealed class HistoricalAttribute : Attribute
|
public sealed class HistoricalAttribute : Attribute
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Esiur.Resource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a named Warehouse rate policy to an exported function or property setter.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(
|
||||||
|
AttributeTargets.Method | AttributeTargets.Property,
|
||||||
|
AllowMultiple = false,
|
||||||
|
Inherited = true)]
|
||||||
|
public sealed class RateControlAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name used to resolve the policy from the owning Warehouse.
|
||||||
|
/// </summary>
|
||||||
|
public string PolicyName { get; }
|
||||||
|
|
||||||
|
public RateControlAttribute(string policyName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(policyName))
|
||||||
|
throw new ArgumentException("A rate policy name is required.", nameof(policyName));
|
||||||
|
|
||||||
|
PolicyName = policyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ using System.Text;
|
|||||||
namespace Esiur.Resource
|
namespace Esiur.Resource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks an exported function as streaming and specifies its delivery mode.
|
/// Marks an AsyncReply-returning exported function as streaming and specifies
|
||||||
|
/// its delivery mode. IEnumerable and IAsyncEnumerable return types infer push
|
||||||
|
/// and pull mode respectively and do not require this attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(
|
[AttributeUsage(
|
||||||
AttributeTargets.Method,
|
AttributeTargets.Method,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Esiur.Resource.Attributes
|
namespace Esiur.Resource
|
||||||
{
|
{
|
||||||
[AttributeUsage(
|
[AttributeUsage(
|
||||||
AttributeTargets.Class |
|
AttributeTargets.Class |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Esiur.Resource.Attributes
|
namespace Esiur.Resource
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that an exported property has volatile synchronization
|
/// Indicates that an exported property has volatile synchronization
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ using Esiur.Protocol;
|
|||||||
using Esiur.Proxy;
|
using Esiur.Proxy;
|
||||||
using Esiur.Security.Authority;
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.RateLimiting;
|
||||||
using Org.BouncyCastle.Asn1.Cms;
|
using Org.BouncyCastle.Asn1.Cms;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
@@ -88,6 +89,8 @@ public class Warehouse
|
|||||||
|
|
||||||
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
|
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
|
||||||
List<IPermissionsManager> _permissionsManagers = new List<IPermissionsManager>();
|
List<IPermissionsManager> _permissionsManagers = new List<IPermissionsManager>();
|
||||||
|
readonly ConcurrentDictionary<string, RatePolicy> _ratePolicies
|
||||||
|
= new ConcurrentDictionary<string, RatePolicy>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
|
||||||
object _typeDefsLock = new object();
|
object _typeDefsLock = new object();
|
||||||
@@ -102,6 +105,11 @@ public class Warehouse
|
|||||||
|
|
||||||
public KeyList<string, ProtocolInstance> Protocols { get; } = new KeyList<string, ProtocolInstance>();
|
public KeyList<string, ProtocolInstance> Protocols { get; } = new KeyList<string, ProtocolInstance>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime settings for this Warehouse.
|
||||||
|
/// </summary>
|
||||||
|
public WarehouseConfiguration Configuration { get; }
|
||||||
|
|
||||||
private Regex urlRegex = new Regex(@"^(?:([\S]*)://([^/]*)/?)");
|
private Regex urlRegex = new Regex(@"^(?:([\S]*)://([^/]*)/?)");
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +129,41 @@ public class Warehouse
|
|||||||
_permissionsManagers.Add(manager);
|
_permissionsManagers.Add(manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a named rate policy referenced by RateControl attributes.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRatePolicy(RatePolicy policy)
|
||||||
|
{
|
||||||
|
if (policy == null)
|
||||||
|
throw new ArgumentNullException(nameof(policy));
|
||||||
|
if (string.IsNullOrWhiteSpace(policy.Name))
|
||||||
|
throw new ArgumentException("The rate policy must have a name.", nameof(policy));
|
||||||
|
if (!_ratePolicies.TryAdd(policy.Name, policy))
|
||||||
|
throw new InvalidOperationException($"A rate policy named `{policy.Name}` is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a policy under the supplied name.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRatePolicy(string name, RatePolicy policy)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
throw new ArgumentException("A rate policy name is required.", nameof(name));
|
||||||
|
if (policy == null)
|
||||||
|
throw new ArgumentNullException(nameof(policy));
|
||||||
|
|
||||||
|
policy.Name = name;
|
||||||
|
AddRatePolicy(policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RatePolicy? TryGetRatePolicy(string name)
|
||||||
|
=> !string.IsNullOrWhiteSpace(name) && _ratePolicies.TryGetValue(name, out var policy)
|
||||||
|
? policy
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public bool RemoveRatePolicy(string name)
|
||||||
|
=> !string.IsNullOrWhiteSpace(name) && _ratePolicies.TryRemove(name, out _);
|
||||||
|
|
||||||
public IAuthenticationProvider GetAuthenticationProvider(string name)
|
public IAuthenticationProvider GetAuthenticationProvider(string name)
|
||||||
{
|
{
|
||||||
if (_authenticationProviders.ContainsKey(name))
|
if (_authenticationProviders.ContainsKey(name))
|
||||||
@@ -137,8 +180,14 @@ public class Warehouse
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Warehouse()
|
public Warehouse() : this(new WarehouseConfiguration())
|
||||||
{
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Warehouse(WarehouseConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
Protocols.Add("EP",
|
Protocols.Add("EP",
|
||||||
async (name, context)
|
async (name, context)
|
||||||
=> await New<EpConnection>(name, context));
|
=> await New<EpConnection>(name, context));
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Esiur.Resource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime configuration owned by a Warehouse instance.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WarehouseConfiguration
|
||||||
|
{
|
||||||
|
public RateControlConfiguration RateControl { get; set; } = new RateControlConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures how repeated rate-control denials affect an EP connection.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RateControlConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Number of denials within <see cref="DenialWindow"/> that blocks the connection.
|
||||||
|
/// Set to zero to disable connection blocking.
|
||||||
|
/// </summary>
|
||||||
|
public int DenialsBeforeConnectionBlock { get; set; } = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window in which denials are accumulated for connection blocking.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan DenialWindow { get; set; } = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delay before a blocked connection is closed, allowing its final error reply to flush.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ConnectionBlockDelay { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using Esiur.Security.Permissions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Esiur.Security.RateLimiting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-connection, per-member token-bucket policy with bounded delayed reservations.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BurstRatePolicy : RatePolicy
|
||||||
|
{
|
||||||
|
sealed class Bucket
|
||||||
|
{
|
||||||
|
public readonly object Sync = new object();
|
||||||
|
public double Tokens;
|
||||||
|
public long LastTimestamp;
|
||||||
|
public int Queued;
|
||||||
|
|
||||||
|
public Bucket(double tokens, long timestamp)
|
||||||
|
{
|
||||||
|
Tokens = tokens;
|
||||||
|
LastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ConnectionBuckets
|
||||||
|
{
|
||||||
|
public readonly ConcurrentDictionary<string, Bucket> Values
|
||||||
|
= new ConcurrentDictionary<string, Bucket>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly ConditionalWeakTable<object, ConnectionBuckets> _connections
|
||||||
|
= new ConditionalWeakTable<object, ConnectionBuckets>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of permits replenished during each period.
|
||||||
|
/// </summary>
|
||||||
|
public int PermitLimit { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replenishment period for <see cref="PermitLimit"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Period { get; set; } = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional permits available for an immediate burst.
|
||||||
|
/// </summary>
|
||||||
|
public int BurstLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of delayed reservations per connection and member.
|
||||||
|
/// Further requests are denied until queue positions become available.
|
||||||
|
/// </summary>
|
||||||
|
public int QueueLimit { get; set; }
|
||||||
|
|
||||||
|
public BurstRatePolicy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BurstRatePolicy(string name) : base(name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Ruling Applicable(RateControlContext context)
|
||||||
|
{
|
||||||
|
Validate();
|
||||||
|
|
||||||
|
var capacity = checked(PermitLimit + BurstLimit);
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
var key = $"{(int)context.Action}:{context.Member.Fullname}";
|
||||||
|
var buckets = _connections.GetValue(context.Connection, _ => new ConnectionBuckets());
|
||||||
|
var bucket = buckets.Values.GetOrAdd(key, _ => new Bucket(capacity, now));
|
||||||
|
|
||||||
|
TimeSpan delay;
|
||||||
|
|
||||||
|
lock (bucket.Sync)
|
||||||
|
{
|
||||||
|
Replenish(bucket, now, capacity);
|
||||||
|
|
||||||
|
if (bucket.Tokens >= 1d)
|
||||||
|
{
|
||||||
|
bucket.Tokens -= 1d;
|
||||||
|
return Ruling.Allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QueueLimit == 0 || bucket.Queued >= QueueLimit)
|
||||||
|
return Ruling.Denied;
|
||||||
|
|
||||||
|
bucket.Tokens -= 1d;
|
||||||
|
bucket.Queued++;
|
||||||
|
|
||||||
|
var seconds = -bucket.Tokens * Period.TotalSeconds / PermitLimit;
|
||||||
|
delay = TimeSpan.FromSeconds(Math.Max(0d, seconds));
|
||||||
|
context.Delay = delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ReleaseQueuePositionAsync(bucket, delay);
|
||||||
|
return Ruling.Allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Validate()
|
||||||
|
{
|
||||||
|
if (PermitLimit <= 0)
|
||||||
|
throw new InvalidOperationException("PermitLimit must be greater than zero.");
|
||||||
|
if (Period <= TimeSpan.Zero)
|
||||||
|
throw new InvalidOperationException("Period must be greater than zero.");
|
||||||
|
if (BurstLimit < 0)
|
||||||
|
throw new InvalidOperationException("BurstLimit cannot be negative.");
|
||||||
|
if (QueueLimit < 0)
|
||||||
|
throw new InvalidOperationException("QueueLimit cannot be negative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Replenish(Bucket bucket, long now, int capacity)
|
||||||
|
{
|
||||||
|
var elapsedTicks = now - bucket.LastTimestamp;
|
||||||
|
if (elapsedTicks <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||||
|
var replenished = elapsedSeconds * PermitLimit / Period.TotalSeconds;
|
||||||
|
bucket.Tokens = Math.Min(capacity, bucket.Tokens + replenished);
|
||||||
|
bucket.LastTimestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task ReleaseQueuePositionAsync(Bucket bucket, TimeSpan delay)
|
||||||
|
{
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
|
|
||||||
|
lock (bucket.Sync)
|
||||||
|
{
|
||||||
|
if (bucket.Queued > 0)
|
||||||
|
bucket.Queued--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Permissions;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Esiur.Security.RateLimiting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the request currently being evaluated by a rate policy.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RateControlContext
|
||||||
|
{
|
||||||
|
public Warehouse Warehouse { get; }
|
||||||
|
public EpConnection Connection { get; }
|
||||||
|
public Session Session { get; }
|
||||||
|
public IResource? Resource { get; }
|
||||||
|
public MemberDef Member { get; }
|
||||||
|
public ActionType Action { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional delay assigned by a policy to an allowed queued request.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Delay { get; set; }
|
||||||
|
|
||||||
|
public RateControlContext(
|
||||||
|
Warehouse warehouse,
|
||||||
|
EpConnection connection,
|
||||||
|
Session session,
|
||||||
|
IResource? resource,
|
||||||
|
MemberDef member,
|
||||||
|
ActionType action)
|
||||||
|
{
|
||||||
|
Warehouse = warehouse ?? throw new ArgumentNullException(nameof(warehouse));
|
||||||
|
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
Session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
Resource = resource;
|
||||||
|
Member = member ?? throw new ArgumentNullException(nameof(member));
|
||||||
|
Action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for named Warehouse rate-control policies.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class RatePolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name referenced by <see cref="RateControlAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected RatePolicy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RatePolicy(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates a context-free policy. Override this for simple policies.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Ruling Applicable() => Ruling.DontCare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates a request. Context-aware policies should override this overload.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Ruling Applicable(RateControlContext context) => Applicable();
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
|||||||
namespace Esiur.Tests.Functional;
|
namespace Esiur.Tests.Functional;
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public class MyRecord:IRecord
|
public class MyRecord : IMyRecord
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using Esiur.Data;
|
using Esiur.Data;
|
||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
|
using Esiur.Data.Types;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Esiur.Protocol;
|
using Esiur.Protocol;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@@ -142,7 +143,7 @@ public partial class MyService
|
|||||||
[Export] public IRecord[] RecordsArray => new IRecord[] { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
[Export] public IRecord[] RecordsArray => new IRecord[] { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
||||||
[Export] public List<MyRecord> RecordsList => new() { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
[Export] public List<MyRecord> RecordsList => new() { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };
|
||||||
|
|
||||||
[Export] public IMyRecord myrecord { get; set; }
|
[Export] public IMyRecord myrecord { get; set; } = new MyRecord();
|
||||||
|
|
||||||
[Export] public MyResource[]? myResources;
|
[Export] public MyResource[]? myResources;
|
||||||
|
|
||||||
@@ -214,4 +215,92 @@ public partial class MyService
|
|||||||
|
|
||||||
[Export] public static string staticFunction(string name) => $"Hello {name}";
|
[Export] public static string staticFunction(string name) => $"Hello {name}";
|
||||||
|
|
||||||
|
int _terminatedPullStreams;
|
||||||
|
|
||||||
|
public int TerminatedPullStreams => Volatile.Read(ref _terminatedPullStreams);
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[RateControl("standard-call")]
|
||||||
|
public int RateLimitedCall() => 1;
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[RateControl("standard-set")]
|
||||||
|
public int RateLimitedValue { get; set; }
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[Cancellable]
|
||||||
|
public async IAsyncEnumerable<int> PullRange(
|
||||||
|
int start,
|
||||||
|
int count,
|
||||||
|
int delayMilliseconds,
|
||||||
|
InvocationContext context)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
await Task.Delay(Math.Max(0, delayMilliseconds), context.CancellationToken);
|
||||||
|
yield return start + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[Cancellable]
|
||||||
|
public async IAsyncEnumerable<int> PullForever(
|
||||||
|
int delayMilliseconds,
|
||||||
|
InvocationContext context)
|
||||||
|
{
|
||||||
|
var value = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(Math.Max(1, delayMilliseconds), context.CancellationToken);
|
||||||
|
yield return value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _terminatedPullStreams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[Cancellable]
|
||||||
|
[Stream(StreamMode.Push, Pausable = true)]
|
||||||
|
public AsyncReply<int> PushSequence(
|
||||||
|
int count,
|
||||||
|
int delayMilliseconds,
|
||||||
|
InvocationContext context)
|
||||||
|
{
|
||||||
|
var reply = new AsyncReply<int>();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(50, context.CancellationToken);
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
await context.WaitWhileHaltedAsync();
|
||||||
|
context.CancellationToken.ThrowIfCancellationRequested();
|
||||||
|
reply.TriggerChunk(i);
|
||||||
|
await Task.Delay(Math.Max(1, delayMilliseconds), context.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Trigger(0);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// TerminateExecution completes the remote stream.
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
reply.TriggerError(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
Copyright (c) 2017-2026 Ahmed Kh. Zamil
|
Copyright (c) 2017-2026 Ahmed Kh. Zamil
|
||||||
|
|
||||||
@@ -22,337 +22,338 @@ SOFTWARE.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using Esiur.Data;
|
|
||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
using Esiur.Net.Http;
|
using Esiur.Data;
|
||||||
using Esiur.Net.Sockets;
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Protocol;
|
||||||
using Esiur.Resource;
|
using Esiur.Resource;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
using Esiur.Security.Permissions;
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.RateLimiting;
|
||||||
using Esiur.Stores;
|
using Esiur.Stores;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Esiur.Security.Integrity;
|
#nullable enable
|
||||||
using System.Linq;
|
|
||||||
using Esiur.Data.Types;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using Esiur.Proxy;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using Esiur.Security.Cryptography;
|
|
||||||
using Esiur.Security.Membership;
|
|
||||||
using Esiur.Net.Packets;
|
|
||||||
using System.Numerics;
|
|
||||||
using Esiur.Protocol;
|
|
||||||
using Esiur.Security.Authority.Providers;
|
|
||||||
using Esiur.Security.Authority;
|
|
||||||
|
|
||||||
namespace Esiur.Tests.Functional;
|
namespace Esiur.Tests.Functional;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
class Program
|
|
||||||
{
|
{
|
||||||
|
static async Task Main()
|
||||||
static void TestSerialization(object x, EpConnection connection = null)
|
|
||||||
{
|
{
|
||||||
|
var serverWarehouse = new Warehouse();
|
||||||
|
var clientWarehouse = new Warehouse();
|
||||||
|
EpConnection? connection = null;
|
||||||
|
|
||||||
var d = Codec.Compose(x, Warehouse.Default, connection);
|
try
|
||||||
// var rr = DC.ToHex(y);
|
|
||||||
|
|
||||||
var y = Codec.ParseSync(d, 0, Warehouse.Default);
|
|
||||||
Console.WriteLine($"{x.GetType().Name}: {x} == {y}, {d.ToHex()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Export]
|
|
||||||
public class StudentRecord : IRecord
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public byte Grade { get; set; }
|
|
||||||
}
|
|
||||||
public enum LogLevel : int
|
|
||||||
{
|
|
||||||
Debug,
|
|
||||||
Warning,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task Main(string[] args)
|
|
||||||
{
|
|
||||||
//TestSerialization("Hello");
|
|
||||||
//TestSerialization(10);
|
|
||||||
//TestSerialization(10.1);
|
|
||||||
//TestSerialization(10.1d);
|
|
||||||
//TestSerialization((byte)1);
|
|
||||||
//TestSerialization((byte)2);
|
|
||||||
//TestSerialization(new int[] { 1, 2, 3, 4 });
|
|
||||||
//var x = LogLevel.Warning;
|
|
||||||
|
|
||||||
//TestSerialization(LogLevel.Warning);
|
|
||||||
|
|
||||||
//TestSerialization(new Map<string, byte?>
|
|
||||||
//{
|
|
||||||
// ["C++"] = 1,
|
|
||||||
// ["C#"] = 2,
|
|
||||||
// ["JS"] = null
|
|
||||||
//});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//TestSerialization(new StudentRecord() { Name = "Ali", Grade = 90 });
|
|
||||||
|
|
||||||
//var tn = Encoding.UTF8.GetBytes("Test.StudentRecord");
|
|
||||||
//var hash = System.Security.Cryptography.SHA256.Create().ComputeHash(tn).Clip(0, 16);
|
|
||||||
//hash[6] = (byte)((hash[6] & 0xF) | 0x80);
|
|
||||||
//hash[8] = (byte)((hash[8] & 0xF) | 0x80);
|
|
||||||
|
|
||||||
//var g = new UUID(hash);
|
|
||||||
|
|
||||||
//Console.WriteLine(g);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//var a = new ECDH();
|
|
||||||
//var b = new ECDH();
|
|
||||||
|
|
||||||
//var apk = a.GetPublicKey();
|
|
||||||
//var bpk = b.GetPublicKey();
|
|
||||||
|
|
||||||
//var ska = a.ComputeSharedKey(bpk);
|
|
||||||
//var skb = b.ComputeSharedKey(apk);
|
|
||||||
|
|
||||||
//Console.WriteLine(ska.ToHex());
|
|
||||||
//Console.WriteLine(skb.ToHex());
|
|
||||||
|
|
||||||
//// Simple membership provider
|
|
||||||
//var membership = new SimpleMembership() { GuestsAllowed = true };
|
|
||||||
|
|
||||||
//membership.AddUser("user", "123456", new SimpleMembership.QuestionAnswer[0]);
|
|
||||||
//membership.AddUser("admin", "admin", new SimpleMembership.QuestionAnswer[]
|
|
||||||
//{
|
|
||||||
// new SimpleMembership.QuestionAnswer()
|
|
||||||
// {
|
|
||||||
// Question = "What is 5+5",
|
|
||||||
// Answer = 10,
|
|
||||||
// Hashed = true,
|
|
||||||
// }
|
|
||||||
//});
|
|
||||||
|
|
||||||
var wh = new Warehouse();
|
|
||||||
wh.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
|
||||||
|
|
||||||
// Create stores to keep objects.
|
|
||||||
var system = await wh.Put("sys", new MemoryStore());
|
|
||||||
var server = await wh.Put("sys/server", new EpServer()
|
|
||||||
{
|
{
|
||||||
AllowedAuthenticationProviders = new string[] { "hash" },
|
var port = FindAvailablePort();
|
||||||
|
var service = await StartServer(serverWarehouse, port);
|
||||||
|
|
||||||
|
connection = await ConnectClient(clientWarehouse, port);
|
||||||
|
var remote = await connection.Get("sys/service") as EpResource
|
||||||
|
?? throw new InvalidOperationException("Remote service was not found.");
|
||||||
|
|
||||||
|
await RunCoreScenarios(connection, service, remote);
|
||||||
|
await RunRateControlScenarios(remote);
|
||||||
|
await RunStreamingScenarios(service, remote);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("All functional scenarios passed.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { connection?.Destroy(); } catch { }
|
||||||
|
try { await clientWarehouse.Close(); } catch { }
|
||||||
|
try { await serverWarehouse.Close(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task<MyService> StartServer(Warehouse warehouse, ushort port)
|
||||||
|
{
|
||||||
|
warehouse.RegisterAuthenticationProvider(new ServerAuthenticationProvider());
|
||||||
|
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 10;
|
||||||
|
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||||
|
{
|
||||||
|
PermitLimit = 1,
|
||||||
|
Period = TimeSpan.FromMinutes(1),
|
||||||
|
QueueLimit = 0,
|
||||||
|
});
|
||||||
|
warehouse.AddRatePolicy(new BurstRatePolicy("standard-set")
|
||||||
|
{
|
||||||
|
PermitLimit = 1,
|
||||||
|
Period = TimeSpan.FromMinutes(1),
|
||||||
|
QueueLimit = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await warehouse.Put("sys", new MemoryStore());
|
||||||
var web = await wh.Put("sys/web", new HttpServer() { Port = 8088 });
|
var server = await warehouse.Put("sys/server", new EpServer
|
||||||
|
|
||||||
var service = await wh.Put("sys/service", new MyService());
|
|
||||||
var res1 = await wh.Put("sys/service/r1", new MyResource() { Description = "Testing 1", CategoryId = 10 });
|
|
||||||
var res2 = await wh.Put("sys/service/r2", new MyResource() { Description = "Testing 2", CategoryId = 11 });
|
|
||||||
var res3 = await wh.Put("sys/service/c1", new MyChildResource() { ChildName = "Child 1", Description = "Child Testing 3", CategoryId = 12 });
|
|
||||||
var res4 = await wh.Put("sys/service/c2", new MyChildResource() { ChildName = "Child 2 Destroy", Description = "Testing Destroy Handler", CategoryId = 12 });
|
|
||||||
|
|
||||||
//TestSerialization(res1);
|
|
||||||
|
|
||||||
server.MapCall("Hello", (string msg, DateTime time, EpConnection sender) =>
|
|
||||||
{
|
{
|
||||||
Console.WriteLine(msg);
|
Port = port,
|
||||||
return "Hi " + DateTime.UtcNow;
|
AllowedAuthenticationProviders = new[] { "hash" },
|
||||||
}).MapCall("temp", () => res4);
|
|
||||||
|
|
||||||
service.Resource = res1;
|
|
||||||
service.ChildResource = res3;
|
|
||||||
service.Resources = new MyResource[] { res1, res2, res1, res3 };
|
|
||||||
service.MyResources = new MyResource[] { res1, res2, res3, res4 };
|
|
||||||
|
|
||||||
//web.MapGet("/{action}/{age}", (int age, string action, HTTPConnection sender) =>
|
|
||||||
//{
|
|
||||||
// Console.WriteLine($"AGE: {age} ACTION: {action}");
|
|
||||||
|
|
||||||
// sender.Response.Number = Esiur.Net.Packets.HTTPResponsePacket.ResponseCode.NotFound;
|
|
||||||
// sender.Send("Not found");
|
|
||||||
//});
|
|
||||||
|
|
||||||
web.MapGet("/", (HttpConnection sender) =>
|
|
||||||
{
|
|
||||||
sender.Send("Hello");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await wh.Open();
|
var service = await warehouse.Put("sys/service", new MyService());
|
||||||
|
service.Instance!.Managers.Add(new AllowPropertySetPermissions());
|
||||||
|
var resource1 = await warehouse.Put("sys/service/r1", new MyResource
|
||||||
|
{
|
||||||
|
Description = "Testing 1",
|
||||||
|
CategoryId = 10,
|
||||||
|
});
|
||||||
|
var resource2 = await warehouse.Put("sys/service/r2", new MyResource
|
||||||
|
{
|
||||||
|
Description = "Testing 2",
|
||||||
|
CategoryId = 11,
|
||||||
|
});
|
||||||
|
var child1 = await warehouse.Put("sys/service/c1", new MyChildResource
|
||||||
|
{
|
||||||
|
ChildName = "Child 1",
|
||||||
|
Description = "Child Testing 3",
|
||||||
|
CategoryId = 12,
|
||||||
|
});
|
||||||
|
var child2 = await warehouse.Put("sys/service/c2", new MyChildResource
|
||||||
|
{
|
||||||
|
ChildName = "Child 2",
|
||||||
|
Description = "Testing lifecycle controls",
|
||||||
|
CategoryId = 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
service.Resource = resource1;
|
||||||
|
service.ChildResource = child1;
|
||||||
|
service.Resources = new MyResource[] { resource1, resource2, resource1, child1 };
|
||||||
|
service.MyResources = new MyResource[] { resource1, resource2, child1, child2 };
|
||||||
|
|
||||||
//var sc = service.GetGenericRecord();
|
server.MapCall("Hello", (string message, DateTime _, EpConnection __) => $"Hi {message}");
|
||||||
//var d = Codec.Compose(sc, Warehouse.Default, null);
|
server.MapCall("temp", () => child2);
|
||||||
|
|
||||||
// Start testing
|
await warehouse.Open();
|
||||||
TestClient(service);
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task<EpConnection> ConnectClient(Warehouse warehouse, ushort port)
|
||||||
// AuthorizationRequest, AsyncReply<object>
|
|
||||||
static AsyncReply<object> Authenticator(AuthorizationRequest x)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Authenticator: {x.Clue}");
|
warehouse.RegisterAuthenticationProvider(new ClientAuthenticationProvider());
|
||||||
|
|
||||||
var format = x.RequiredFormat;
|
return await warehouse.Get<EpConnection>($"ep://localhost:{port}", new EpConnectionContext
|
||||||
|
|
||||||
if (format == EpAuthPacketIAuthFormat.Number)
|
|
||||||
return new AsyncReply<object>(Convert.ToInt32(10));
|
|
||||||
else if (format == EpAuthPacketIAuthFormat.Text)
|
|
||||||
return new AsyncReply<object>(Console.ReadLine().Trim());
|
|
||||||
|
|
||||||
throw new NotImplementedException("Not supported format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async void TestClient(IResource local)
|
|
||||||
{
|
|
||||||
var wh = new Warehouse();
|
|
||||||
var auth = new ClientAuthenticationProvider();
|
|
||||||
wh.RegisterAuthenticationProvider(auth);
|
|
||||||
|
|
||||||
var con = await wh.Get<EpConnection>("ep://localhost", new EpConnectionContext
|
|
||||||
{
|
{
|
||||||
AuthenticationMode = AuthenticationMode.InitializerIdentity,
|
AuthenticationMode = AuthenticationMode.InitializerIdentity,
|
||||||
AutoReconnect = true,
|
AutoReconnect = false,
|
||||||
Identity = "tester",
|
Identity = "tester",
|
||||||
AuthenticationProtocol = "hash",
|
AuthenticationProtocol = "hash",
|
||||||
Domain = "test",
|
Domain = "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
dynamic remote = await con.Get("sys/service");
|
|
||||||
|
|
||||||
TestObjectProps(local, remote);
|
|
||||||
|
|
||||||
//return;
|
|
||||||
|
|
||||||
//return;
|
|
||||||
|
|
||||||
Console.WriteLine("OK");
|
|
||||||
|
|
||||||
perodicTimer = new Timer(new TimerCallback(perodicTimerElapsed), remote, 0, 1000);
|
|
||||||
|
|
||||||
var pcall = await con.Call("Hello", "whats up ?", DateTime.UtcNow);
|
|
||||||
|
|
||||||
var temp = await con.Call("temp");
|
|
||||||
Console.WriteLine("Temp: " + temp.GetHashCode());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var opt = await remote.Optional(new { a1 = 22, a2 = 33, a4 = "What?" });
|
|
||||||
Console.WriteLine(opt);
|
|
||||||
|
|
||||||
var hello = await remote.AsyncHello();
|
|
||||||
|
|
||||||
await remote.Void();
|
|
||||||
await remote.Connection("ss", 33);
|
|
||||||
await remote.ConnectionOptional("Test 2", 88);
|
|
||||||
var rt = await remote.Optional("Optiona", 311);
|
|
||||||
Console.WriteLine(rt);
|
|
||||||
|
|
||||||
var t2 = await remote.GetTuple2(1, "A");
|
|
||||||
Console.WriteLine(t2);
|
|
||||||
var t3 = await remote.GetTuple3(1, "A", 1.3);
|
|
||||||
Console.WriteLine(t3);
|
|
||||||
var t4 = await remote.GetTuple4(1, "A", 1.3, true);
|
|
||||||
Console.WriteLine(t4);
|
|
||||||
|
|
||||||
remote.StringEvent += new EpResourceEvent((sender, args) =>
|
|
||||||
Console.WriteLine($"StringEvent {args}")
|
|
||||||
);
|
|
||||||
|
|
||||||
remote.ArrayEvent += new EpResourceEvent((sender, args) =>
|
|
||||||
Console.WriteLine($"ArrayEvent {args}")
|
|
||||||
);
|
|
||||||
|
|
||||||
await remote.InvokeEvents("Hello");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//var path = TemplateGenerator.GetTemplate("EP://localhost/sys/service", "Generated");
|
|
||||||
|
|
||||||
//Console.WriteLine(path);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async void perodicTimerElapsed(object state)
|
static async Task RunCoreScenarios(EpConnection connection, MyService local, EpResource remote)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Core RPC and serialization");
|
||||||
|
ReportProperties(local, remote);
|
||||||
|
|
||||||
|
dynamic api = remote;
|
||||||
|
|
||||||
|
var procedureResult = (string)await connection.Call("Hello", "functional", DateTime.UtcNow);
|
||||||
|
Require(procedureResult == "Hi functional", "Procedure call returned an unexpected value.");
|
||||||
|
|
||||||
|
var temporaryResource = (IResource)await connection.Call("temp");
|
||||||
|
Require(
|
||||||
|
temporaryResource is EpResource &&
|
||||||
|
temporaryResource.Instance?.Definition.Name.EndsWith(nameof(MyChildResource), StringComparison.Ordinal) == true,
|
||||||
|
"Procedure call did not return the expected remote resource type.");
|
||||||
|
|
||||||
|
var optional = await api.Optional(new { a1 = 22, a2 = 33, a4 = "What?" });
|
||||||
|
Require(optional is double, "Optional argument invocation failed.");
|
||||||
|
|
||||||
|
var hello = await api.AsyncHello();
|
||||||
|
Require(hello is not null, "AsyncReply invocation returned null.");
|
||||||
|
|
||||||
|
await api.Void();
|
||||||
|
await api.Connection("connection", 33);
|
||||||
|
await api.ConnectionOptional("optional connection", 88);
|
||||||
|
|
||||||
|
var tuple = await api.GetTuple4(1, "A", 1.3, true);
|
||||||
|
Require(tuple is not null, "Tuple invocation returned null.");
|
||||||
|
|
||||||
|
var eventReceived = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
EpResourceEvent handler = (_, value) => eventReceived.TrySetResult((string)value);
|
||||||
|
await remote.Subscribe(nameof(MyService.StringEvent));
|
||||||
|
api.StringEvent += handler;
|
||||||
|
|
||||||
|
await api.InvokeEvents("event payload");
|
||||||
|
var eventValue = await eventReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
Require(eventValue == "event payload", "Remote event payload did not round-trip.");
|
||||||
|
|
||||||
|
api.StringEvent -= handler;
|
||||||
|
await remote.Unsubscribe(nameof(MyService.StringEvent));
|
||||||
|
Console.WriteLine(" PASS core RPC, optional arguments, tuples, and events");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task RunStreamingScenarios(MyService local, EpResource remote)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Streaming and execution controls");
|
||||||
|
|
||||||
|
var pull = InvokeStream<int>(remote, nameof(MyService.PullRange), 10, 5, 5);
|
||||||
|
var pulledValues = new List<int>();
|
||||||
|
using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
await foreach (var value in pull.WithCancellation(timeout.Token))
|
||||||
|
pulledValues.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Require(
|
||||||
|
pulledValues.SequenceEqual(new[] { 10, 11, 12, 13, 14 }),
|
||||||
|
"PullStream did not preserve item order or backpressure.");
|
||||||
|
Console.WriteLine(" PASS PullStream / IAsyncEnumerable<T>");
|
||||||
|
|
||||||
|
var terminatedBefore = local.TerminatedPullStreams;
|
||||||
|
var infinite = InvokeStream<int>(remote, nameof(MyService.PullForever), 5);
|
||||||
|
var infiniteEnumerator = infinite.GetAsyncEnumerator();
|
||||||
|
|
||||||
|
Require(await infiniteEnumerator.MoveNextAsync(), "Infinite pull stream returned no first item.");
|
||||||
|
Require(await infiniteEnumerator.MoveNextAsync(), "Infinite pull stream returned no second item.");
|
||||||
|
await infiniteEnumerator.DisposeAsync();
|
||||||
|
|
||||||
|
await WaitUntil(
|
||||||
|
() => local.TerminatedPullStreams == terminatedBefore + 1,
|
||||||
|
TimeSpan.FromSeconds(5));
|
||||||
|
Require(infinite.Completed, "TerminateExecution did not complete the client stream.");
|
||||||
|
Console.WriteLine(" PASS TerminateExecution / enumerator disposal");
|
||||||
|
|
||||||
|
var push = InvokeStream<int>(remote, nameof(MyService.PushSequence), 4, 150);
|
||||||
|
var pushEnumerator = push.GetAsyncEnumerator();
|
||||||
|
var pushedValues = new List<int>();
|
||||||
|
|
||||||
|
Require(await pushEnumerator.MoveNextAsync(), "Push stream returned no first item.");
|
||||||
|
pushedValues.Add(pushEnumerator.Current);
|
||||||
|
|
||||||
|
await push.Halt();
|
||||||
|
var haltedMove = pushEnumerator.MoveNextAsync().AsTask();
|
||||||
|
await Task.Delay(250);
|
||||||
|
Require(!haltedMove.IsCompleted, "HaltExecution did not pause the producer.");
|
||||||
|
|
||||||
|
await push.Resume();
|
||||||
|
Require(
|
||||||
|
await haltedMove.WaitAsync(TimeSpan.FromSeconds(5)),
|
||||||
|
"ResumeExecution did not resume the producer.");
|
||||||
|
pushedValues.Add(pushEnumerator.Current);
|
||||||
|
|
||||||
|
while (await pushEnumerator.MoveNextAsync())
|
||||||
|
pushedValues.Add(pushEnumerator.Current);
|
||||||
|
|
||||||
|
Require(
|
||||||
|
pushedValues.SequenceEqual(new[] { 0, 1, 2, 3 }),
|
||||||
|
"Push stream lost or reordered items across halt/resume.");
|
||||||
|
Console.WriteLine(" PASS HaltExecution / ResumeExecution");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task RunRateControlScenarios(EpResource remote)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Rate control");
|
||||||
|
|
||||||
|
var function = remote.Instance.Definition.GetFunctionDefByName(nameof(MyService.RateLimitedCall))
|
||||||
|
?? throw new InvalidOperationException("Rate-limited function was not found.");
|
||||||
|
var property = remote.Instance.Definition.GetPropertyDefByName(nameof(MyService.RateLimitedValue))
|
||||||
|
?? throw new InvalidOperationException("Rate-limited property was not found.");
|
||||||
|
|
||||||
|
await remote._Invoke(function.Index, Array.Empty<object>());
|
||||||
|
await ExpectRateLimit(() => remote._Invoke(function.Index, Array.Empty<object>()));
|
||||||
|
|
||||||
|
await remote.SetResourcePropertyAsync(property.Index, 1);
|
||||||
|
await ExpectRateLimit(() => remote.SetResourcePropertyAsync(property.Index, 2));
|
||||||
|
|
||||||
|
Console.WriteLine(" PASS function-call and property-set policies");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task ExpectRateLimit(Func<AsyncReply> action)
|
||||||
{
|
{
|
||||||
GC.Collect();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dynamic remote = state;
|
await action();
|
||||||
await remote.InvokeEvents("Hello");
|
|
||||||
|
|
||||||
Console.WriteLine("Perodic : " + await remote.AsyncHello());
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (AsyncException exception) when (exception.Code == ExceptionCode.RateLimitExceeded)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Perodic : " + ex.ToString());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("The request was expected to be rate limited.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static Timer perodicTimer;
|
static AsyncStreamReply<T> InvokeStream<T>(
|
||||||
|
EpResource resource,
|
||||||
static void TestObjectProps(IResource local, EpResource remote)
|
string functionName,
|
||||||
|
params object[] arguments)
|
||||||
{
|
{
|
||||||
|
var function = resource.Instance.Definition.GetFunctionDefByName(functionName)
|
||||||
|
?? throw new InvalidOperationException($"Function `{functionName}` was not found.");
|
||||||
|
|
||||||
foreach (var pt in local.Instance.Definition.Properties)
|
var indexedArguments = new Map<byte, object>();
|
||||||
{
|
for (byte i = 0; i < arguments.Length; i++)
|
||||||
|
indexedArguments[i] = arguments[i];
|
||||||
var lv = pt.PropertyInfo.GetValue(local);
|
|
||||||
object v;
|
|
||||||
var rv = remote.TryGetPropertyValue(pt.Index, out v);
|
|
||||||
if (!rv)
|
|
||||||
Console.WriteLine($" ** {pt.Name} Failed");
|
|
||||||
else
|
|
||||||
Console.WriteLine($"{pt.Name} {GetString(lv)} == {GetString(v)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return resource._InvokeStream<T>(function.Index, indexedArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
static string GetString(object value)
|
static void ReportProperties(MyService local, EpResource remote)
|
||||||
{
|
{
|
||||||
if (value == null)
|
var compared = 0;
|
||||||
return "NULL";
|
var definition = local.Instance?.Definition
|
||||||
|
?? throw new InvalidOperationException("Local service was not initialized.");
|
||||||
|
|
||||||
var t = value.GetType();
|
foreach (var property in definition.Properties)
|
||||||
var nt = Nullable.GetUnderlyingType(t);
|
|
||||||
if (nt != null)
|
|
||||||
t = nt;
|
|
||||||
if (t.IsArray)
|
|
||||||
{
|
{
|
||||||
var ar = (Array)value;
|
if (!remote.TryGetPropertyValue(property.Index, out _))
|
||||||
if (ar.Length == 0)
|
throw new InvalidOperationException($"Remote property `{property.Name}` was not attached.");
|
||||||
return "[]";
|
|
||||||
var rt = "[";
|
|
||||||
for (var i = 0; i < ar.Length - 1; i++)
|
|
||||||
rt += GetString(ar.GetValue(i)) + ",";
|
|
||||||
rt += GetString(ar.GetValue(ar.Length - 1)) + "]";
|
|
||||||
|
|
||||||
return rt;
|
compared++;
|
||||||
}
|
|
||||||
else if (value is Record)
|
|
||||||
{
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
else if (value is IRecord)
|
|
||||||
{
|
|
||||||
return "{" + String.Join(", ", t.GetProperties().Select(x => x.Name + ": " + x.GetValue(value))) + "}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
Console.WriteLine($" Attached {compared} exported properties");
|
||||||
return value.ToString();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
static async Task WaitUntil(Func<bool> condition, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
var deadline = DateTime.UtcNow + timeout;
|
||||||
|
while (!condition())
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow >= deadline)
|
||||||
|
throw new TimeoutException("Timed out waiting for the functional condition.");
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Require(bool condition, string message)
|
||||||
|
{
|
||||||
|
if (!condition)
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ushort FindAvailablePort()
|
||||||
|
{
|
||||||
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
var port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
listener.Stop();
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,43 @@
|
|||||||
|
# Esiur functional smoke test
|
||||||
|
|
||||||
|
This project starts an in-process Esiur server and an authenticated client,
|
||||||
|
runs its scenarios over a real loopback connection, and exits with a non-zero
|
||||||
|
status if any assertion fails.
|
||||||
|
|
||||||
|
Coverage includes:
|
||||||
|
|
||||||
|
- resource attachment and property serialization;
|
||||||
|
- procedure and resource calls, optional arguments, tuples, and events;
|
||||||
|
- named rate policies for function calls and property setters;
|
||||||
|
- rate-limit denial propagation to callers;
|
||||||
|
- pull streams backed by `IAsyncEnumerable<T>`;
|
||||||
|
- `TerminateExecution` through async-enumerator disposal;
|
||||||
|
- `HaltExecution` and `ResumeExecution` for a cooperative push stream.
|
||||||
|
|
||||||
|
Run it from the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project Tests\Features\Functional\Esiur.Tests.Functional.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
The runner selects an available loopback port and shuts down its client,
|
||||||
|
server, and warehouses when complete.
|
||||||
|
|
||||||
|
Rate policies are registered by name on the server Warehouse:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
warehouse.AddRatePolicy(new BurstRatePolicy("standard-call")
|
||||||
|
{
|
||||||
|
PermitLimit = 100,
|
||||||
|
Period = TimeSpan.FromSeconds(1),
|
||||||
|
BurstLimit = 20,
|
||||||
|
QueueLimit = 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
warehouse.Configuration.RateControl.DenialsBeforeConnectionBlock = 5;
|
||||||
|
|
||||||
|
[RateControl("standard-call")]
|
||||||
|
public void Call()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
using Esiur.Core;
|
||||||
|
using Esiur.Data.Types;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class AsyncStreamReplyTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task PullStream_RequestsOneItemPerMoveNext()
|
||||||
|
{
|
||||||
|
var pulls = 0;
|
||||||
|
var stream = CreateStream<int>(StreamMode.Pull, pull: () =>
|
||||||
|
{
|
||||||
|
pulls++;
|
||||||
|
return CompletedReply();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.TriggerStreamStarted();
|
||||||
|
var enumerator = stream.GetAsyncEnumerator();
|
||||||
|
|
||||||
|
var firstMove = enumerator.MoveNextAsync().AsTask();
|
||||||
|
Assert.Equal(1, pulls);
|
||||||
|
Assert.False(firstMove.IsCompleted);
|
||||||
|
|
||||||
|
stream.TriggerChunk(17);
|
||||||
|
Assert.True(await firstMove);
|
||||||
|
Assert.Equal(17, enumerator.Current);
|
||||||
|
|
||||||
|
var secondMove = enumerator.MoveNextAsync().AsTask();
|
||||||
|
Assert.Equal(2, pulls);
|
||||||
|
stream.TriggerStreamCompleted();
|
||||||
|
|
||||||
|
Assert.False(await secondMove);
|
||||||
|
await enumerator.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposingStream_TerminatesRemoteExecutionOnce()
|
||||||
|
{
|
||||||
|
var terminations = 0;
|
||||||
|
var stream = CreateStream<int>(StreamMode.Pull, terminate: () =>
|
||||||
|
{
|
||||||
|
terminations++;
|
||||||
|
return CompletedReply();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.TriggerStreamStarted();
|
||||||
|
var enumerator = stream.GetAsyncEnumerator();
|
||||||
|
|
||||||
|
await enumerator.DisposeAsync();
|
||||||
|
await enumerator.DisposeAsync();
|
||||||
|
|
||||||
|
Assert.Equal(1, terminations);
|
||||||
|
Assert.True(stream.Completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LifecycleControls_AreSentThroughStreamReply()
|
||||||
|
{
|
||||||
|
var halts = 0;
|
||||||
|
var resumes = 0;
|
||||||
|
var stream = CreateStream<int>(
|
||||||
|
StreamMode.Push,
|
||||||
|
halt: () =>
|
||||||
|
{
|
||||||
|
halts++;
|
||||||
|
return CompletedReply();
|
||||||
|
},
|
||||||
|
resume: () =>
|
||||||
|
{
|
||||||
|
resumes++;
|
||||||
|
return CompletedReply();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.TriggerStreamStarted();
|
||||||
|
stream.Halt();
|
||||||
|
stream.Resume();
|
||||||
|
|
||||||
|
Assert.Equal(1, halts);
|
||||||
|
Assert.Equal(1, resumes);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => stream.Pull());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvocationContext_PullsGenericAsyncEnumerableAndCancelsIt()
|
||||||
|
{
|
||||||
|
var disposed = false;
|
||||||
|
var context = new InvocationContext(null!, 1);
|
||||||
|
context.InitializeStream(StreamMode.Pull, pausable: false);
|
||||||
|
|
||||||
|
Assert.True(context.SetAsyncEnumerable(Values(() => disposed = true)));
|
||||||
|
|
||||||
|
var first = await context.PullAsync();
|
||||||
|
var second = await context.PullAsync();
|
||||||
|
|
||||||
|
Assert.True(first.HasValue);
|
||||||
|
Assert.Equal(1, first.Value);
|
||||||
|
Assert.True(second.HasValue);
|
||||||
|
Assert.Equal(2, second.Value);
|
||||||
|
|
||||||
|
await context.TerminateAsync();
|
||||||
|
|
||||||
|
Assert.True(context.CancellationToken.IsCancellationRequested);
|
||||||
|
Assert.True(disposed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvocationContext_HaltAndResumeGatePushEnumeration()
|
||||||
|
{
|
||||||
|
var context = new InvocationContext(null!, 1);
|
||||||
|
context.InitializeStream(StreamMode.Push, pausable: true);
|
||||||
|
context.SetEnumerable(new[] { 3, 4 });
|
||||||
|
|
||||||
|
Assert.True(context.Halt());
|
||||||
|
var move = context.MoveNextAsync();
|
||||||
|
Assert.False(move.IsCompleted);
|
||||||
|
|
||||||
|
Assert.True(context.Resume());
|
||||||
|
var item = await move;
|
||||||
|
|
||||||
|
Assert.True(item.HasValue);
|
||||||
|
Assert.Equal(3, item.Value);
|
||||||
|
await context.EndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static AsyncStreamReply<T> CreateStream<T>(
|
||||||
|
StreamMode mode,
|
||||||
|
Func<AsyncReply>? pull = null,
|
||||||
|
Func<AsyncReply>? terminate = null,
|
||||||
|
Func<AsyncReply>? halt = null,
|
||||||
|
Func<AsyncReply>? resume = null)
|
||||||
|
=> new(
|
||||||
|
mode,
|
||||||
|
pull ?? CompletedReply,
|
||||||
|
terminate ?? CompletedReply,
|
||||||
|
halt ?? CompletedReply,
|
||||||
|
resume ?? CompletedReply);
|
||||||
|
|
||||||
|
static AsyncReply CompletedReply() => new((object)null!);
|
||||||
|
|
||||||
|
static async IAsyncEnumerable<int> Values(
|
||||||
|
Action onDisposed,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
yield return 1;
|
||||||
|
await Task.Yield();
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return 2;
|
||||||
|
await Task.Delay(Timeout.Infinite, cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
onDisposed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Protocol;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Permissions;
|
||||||
|
using Esiur.Security.RateLimiting;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class RatePolicyTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RateControlAttribute_IsCapturedForFunctionsAndProperties()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var method = typeof(RateFixture).GetMethod(nameof(RateFixture.Call))!;
|
||||||
|
var property = typeof(RateFixture).GetProperty(nameof(RateFixture.Value))!;
|
||||||
|
|
||||||
|
var functionDefinition = FunctionDef.MakeFunctionDef(
|
||||||
|
warehouse, typeof(RateFixture), method, 0, method.Name, new TypeDef());
|
||||||
|
var propertyDefinition = PropertyDef.MakePropertyDef(
|
||||||
|
warehouse, typeof(RateFixture), property, property.Name, 0, new TypeDef());
|
||||||
|
|
||||||
|
Assert.Equal("standard-call", functionDefinition.RatePolicyName);
|
||||||
|
Assert.Equal("standard-set", propertyDefinition.RatePolicyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Warehouse_RegistersPoliciesByTheirConfiguredName()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var policy = new DenyPolicy { Name = "deny" };
|
||||||
|
|
||||||
|
warehouse.AddRatePolicy(policy);
|
||||||
|
|
||||||
|
Assert.Same(policy, warehouse.TryGetRatePolicy("deny"));
|
||||||
|
Assert.Throws<InvalidOperationException>(() => warehouse.AddRatePolicy(policy));
|
||||||
|
Assert.True(warehouse.RemoveRatePolicy("deny"));
|
||||||
|
Assert.Null(warehouse.TryGetRatePolicy("deny"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContextFreePolicies_AreSupported()
|
||||||
|
{
|
||||||
|
var policy = new DenyPolicy();
|
||||||
|
Assert.Equal(Ruling.Denied, policy.Applicable(CreateContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BurstPolicy_AllowsBurstQueuesOverflowAndThenDenies()
|
||||||
|
{
|
||||||
|
var policy = new BurstRatePolicy("standard-call")
|
||||||
|
{
|
||||||
|
PermitLimit = 1,
|
||||||
|
Period = TimeSpan.FromSeconds(1),
|
||||||
|
BurstLimit = 1,
|
||||||
|
QueueLimit = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var immediateOne = CreateContext();
|
||||||
|
var immediateTwo = CreateContext(immediateOne.Connection);
|
||||||
|
var queued = CreateContext(immediateOne.Connection);
|
||||||
|
var denied = CreateContext(immediateOne.Connection);
|
||||||
|
|
||||||
|
Assert.Equal(Ruling.Allowed, policy.Applicable(immediateOne));
|
||||||
|
Assert.Equal(Ruling.Allowed, policy.Applicable(immediateTwo));
|
||||||
|
Assert.Equal(Ruling.Allowed, policy.Applicable(queued));
|
||||||
|
Assert.True(queued.Delay > TimeSpan.Zero);
|
||||||
|
Assert.Equal(Ruling.Denied, policy.Applicable(denied));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarehouseConfiguration_IsIsolatedPerWarehouse()
|
||||||
|
{
|
||||||
|
var configured = new Warehouse(new WarehouseConfiguration
|
||||||
|
{
|
||||||
|
RateControl = new RateControlConfiguration
|
||||||
|
{
|
||||||
|
DenialsBeforeConnectionBlock = 2,
|
||||||
|
DenialWindow = TimeSpan.FromSeconds(10),
|
||||||
|
ConnectionBlockDelay = TimeSpan.Zero,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var defaults = new Warehouse();
|
||||||
|
|
||||||
|
Assert.Equal(2, configured.Configuration.RateControl.DenialsBeforeConnectionBlock);
|
||||||
|
Assert.Equal(5, defaults.Configuration.RateControl.DenialsBeforeConnectionBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RateControlContext CreateContext(EpConnection? connection = null)
|
||||||
|
=> new(
|
||||||
|
new Warehouse(),
|
||||||
|
connection ?? new EpConnection(),
|
||||||
|
new Session(),
|
||||||
|
null,
|
||||||
|
new FunctionDef { Name = "Call" },
|
||||||
|
ActionType.Execute);
|
||||||
|
|
||||||
|
sealed class DenyPolicy : RatePolicy
|
||||||
|
{
|
||||||
|
public override Ruling Applicable() => Ruling.Denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class RateFixture
|
||||||
|
{
|
||||||
|
[RateControl("standard-call")]
|
||||||
|
public void Call()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[RateControl("standard-set")]
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
using Esiur.Core;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Data.Types;
|
||||||
|
using Esiur.Resource;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
public class TypeDefAttributeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FunctionStreamMode_IsInferredOrReadFromAttribute()
|
||||||
|
{
|
||||||
|
var pull = MakeFunction(nameof(StreamFixture.Pull));
|
||||||
|
var push = MakeFunction(nameof(StreamFixture.Push));
|
||||||
|
var explicitStream = MakeFunction(nameof(StreamFixture.Explicit));
|
||||||
|
|
||||||
|
Assert.Equal(StreamMode.Pull, pull.StreamMode);
|
||||||
|
Assert.Equal(TruIdentifier.Int32, pull.ReturnType.Identifier);
|
||||||
|
Assert.Equal(StreamMode.Push, push.StreamMode);
|
||||||
|
Assert.Equal(TruIdentifier.Int32, push.ReturnType.Identifier);
|
||||||
|
Assert.Equal(StreamMode.Push, explicitStream.StreamMode);
|
||||||
|
Assert.True(explicitStream.Pausable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(nameof(StreamFixture.InvalidReturn))]
|
||||||
|
[InlineData(nameof(StreamFixture.InvalidPausablePull))]
|
||||||
|
[InlineData(nameof(StreamFixture.ConflictingMode))]
|
||||||
|
public void FunctionStreamMode_RejectsInvalidDeclarations(string methodName)
|
||||||
|
{
|
||||||
|
var exception = Assert.Throws<Exception>(() => MakeFunction(methodName));
|
||||||
|
Assert.Contains("stream", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Attributes_AreIncludedInSerializedTypeDef()
|
||||||
|
{
|
||||||
|
var warehouse = new Warehouse();
|
||||||
|
var definition = new LocalTypeDef(typeof(AttributedResource), warehouse);
|
||||||
|
var bytes = Codec.Compose(TypeDefInfo.FromTypeDef(definition), warehouse, null);
|
||||||
|
var (_, info) = Codec.ParseIndexedType<TypeDefInfo>(bytes, 0, warehouse);
|
||||||
|
|
||||||
|
Assert.Equal("type usage", info.Usage);
|
||||||
|
Assert.Equal("type description", info.Description);
|
||||||
|
Assert.Equal("type example", info.Example);
|
||||||
|
Assert.Equal("tests", info.Category);
|
||||||
|
Assert.Equal("3.1", info.Since);
|
||||||
|
Assert.Equal("type", info.Annotations!["scope"]);
|
||||||
|
|
||||||
|
var property = Assert.Single(info.Properties!, x => x.Name == "Value");
|
||||||
|
var propertyFlags = (PropertyDefFlags)property.Flags;
|
||||||
|
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.ReadOnly));
|
||||||
|
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Volatile));
|
||||||
|
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Historical));
|
||||||
|
Assert.True(propertyFlags.HasFlag(PropertyDefFlags.Deprecated));
|
||||||
|
Assert.Equal(OrderingControl.LatestOnly, property.OrderingControl);
|
||||||
|
Assert.Equal(5, Convert.ToInt32(property.DefaultValue));
|
||||||
|
Assert.Equal("property description", property.Description);
|
||||||
|
Assert.Equal("property usage", property.Usage);
|
||||||
|
Assert.Equal(2, property.Examples!.Count);
|
||||||
|
Assert.Equal(new[] { "state", "sample" }, property.Tags);
|
||||||
|
Assert.Equal("units", property.Unit);
|
||||||
|
Assert.Equal(0, Convert.ToInt32(property.Minimum));
|
||||||
|
Assert.Equal(10, Convert.ToInt32(property.Maximum));
|
||||||
|
Assert.Equal(new[] { 1, 5 }, property.AllowedValues!.Select(Convert.ToInt32));
|
||||||
|
Assert.Equal("^[0-9]+$", property.Pattern);
|
||||||
|
Assert.Equal("integer", property.Format);
|
||||||
|
Assert.Equal("property warning", Assert.Single(property.Warnings!));
|
||||||
|
Assert.Equal((byte)7, Assert.Single(property.RelatedMembers!));
|
||||||
|
Assert.Equal("use NewValue", property.DeprecationMessage);
|
||||||
|
Assert.Equal("property", property.Annotations!["scope"]);
|
||||||
|
|
||||||
|
var function = Assert.Single(info.Functions!, x => x.Name == nameof(AttributedResource.Watch));
|
||||||
|
var functionFlags = (FunctionDefFlags)function.Flags;
|
||||||
|
Assert.True(functionFlags.HasFlag(FunctionDefFlags.ReadOnly));
|
||||||
|
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Idempotent));
|
||||||
|
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Cancellable));
|
||||||
|
Assert.True(functionFlags.HasFlag(FunctionDefFlags.Pausable));
|
||||||
|
Assert.Equal(StreamMode.Push, function.StreamMode);
|
||||||
|
Assert.Equal("ready", Assert.Single(function.Preconditions!));
|
||||||
|
Assert.Equal("complete", Assert.Single(function.Postconditions!));
|
||||||
|
Assert.Equal(OperationEffects.EmitsEvents, function.Effects);
|
||||||
|
|
||||||
|
var eventInfo = Assert.Single(info.Events!, x => x.Name == nameof(AttributedResource.Changed));
|
||||||
|
var eventFlags = (EventDefFlags)eventInfo.Flags;
|
||||||
|
Assert.True(eventFlags.HasFlag(EventDefFlags.AutoDelivered));
|
||||||
|
Assert.True(eventFlags.HasFlag(EventDefFlags.Historical));
|
||||||
|
Assert.Equal(OrderingControl.Relaxed, eventInfo.OrderingControl);
|
||||||
|
|
||||||
|
var constant = Assert.Single(info.Constants!, x => x.Name == nameof(AttributedResource.Limit));
|
||||||
|
Assert.Equal("constant description", constant.Description);
|
||||||
|
Assert.Equal("constant usage", constant.Usage);
|
||||||
|
Assert.Equal(10, Convert.ToInt32(constant.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FunctionDef MakeFunction(string name)
|
||||||
|
{
|
||||||
|
var method = typeof(StreamFixture).GetMethod(name, BindingFlags.Public | BindingFlags.Instance)!;
|
||||||
|
return FunctionDef.MakeFunctionDef(
|
||||||
|
Warehouse.Default, typeof(StreamFixture), method, 0, name, new TypeDef());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StreamFixture
|
||||||
|
{
|
||||||
|
public IAsyncEnumerable<int> Pull() => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public IEnumerable<int> Push() => Array.Empty<int>();
|
||||||
|
|
||||||
|
[Stream(StreamMode.Push, Pausable = true)]
|
||||||
|
public AsyncReply<int> Explicit() => new(1);
|
||||||
|
|
||||||
|
[Stream]
|
||||||
|
public int InvalidReturn() => 1;
|
||||||
|
|
||||||
|
[Stream(StreamMode.Pull, Pausable = true)]
|
||||||
|
public AsyncReply<int> InvalidPausablePull() => new(1);
|
||||||
|
|
||||||
|
[Stream(StreamMode.Push)]
|
||||||
|
public IAsyncEnumerable<int> ConflictingMode() => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
[Usage("type usage")]
|
||||||
|
[Description("type description")]
|
||||||
|
[Example("type example")]
|
||||||
|
[Category("tests")]
|
||||||
|
[Since("3.1")]
|
||||||
|
[Annotation("scope", "type")]
|
||||||
|
private sealed class AttributedResource : Esiur.Resource.Resource
|
||||||
|
{
|
||||||
|
[Description("constant description")]
|
||||||
|
[Usage("constant usage")]
|
||||||
|
[Example(10)]
|
||||||
|
[Annotation("scope", "constant")]
|
||||||
|
public const int Limit = 10;
|
||||||
|
|
||||||
|
[ReadOnly]
|
||||||
|
[Volatile]
|
||||||
|
[Historical]
|
||||||
|
[Ordering(OrderingControl.LatestOnly)]
|
||||||
|
[System.ComponentModel.DefaultValue(5)]
|
||||||
|
[Description("property description")]
|
||||||
|
[Usage("property usage")]
|
||||||
|
[Example(1)]
|
||||||
|
[Example(5)]
|
||||||
|
[Tags("state", "sample")]
|
||||||
|
[Unit("units")]
|
||||||
|
[Minimum(0)]
|
||||||
|
[Maximum(10)]
|
||||||
|
[AllowedValue(1)]
|
||||||
|
[AllowedValue(5)]
|
||||||
|
[Pattern("^[0-9]+$")]
|
||||||
|
[Format("integer")]
|
||||||
|
[Warning("property warning")]
|
||||||
|
[RelatedMembers(7)]
|
||||||
|
[Obsolete("use NewValue")]
|
||||||
|
[Annotation("scope", "property")]
|
||||||
|
public int Value { get; set; }
|
||||||
|
|
||||||
|
[Stream(StreamMode.Push, Pausable = true)]
|
||||||
|
[ReadOnly]
|
||||||
|
[Idempotent]
|
||||||
|
[Cancellable]
|
||||||
|
[Precondition("ready")]
|
||||||
|
[Postcondition("complete")]
|
||||||
|
[Effects(OperationEffects.EmitsEvents)]
|
||||||
|
public AsyncReply<int> Watch() => new(1);
|
||||||
|
|
||||||
|
[AutoDelivery]
|
||||||
|
[Historical]
|
||||||
|
[Ordering(OrderingControl.Relaxed)]
|
||||||
|
public event ResourceEventHandler<int>? Changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user