mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
470 lines
14 KiB
C#
470 lines
14 KiB
C#
/*
|
|
|
|
Copyright (c) 2017 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.Misc;
|
|
using Esiur.Resource;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Esiur.Core;
|
|
|
|
[AsyncMethodBuilder(typeof(AsyncReplyBuilder))]
|
|
public class AsyncReply
|
|
{
|
|
public DateTime ReadyTime;
|
|
|
|
// These lists are intentionally lazy. Completed replies are common and should not
|
|
// allocate callback storage or a kernel-backed wait primitive unless it is needed.
|
|
protected List<Action<object>> callbacks;
|
|
protected List<Action<AsyncException>> errorCallbacks;
|
|
protected List<Action<ProgressType, uint, uint>> progressCallbacks;
|
|
protected List<Action<object>> chunkCallbacks;
|
|
protected List<Action<object>> propagationCallbacks;
|
|
protected List<Action<byte, string>> warningCallbacks;
|
|
|
|
protected volatile object result;
|
|
protected volatile bool resultReady;
|
|
|
|
private readonly object asyncLock = new object();
|
|
private volatile AsyncException exception;
|
|
private Exception observedException;
|
|
private ManualResetEventSlim completionEvent;
|
|
|
|
public static int MaxId;
|
|
public int Id;
|
|
|
|
protected string codePath, codeMethod;
|
|
protected int codeLine;
|
|
|
|
public bool Ready => resultReady;
|
|
|
|
public bool Failed => exception != null;
|
|
|
|
public Exception Exception => exception;
|
|
|
|
public object Result => result;
|
|
|
|
public static AsyncReply<T> FromResult<T>(T result) => new AsyncReply<T>(result);
|
|
|
|
public object Wait()
|
|
{
|
|
ManualResetEventSlim waiter;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (resultReady)
|
|
return result;
|
|
if (exception != null)
|
|
throw observedException ?? exception;
|
|
|
|
waiter = completionEvent ?? (completionEvent = new ManualResetEventSlim(false));
|
|
}
|
|
|
|
waiter.Wait();
|
|
return GetWaitResult();
|
|
}
|
|
|
|
public object Wait(int millisecondsTimeout)
|
|
{
|
|
ManualResetEventSlim waiter;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (resultReady)
|
|
return result;
|
|
if (exception != null)
|
|
throw observedException ?? exception;
|
|
|
|
waiter = completionEvent ?? (completionEvent = new ManualResetEventSlim(false));
|
|
}
|
|
|
|
if (!waiter.Wait(millisecondsTimeout))
|
|
{
|
|
var timeoutException = new Exception("AsyncReply timeout");
|
|
|
|
// If completion won the timeout race, return that terminal state. Otherwise
|
|
// retain the timeout on the reply and preserve Wait's historical exception.
|
|
if (TrySetException(timeoutException))
|
|
throw timeoutException;
|
|
}
|
|
|
|
return GetWaitResult();
|
|
}
|
|
|
|
public void Timeout(int milliseconds, Action callback = null)
|
|
{
|
|
_ = Task.Delay(milliseconds).ContinueWith(_ =>
|
|
{
|
|
var timeoutException = new AsyncException(
|
|
ErrorType.Management,
|
|
(ushort)ExceptionCode.Timeout,
|
|
"Execution timeout expired.");
|
|
|
|
if (TrySetException(timeoutException))
|
|
callback?.Invoke();
|
|
});
|
|
}
|
|
|
|
public AsyncReply Then(
|
|
Action<object> callback,
|
|
[CallerMemberName] string methodName = null,
|
|
[CallerFilePath] string filePath = null,
|
|
[CallerLineNumber] int lineNumber = 0)
|
|
{
|
|
object completedResult = null;
|
|
var invokeImmediately = false;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (codeLine == 0)
|
|
{
|
|
codeLine = lineNumber;
|
|
codeMethod = methodName;
|
|
codePath = filePath;
|
|
}
|
|
|
|
if (resultReady)
|
|
{
|
|
completedResult = result;
|
|
invokeImmediately = true;
|
|
}
|
|
else if (exception == null)
|
|
{
|
|
(callbacks ?? (callbacks = new List<Action<object>>())).Add(callback);
|
|
}
|
|
}
|
|
|
|
if (invokeImmediately)
|
|
callback(completedResult);
|
|
|
|
return this;
|
|
}
|
|
|
|
public AsyncReply Error(Action<AsyncException> callback)
|
|
{
|
|
AsyncException completedException = null;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (exception != null)
|
|
{
|
|
completedException = exception;
|
|
}
|
|
else if (!resultReady)
|
|
{
|
|
(errorCallbacks ?? (errorCallbacks = new List<Action<AsyncException>>())).Add(callback);
|
|
}
|
|
}
|
|
|
|
if (completedException != null)
|
|
callback(completedException);
|
|
|
|
return this;
|
|
}
|
|
|
|
public AsyncReply Progress(Action<ProgressType, uint, uint> callback)
|
|
{
|
|
lock (asyncLock)
|
|
(progressCallbacks ?? (progressCallbacks = new List<Action<ProgressType, uint, uint>>())).Add(callback);
|
|
|
|
return this;
|
|
}
|
|
|
|
public AsyncReply Warning(Action<byte, string> callback)
|
|
{
|
|
lock (asyncLock)
|
|
(warningCallbacks ?? (warningCallbacks = new List<Action<byte, string>>())).Add(callback);
|
|
|
|
return this;
|
|
}
|
|
|
|
public AsyncReply Chunk(Action<object> callback)
|
|
{
|
|
lock (asyncLock)
|
|
(chunkCallbacks ?? (chunkCallbacks = new List<Action<object>>())).Add(callback);
|
|
|
|
return this;
|
|
}
|
|
|
|
public AsyncReply Propagation(Action<object> callback)
|
|
{
|
|
lock (asyncLock)
|
|
(propagationCallbacks ?? (propagationCallbacks = new List<Action<object>>())).Add(callback);
|
|
|
|
return this;
|
|
}
|
|
|
|
public void Trigger(object result) => TrySetResult(result, dispatchCallbacksAsynchronously: false);
|
|
|
|
/// <summary>
|
|
/// Completes the reply immediately while dispatching callbacks independently.
|
|
/// This is reserved for teardown paths where consumer code must not hold a
|
|
/// transport or other infrastructure resource open.
|
|
/// </summary>
|
|
internal void TriggerDetached(object result) =>
|
|
TrySetResult(result, dispatchCallbacksAsynchronously: true);
|
|
|
|
private bool TrySetResult(object result, bool dispatchCallbacksAsynchronously)
|
|
{
|
|
Action<object> singleCallback = null;
|
|
Action<object>[] registeredCallbacks = null;
|
|
var callbackCount = 0;
|
|
ManualResetEventSlim waiter;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (exception != null || resultReady)
|
|
return false;
|
|
|
|
ReadyTime = DateTime.Now;
|
|
this.result = result;
|
|
resultReady = true;
|
|
|
|
// AsyncQueue deliberately resets resultReady between deliveries. Snapshot a
|
|
// multi-callback list before unlocking, while keeping its usual one-callback
|
|
// delivery path allocation-free.
|
|
if (callbacks != null)
|
|
{
|
|
callbackCount = callbacks.Count;
|
|
if (callbackCount == 1)
|
|
singleCallback = callbacks[0];
|
|
else if (callbackCount > 1)
|
|
registeredCallbacks = callbacks.ToArray();
|
|
}
|
|
|
|
waiter = completionEvent;
|
|
}
|
|
|
|
waiter?.Set();
|
|
|
|
if (dispatchCallbacksAsynchronously)
|
|
{
|
|
if (callbackCount == 1)
|
|
QueueDetachedCallbacks(singleCallback, null, result);
|
|
else if (registeredCallbacks != null)
|
|
QueueDetachedCallbacks(null, registeredCallbacks, result);
|
|
}
|
|
else if (callbackCount == 1)
|
|
{
|
|
singleCallback(result);
|
|
}
|
|
else if (registeredCallbacks != null)
|
|
{
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(result);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual void TriggerError(Exception exception)
|
|
{
|
|
TrySetException(exception);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Faults the reply immediately while dispatching callbacks independently.
|
|
/// This is reserved for teardown paths where consumer code must not hold a
|
|
/// transport or other infrastructure resource open.
|
|
/// </summary>
|
|
internal void TriggerErrorDetached(Exception exception)
|
|
{
|
|
TrySetException(
|
|
exception,
|
|
preserveSourceForAwait: false,
|
|
dispatchCallbacksAsynchronously: true);
|
|
}
|
|
|
|
internal void TriggerErrorFromBuilder(Exception exception, bool preserveSourceForAwait)
|
|
{
|
|
TrySetException(exception, preserveSourceForAwait);
|
|
}
|
|
|
|
internal Exception GetExceptionForAwait()
|
|
{
|
|
lock (asyncLock)
|
|
return observedException ?? exception;
|
|
}
|
|
|
|
public void TriggerProgress(ProgressType type, uint value, uint max)
|
|
{
|
|
Action<ProgressType, uint, uint>[] registeredCallbacks;
|
|
|
|
lock (asyncLock)
|
|
registeredCallbacks = progressCallbacks?.ToArray();
|
|
|
|
if (registeredCallbacks != null)
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(type, value, max);
|
|
}
|
|
|
|
public void TriggerWarning(byte level, string message)
|
|
{
|
|
Action<byte, string>[] registeredCallbacks;
|
|
|
|
lock (asyncLock)
|
|
registeredCallbacks = warningCallbacks?.ToArray();
|
|
|
|
if (registeredCallbacks != null)
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(level, message);
|
|
}
|
|
|
|
public void TriggerPropagation(object value)
|
|
{
|
|
Action<object>[] registeredCallbacks;
|
|
|
|
lock (asyncLock)
|
|
registeredCallbacks = propagationCallbacks?.ToArray();
|
|
|
|
if (registeredCallbacks != null)
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(value);
|
|
}
|
|
|
|
public void TriggerChunk(object value)
|
|
{
|
|
Action<object>[] registeredCallbacks;
|
|
|
|
lock (asyncLock)
|
|
registeredCallbacks = chunkCallbacks?.ToArray();
|
|
|
|
if (registeredCallbacks != null)
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(value);
|
|
}
|
|
|
|
public AsyncAwaiter GetAwaiter() => new AsyncAwaiter(this);
|
|
|
|
public AsyncReply()
|
|
{
|
|
Id = Interlocked.Increment(ref MaxId) - 1;
|
|
}
|
|
|
|
public AsyncReply(object result)
|
|
{
|
|
ReadyTime = DateTime.Now;
|
|
resultReady = true;
|
|
this.result = result;
|
|
Id = Interlocked.Increment(ref MaxId) - 1;
|
|
}
|
|
|
|
private object GetWaitResult()
|
|
{
|
|
lock (asyncLock)
|
|
{
|
|
if (exception != null)
|
|
throw observedException ?? exception;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private bool TrySetException(
|
|
Exception sourceException,
|
|
bool preserveSourceForAwait = false,
|
|
bool dispatchCallbacksAsynchronously = false)
|
|
{
|
|
AsyncException asyncException;
|
|
Action<AsyncException> singleCallback = null;
|
|
Action<AsyncException>[] registeredCallbacks = null;
|
|
var callbackCount = 0;
|
|
ManualResetEventSlim waiter;
|
|
|
|
lock (asyncLock)
|
|
{
|
|
if (resultReady || exception != null)
|
|
return false;
|
|
|
|
asyncException = sourceException as AsyncException ?? new AsyncException(sourceException);
|
|
observedException = preserveSourceForAwait ? sourceException : asyncException;
|
|
exception = asyncException;
|
|
|
|
if (errorCallbacks != null)
|
|
{
|
|
callbackCount = errorCallbacks.Count;
|
|
if (callbackCount == 1)
|
|
singleCallback = errorCallbacks[0];
|
|
else if (callbackCount > 1)
|
|
registeredCallbacks = errorCallbacks.ToArray();
|
|
}
|
|
|
|
waiter = completionEvent;
|
|
}
|
|
|
|
waiter?.Set();
|
|
|
|
if (dispatchCallbacksAsynchronously)
|
|
{
|
|
if (callbackCount == 1)
|
|
QueueDetachedCallbacks(singleCallback, null, asyncException);
|
|
else if (registeredCallbacks != null)
|
|
QueueDetachedCallbacks(null, registeredCallbacks, asyncException);
|
|
}
|
|
else if (callbackCount == 1)
|
|
{
|
|
singleCallback(asyncException);
|
|
}
|
|
else if (registeredCallbacks != null)
|
|
{
|
|
foreach (var callback in registeredCallbacks)
|
|
callback(asyncException);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void QueueDetachedCallbacks<T>(
|
|
Action<T> singleCallback,
|
|
Action<T>[] registeredCallbacks,
|
|
T value)
|
|
{
|
|
ThreadPool.QueueUserWorkItem(_ =>
|
|
{
|
|
try
|
|
{
|
|
if (singleCallback != null)
|
|
{
|
|
singleCallback(value);
|
|
}
|
|
else
|
|
{
|
|
foreach (var callback in registeredCallbacks)
|
|
{
|
|
try { callback(value); }
|
|
catch (Exception exception) { Global.Log(exception); }
|
|
}
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Global.Log(exception);
|
|
}
|
|
});
|
|
}
|
|
}
|