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;
|
||||
}
|
||||
|
||||
public void TriggerError(Exception exception)
|
||||
public virtual void TriggerError(Exception exception)
|
||||
{
|
||||
//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,
|
||||
NotSupported,
|
||||
NotImplemented,
|
||||
NotAllowed
|
||||
NotAllowed,
|
||||
RateLimitExceeded
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user