Rate Policy

This commit is contained in:
2026-07-13 17:15:00 +03:00
parent 31bda58460
commit 4f6d2d0801
46 changed files with 3142 additions and 468 deletions
+1 -1
View File
@@ -271,7 +271,7 @@ public class AsyncReply
return;
}
public void TriggerError(Exception exception)
public virtual void TriggerError(Exception exception)
{
//timeout?.Dispose();
+409
View File
@@ -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());
}
}
+2 -1
View File
@@ -46,5 +46,6 @@ public enum ExceptionCode : ushort
Timeout,
NotSupported,
NotImplemented,
NotAllowed
NotAllowed,
RateLimitExceeded
}
+219 -8
View File
@@ -1,15 +1,47 @@
using Esiur.Protocol;
using Esiur.Data.Types;
using Esiur.Protocol;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Esiur.Core
{
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)
{
@@ -19,8 +51,8 @@ namespace Esiur.Core
Connection.SendChunk(CallbackId, value);
}
public void Progress(uint value, uint max) {
public void Progress(uint value, uint max)
{
if (Ended)
throw new Exception("Execution has ended.");
@@ -29,7 +61,6 @@ namespace Esiur.Core
public void Warning(byte level, string message)
{
if (Ended)
throw new Exception("Execution has ended.");
@@ -38,12 +69,192 @@ namespace Esiur.Core
public EpConnection Connection { get; internal set; }
internal InvocationContext(EpConnection connection, uint callbackId)
{
Connection = connection;
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();
}
}
+2 -3
View File
@@ -14,7 +14,6 @@ public class ConstantDef : MemberDef
{
public object Value { get; set; }
public Map<string, string> Annotations { get; set; }
public Tru ValueType { 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,
Index = index,
@@ -137,7 +136,7 @@ public class ConstantDef : MemberDef
Value = value,
FieldInfo = ci,
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;
}
}
+11 -11
View File
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
public class EventDef : MemberDef
{
public Map<string, string> Annotations
{
get;
set;
}
public override string ToString()
{
return $"{Name}: {ArgumentType}";
@@ -35,7 +29,9 @@ public class EventDef : MemberDef
set => AutoDelivered = !value;
}
public bool Deprecated { get; set; }
public bool Historical { get; set; }
public OrderingControl OrderingControl { 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}`");
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;
@@ -209,7 +207,7 @@ public class EventDef : MemberDef
}
return new EventDef()
return DefinitionAttributeReader.Apply(ei, new EventDef()
{
Name = name,
ArgumentType = evtType,
@@ -217,8 +215,10 @@ public class EventDef : MemberDef
Inherited = ei.DeclaringType != type,
Annotations = annotations,
EventInfo = ei,
AutoDelivered = autoDeliveredAttr != null
};
AutoDelivered = autoDeliveryAttr != null,
Historical = historicalAttr != null,
OrderingControl = orderingAttr?.Control ?? OrderingControl.Strict,
});
}
}
+58 -20
View File
@@ -15,12 +15,6 @@ namespace Esiur.Data.Types;
public class FunctionDef : MemberDef
{
public Map<string, string> Annotations
{
get;
set;
}
//public bool IsVoid
//{
// get;
@@ -33,7 +27,7 @@ public class FunctionDef : MemberDef
public bool ReadOnly { get; set; }
public bool Idempotent { get; set; }
public bool Cancellable { get; set; }
public bool Deprecated { get; set; }
public bool Pausable { get; set; }
//public FunctionDefFlags Flags { 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 streamAttribute = mi.GetCustomAttribute<StreamAttribute>(true);
var streamMode = StreamMode.None;
var pausable = false;
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);
}
else if (genericRtType == typeof(Task<>))
else if (genericRtType == typeof(IEnumerable<>))
{
streamMode = StreamMode.Push;
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);
}
else
{
if (mi.ReturnType == typeof(Task))
rtType = Tru.FromType(null, warehouse);
else
rtType = Tru.FromType(mi.ReturnType, warehouse);
rtType = mi.ReturnType == typeof(Task)
? Tru.FromType(null, warehouse)
: 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)
@@ -237,7 +264,12 @@ public class FunctionDef : MemberDef
//var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List<byte>();
//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);
if (rtNullableContextAttrFlag == 2)
@@ -333,7 +365,7 @@ public class FunctionDef : MemberDef
}
return new FunctionDef()
return DefinitionAttributeReader.Apply(mi, new FunctionDef()
{
Name = name,
Index = index,
@@ -342,8 +374,14 @@ public class FunctionDef : MemberDef
ReturnType = rtType,
Arguments = arguments,
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,
Idempotent = 0x10,
Cancellable = 0x20,
Pausable = 0x40,
}
}
}
@@ -61,6 +61,9 @@ public class LocalTypeDef:TypeDef
if (genericType == typeof(List<>)
|| genericType == typeof(PropertyContext<>)
|| genericType == typeof(AsyncReply<>)
|| genericType == typeof(Task<>)
|| genericType == typeof(IEnumerable<>)
|| genericType == typeof(IAsyncEnumerable<>)
|| genericType == typeof(ResourceLink<>))
{
return GetDistributedTypes(genericTypeArgs[0]);
@@ -273,6 +276,8 @@ public class LocalTypeDef:TypeDef
_typeName = GetTypeName(type);
DefinitionAttributeReader.Apply(type, this);
warehouse.TryRegisterLocalTypeDef(this);
+10
View File
@@ -16,6 +16,14 @@ public class MemberDef
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!;
// Human-readable metadata
@@ -54,6 +62,8 @@ public class MemberDef
// Compatibility guidance
public string? DeprecationMessage { get; set; }
public Map<string, string>? Annotations { get; set; }
public string Fullname =>
Definition is null || string.IsNullOrEmpty(Definition.Name)
? Name
@@ -34,5 +34,8 @@ namespace Esiur.Data.Types
// Compatibility guidance
DeprecationMessage = 0x2F, // string
// Extensible application metadata
Annotations = 0x30, // Map<string, string>
}
}
@@ -68,4 +68,7 @@ public class MemberDefInfo : IndexedStructure
[Index((int)MemberDefField.DeprecationMessage)]
public string? DeprecationMessage { get; set; }
[Index((int)MemberDefField.Annotations)]
public Map<string, string>? Annotations { get; set; }
}
+18 -9
View File
@@ -14,11 +14,6 @@ namespace Esiur.Data.Types;
public class PropertyDef : MemberDef
{
public Map<string, string> Annotations { get; set; }
public PropertyInfo PropertyInfo
{
get;
@@ -52,10 +47,15 @@ public class PropertyDef : MemberDef
}
public bool Constant { get; set; }
public bool Volatile { get; set; }
//public bool IsNullable { get; set; }
public bool Historical { get; set; }
public OrderingControl OrderingControl { get; set; }
public object? DefaultValue { get; set; }
public bool HasHistory
{
get => Historical;
@@ -258,6 +258,10 @@ public class PropertyDef : MemberDef
var annotationAttrs = pi.GetCustomAttributes<AnnotationAttribute>(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();
//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,
Index = index,
Inherited = pi.DeclaringType != type,
ValueType = propType,
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,
};
});
}
@@ -325,7 +334,7 @@ public class PropertyDef : MemberDef
PropertyPermission permission, TypeDef typeDef)
{
var definition = MakePropertyDef(warehouse, type, pi, name, index, typeDef);
definition.Permission = permission;
definition.ReadOnly = definition.ReadOnly || permission == PropertyPermission.Read;
return definition;
}
+14 -2
View File
@@ -194,6 +194,11 @@ public class RemoteTypeDef:TypeDef
: $"{info.Namespace}.{info.Name}";
definition._typeDefKind = info.Kind;
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._properties.Clear();
@@ -218,6 +223,7 @@ public class RemoteTypeDef:TypeDef
member.Index = info.Index;
member.Name = info.Name;
member.Inherited = (info.Flags & (byte)MemberDefFlags.Inherited) != 0;
member.Deprecated = (info.Flags & (byte)MemberDefFlags.Deprecated) != 0;
member.Description = info.Description;
member.Usage = info.Usage;
member.Examples = info.Examples;
@@ -234,6 +240,7 @@ public class RemoteTypeDef:TypeDef
member.Warnings = info.Warnings;
member.RelatedMembers = info.RelatedMembers;
member.DeprecationMessage = info.DeprecationMessage;
member.Annotations = info.Annotations;
return member;
}
@@ -245,7 +252,10 @@ public class RemoteTypeDef:TypeDef
ValueType = info.ValueType,
ReadOnly = flags.HasFlag(PropertyDefFlags.ReadOnly),
Constant = flags.HasFlag(PropertyDefFlags.Constant),
Volatile = flags.HasFlag(PropertyDefFlags.Volatile),
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),
Idempotent = flags.HasFlag(FunctionDefFlags.Idempotent),
Cancellable = flags.HasFlag(FunctionDefFlags.Cancellable),
Deprecated = flags.HasFlag(FunctionDefFlags.Deprecated),
Pausable = flags.HasFlag(FunctionDefFlags.Pausable),
Arguments = info.Arguments?.Select(ToArgument).ToArray() ?? Array.Empty<ArgumentDef>(),
});
}
@@ -275,6 +285,7 @@ public class RemoteTypeDef:TypeDef
Optional = flags.HasFlag(ArgumentDefFlags.Optional),
Type = info.ValueType,
DefaultValue = info.DefaultValue,
Annotations = info.Annotations,
};
}
@@ -285,7 +296,8 @@ public class RemoteTypeDef:TypeDef
{
ArgumentType = info.ArgumentType,
AutoDelivered = flags.HasFlag(EventDefFlags.AutoDelivered),
Deprecated = flags.HasFlag(EventDefFlags.Deprecated),
Historical = flags.HasFlag(EventDefFlags.Historical) || info.HistoryControl != 0,
OrderingControl = info.OrderingControl,
});
}
+10
View File
@@ -23,6 +23,16 @@ public class TypeDef
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 List<FunctionDef> _functions = new List<FunctionDef>();
protected List<EventDef> _events = new List<EventDef>();
+16
View File
@@ -66,6 +66,11 @@ public class TypeDefInfo : IndexedStructure
Name = definition.Name,
Kind = definition.Kind,
Parent = definition.ParentTypeId,
Usage = definition.Usage,
Description = definition.Description,
Example = definition.Example,
Category = definition.Category,
Since = definition.Since,
Annotations = definition.Annotations,
Properties = definition.Properties.Select(FromProperty).ToList(),
Functions = definition.Functions.Select(FromFunction).ToList(),
@@ -94,21 +99,26 @@ public class TypeDefInfo : IndexedStructure
target.Warnings = source.Warnings;
target.RelatedMembers = source.RelatedMembers;
target.DeprecationMessage = source.DeprecationMessage;
target.Annotations = source.Annotations;
return target;
}
private static PropertyDefInfo FromProperty(PropertyDef source)
{
var flags = source.Inherited ? PropertyDefFlags.Inherited : PropertyDefFlags.None;
if (source.Deprecated) flags |= PropertyDefFlags.Deprecated;
if (source.ReadOnly) flags |= PropertyDefFlags.ReadOnly;
if (source.Constant) flags |= PropertyDefFlags.Constant;
if (source.Volatile) flags |= PropertyDefFlags.Volatile;
if (source.Historical) flags |= PropertyDefFlags.Historical;
return CopyMember(source, new PropertyDefInfo
{
Flags = (byte)flags,
ValueType = source.ValueType,
OrderingControl = source.OrderingControl,
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.Idempotent) flags |= FunctionDefFlags.Idempotent;
if (source.Cancellable) flags |= FunctionDefFlags.Cancellable;
if (source.Pausable) flags |= FunctionDefFlags.Pausable;
return CopyMember(source, new FunctionDefInfo
{
@@ -139,6 +150,7 @@ public class TypeDefInfo : IndexedStructure
Flags = source.Optional ? (byte)ArgumentDefFlags.Optional : (byte)ArgumentDefFlags.None,
ValueType = source.Type,
DefaultValue = source.DefaultValue,
Annotations = source.Annotations,
};
}
@@ -147,17 +159,21 @@ public class TypeDefInfo : IndexedStructure
var flags = source.Inherited ? EventDefFlags.Inherited : EventDefFlags.None;
if (source.Deprecated) flags |= EventDefFlags.Deprecated;
if (source.AutoDelivered) flags |= EventDefFlags.AutoDelivered;
if (source.Historical) flags |= EventDefFlags.Historical;
return CopyMember(source, new EventDefInfo
{
Flags = (byte)flags,
ArgumentType = source.ArgumentType,
OrderingControl = source.OrderingControl,
HistoryControl = source.Historical ? (byte)1 : (byte)0,
});
}
private static ConstantDefInfo FromConstant(ConstantDef source)
{
var flags = source.Inherited ? ConstantDefFlags.Inherited : ConstantDefFlags.None;
if (source.Deprecated) flags |= ConstantDefFlags.Deprecated;
return CopyMember(source, new ConstantDefInfo
{
Flags = (byte)flags,
+1 -1
View File
@@ -103,7 +103,7 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
if (s == null)
{
Global.Log("NetworkServer", LogType.Error, "sock == null");
//Global.Log("NetworkServer", LogType.Error, "sock == null");
return;
}
@@ -37,6 +37,7 @@ namespace Esiur.Net.Packets
PullStream = 0x1C,
TerminateExecution = 0x1D,
HaltExecution = 0x1E,
ResumeExecution = 0x1F,
}
}
+36 -8
View File
@@ -454,6 +454,7 @@ public partial class EpConnection : NetworkConnection, IStore
public override void Destroy()
{
TerminateInvocations();
UnsubscribeAll();
this.OnReady = null;
this.OnError = null;
@@ -477,7 +478,9 @@ public partial class EpConnection : NetworkConnection, IStore
{
offset += (uint)rt;
if (_packet.Tdu == null)
if (_packet.Tdu == null &&
_packet.Method != EpPacketMethod.Reply &&
_packet.Method != EpPacketMethod.Extension)
return offset;
#if VERBOSE
@@ -514,6 +517,9 @@ public partial class EpConnection : NetworkConnection, IStore
}
else if (_packet.Method == EpPacketMethod.Request)
{
if (IsRateControlBlocked)
return offset;
var dt = _packet.Tdu.Value;
switch (_packet.Request)
@@ -582,37 +588,58 @@ public partial class EpConnection : NetworkConnection, IStore
case EpPacketRequest.StaticCall:
EpRequestStaticCall(_packet.CallbackId, dt);
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)
{
var dt = _packet.Tdu.Value;
var dt = _packet.Tdu;
switch (_packet.Reply)
{
case EpPacketReply.Completed:
EpReplyCompleted(_packet.CallbackId, dt);
break;
case EpPacketReply.Stream:
EpReplyStream(_packet.CallbackId);
break;
case EpPacketReply.Propagated:
EpReplyPropagated(_packet.CallbackId, dt);
if (dt.HasValue)
EpReplyPropagated(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.PermissionError:
EpReplyError(_packet.CallbackId, dt, ErrorType.Management);
if (dt.HasValue)
EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Management);
break;
case EpPacketReply.ExecutionError:
EpReplyError(_packet.CallbackId, dt, ErrorType.Exception);
if (dt.HasValue)
EpReplyError(_packet.CallbackId, dt.Value, ErrorType.Exception);
break;
case EpPacketReply.Progress:
EpReplyProgress(_packet.CallbackId, dt);
if (dt.HasValue)
EpReplyProgress(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.Chunk:
EpReplyChunk(_packet.CallbackId, dt);
if (dt.HasValue)
EpReplyChunk(_packet.CallbackId, dt.Value);
break;
case EpPacketReply.Warning:
EpReplyWarning(_packet.Extension, dt);
if (dt.HasValue)
EpReplyWarning(_packet.CallbackId, dt.Value);
break;
}
@@ -2102,6 +2129,7 @@ public partial class EpConnection : NetworkConnection, IStore
protected override void Disconnected()
{
// clean up
TerminateInvocations();
_authenticated = false;
Status = EpConnectionStatus.Closed;
+538 -102
View File
@@ -31,6 +31,7 @@ using Esiur.Net.Packets;
using Esiur.Resource;
using Esiur.Security.Authority;
using Esiur.Security.Permissions;
using Esiur.Security.RateLimiting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections;
@@ -47,6 +48,140 @@ namespace Esiur.Protocol;
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> _cachedTypeDefs = new KeyList<ulong, RemoteTypeDef>();
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>();
readonly object _invocationsLock = new object();
readonly Dictionary<uint, InvocationContext> _invocations = new Dictionary<uint, InvocationContext>();
volatile int _callbackCounter = 0;
Dictionary<IResource, List<byte>> _subscriptions = new Dictionary<IResource, List<byte>>();
@@ -125,18 +263,25 @@ partial class EpConnection
//callbackCounter++; // avoid thread racing
_requests.Add(c, reply);
SendRequestPacket(action, c, args);
return reply;
}
void SendRequestPacket(EpPacketRequest action, uint callbackId, object[] args)
{
if (args.Length == 0)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x40 | (byte)action))
.AddUInt32(c);
.AddUInt32(callbackId);
Send(bl.ToArray());
}
if (args.Length == 1)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x60 | (byte)action))
.AddUInt32(c)
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
@@ -144,11 +289,39 @@ partial class EpConnection
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x60 | (byte)action))
.AddUInt32(c)
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
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;
}
@@ -304,6 +477,16 @@ partial class EpConnection
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)
{
//var args = new Map<byte, object>();
@@ -324,6 +507,16 @@ partial class EpConnection
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)
{
return SendRequest(EpPacketRequest.SetProperty, instanceId, index, value);
@@ -383,7 +576,7 @@ partial class EpConnection
SendReply(EpPacketReply.Chunk, callbackId, chunk);
}
void EpReplyCompleted(uint callbackId, PlainTdu tdu)
void EpReplyCompleted(uint callbackId, PlainTdu? tdu)
{
var req = _requests.Take(callbackId);
@@ -395,7 +588,19 @@ partial class EpConnection
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)
{
@@ -421,6 +626,12 @@ partial class EpConnection
//}).Error(req.TriggerError);
}
void EpReplyStream(uint callbackId)
{
if (_requests[callbackId] is AsyncStreamReply streamReply)
streamReply.TriggerStreamStarted();
}
void EpExtensionAction(byte actionId, PlainTdu? tdu)
{
// nothing is supported now
@@ -1419,6 +1630,22 @@ partial class EpConnection
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
@@ -1559,59 +1786,44 @@ partial class EpConnection
return;
}
if (rt is IAsyncEnumerable<object>)
if (ft.StreamMode != StreamMode.None)
{
var enu = rt as IAsyncEnumerable<object>;
var enumerator = enu.GetAsyncEnumerator();
Task.Run(async () =>
context ??= new InvocationContext(this, callback);
context.InitializeStream(ft.StreamMode, ft.Pausable);
if (ft.StreamMode == StreamMode.Pull && context.SetAsyncEnumerable(rt))
{
try
{
while (await enumerator.MoveNextAsync())
RegisterInvocation(callback, context);
SendReply(EpPacketReply.Stream, callback);
}
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;
SendChunk(callback, v);
}
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;
if (!context.Ended)
context.Chunk(value);
})
.Warning((level, message) => SendWarning(callback, level, message));
}
catch (Exception ex)
else
{
if (context != null)
context.Ended = true;
var (code, msg) = SummerizeException(ex);
SendError(ErrorType.Exception, callback, code, msg);
context.Ended = true;
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.NotSupported,
$"Stream mode `{ft.StreamMode}` is not compatible with `{rt?.GetType()}`.");
}
}
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)
{
@@ -1683,7 +2106,7 @@ partial class EpConnection
var et = r.Instance.Definition.GetEventDefByIndex(index);
if (et != null)
if (et == null)
{
// et not found
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.MethodNotFound);
@@ -1789,7 +2212,7 @@ partial class EpConnection
var (offset, length, args) = DataDeserializer.LimitedCountListParser(tdu.Data, tdu.PayloadOffset,
tdu.PayloadLength, Instance.Warehouse, 2);
var rid = (uint)args[0];
var rid = Convert.ToUInt32(args[0]);
var index = (byte)args[1];
// un hold the socket to send data immediately
@@ -1806,13 +2229,27 @@ partial class EpConnection
var pt = r.Instance.Definition.GetPropertyDefByIndex(index);
if (pt != null)
if (pt == null)
{
// property not found
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.PropertyNotFound);
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)
{
@@ -1823,25 +2260,27 @@ partial class EpConnection
asyncReply.Then((value) =>
{
// propagation
dynamicResource.SetResourcePropertyAsync(index, value).Then((x) =>
{
SendReply(EpPacketReply.Completed, callback);
}).Error(x =>
{
SendError(x.Type, callback, (ushort)x.Code, x.Message);
});
ExecuteRateControlled(callback, rateControlDelay, () =>
dynamicResource.SetResourcePropertyAsync(index, value).Then((x) =>
{
SendReply(EpPacketReply.Completed, callback);
}).Error(x =>
{
SendError(x.Type, callback, (ushort)x.Code, x.Message);
}));
});
}
else
{
// propagation
dynamicResource.SetResourcePropertyAsync(index, pr.Value).Then((x) =>
{
SendReply(EpPacketReply.Completed, callback);
}).Error(x =>
{
SendError(x.Type, callback, (ushort)x.Code, x.Message);
});
ExecuteRateControlled(callback, rateControlDelay, () =>
dynamicResource.SetResourcePropertyAsync(index, pr.Value).Then((x) =>
{
SendReply(EpPacketReply.Completed, callback);
}).Error(x =>
{
SendError(x.Type, callback, (ushort)x.Code, x.Message);
}));
}
}).Error(x => SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ParseError)); ;
@@ -1856,12 +2295,6 @@ partial class EpConnection
return;
}
if (r.Instance.Applicable(_session, ActionType.SetProperty, pt, this) == Ruling.Denied)
{
SendError(ErrorType.Exception, callback, (ushort)ExceptionCode.SetPropertyDenied);
return;
}
if (!pi.CanWrite)
{
SendError(ErrorType.Management, callback, (ushort)ExceptionCode.ReadOnlyProperty);
@@ -1876,6 +2309,36 @@ partial class EpConnection
if (pr.Value is AsyncReply asyncReply)
{
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()
== typeof(PropertyContext<>))
@@ -1884,7 +2347,6 @@ partial class EpConnection
}
else
{
// cast new value type to property type
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));
}
});
+30 -5
View File
@@ -289,9 +289,33 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
throw new Exception("Function definition not found.");
if (ft.IsStatic)
return _connection.StaticCall(Instance.Definition.Id, index, args);
else
return _connection.SendInvoke(_instanceId, index, args);
return ft.StreamMode == StreamMode.None
? _connection.StaticCall(Instance.Definition.Id, 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)
@@ -634,7 +658,8 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
// this only happens if the programmer forgot to emit in property setter
_properties[index] = value;
reply.Trigger(null);
});
})
.Error(reply.TriggerError);
return reply;
@@ -655,4 +680,4 @@ public class EpResource : DynamicObject, IResource, INotifyPropertyChanged, IDyn
{
Destroy();
}
}
}
+2 -2
View File
@@ -113,7 +113,7 @@ namespace Esiur.Proxy
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;
}
}
}
}
+17 -3
View File
@@ -322,7 +322,7 @@ public static class TypeDefGenerator
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($"namespace {nameSpace} {{");
@@ -355,6 +355,8 @@ public static class TypeDefGenerator
continue;
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 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)
{
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)
rt.Append(", " +
@@ -383,7 +388,7 @@ public static class TypeDefGenerator
}
else
{
rt.Append($"[Export] public AsyncReply<{rtTypeName}> {f.Name}(");
rt.Append($"[Export] public {replyType}<{rtTypeName}> {f.Name}(");
if (positionalArgs.Length > 0)
rt.Append(
@@ -409,6 +414,15 @@ public static class TypeDefGenerator
$"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}>();");
@@ -38,6 +38,8 @@ namespace Esiur.Resource;
AttributeTargets.Field,
AllowMultiple = false,
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
{
/// <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.
/// </summary>
[AttributeUsage(
AttributeTargets.Property |
AttributeTargets.Field,
AttributeTargets.Field |
AttributeTargets.Event,
AllowMultiple = false,
Inherited = true)]
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
{
/// <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>
[AttributeUsage(
AttributeTargets.Method,
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace Esiur.Resource.Attributes
namespace Esiur.Resource
{
[AttributeUsage(
AttributeTargets.Class |
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace Esiur.Resource.Attributes
namespace Esiur.Resource
{
/// <summary>
/// Indicates that an exported property has volatile synchronization
+50 -1
View File
@@ -31,6 +31,7 @@ using Esiur.Protocol;
using Esiur.Proxy;
using Esiur.Security.Authority;
using Esiur.Security.Permissions;
using Esiur.Security.RateLimiting;
using Org.BouncyCastle.Asn1.Cms;
using System;
using System.Collections;
@@ -88,6 +89,8 @@ public class Warehouse
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
List<IPermissionsManager> _permissionsManagers = new List<IPermissionsManager>();
readonly ConcurrentDictionary<string, RatePolicy> _ratePolicies
= new ConcurrentDictionary<string, RatePolicy>(StringComparer.Ordinal);
object _typeDefsLock = new object();
@@ -102,6 +105,11 @@ public class Warehouse
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]*)://([^/]*)/?)");
@@ -121,6 +129,41 @@ public class Warehouse
_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)
{
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",
async (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();
}