/* 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> callbacks; protected List> errorCallbacks; protected List> progressCallbacks; protected List> chunkCallbacks; protected List> propagationCallbacks; protected List> 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 FromResult(T result) => new AsyncReply(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 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>())).Add(callback); } } if (invokeImmediately) callback(completedResult); return this; } public AsyncReply Error(Action callback) { AsyncException completedException = null; lock (asyncLock) { if (exception != null) { completedException = exception; } else if (!resultReady) { (errorCallbacks ?? (errorCallbacks = new List>())).Add(callback); } } if (completedException != null) callback(completedException); return this; } public AsyncReply Progress(Action callback) { lock (asyncLock) (progressCallbacks ?? (progressCallbacks = new List>())).Add(callback); return this; } public AsyncReply Warning(Action callback) { lock (asyncLock) (warningCallbacks ?? (warningCallbacks = new List>())).Add(callback); return this; } public AsyncReply Chunk(Action callback) { lock (asyncLock) (chunkCallbacks ?? (chunkCallbacks = new List>())).Add(callback); return this; } public AsyncReply Propagation(Action callback) { lock (asyncLock) (propagationCallbacks ?? (propagationCallbacks = new List>())).Add(callback); return this; } public void Trigger(object result) => TrySetResult(result, dispatchCallbacksAsynchronously: false); /// /// 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. /// internal void TriggerDetached(object result) => TrySetResult(result, dispatchCallbacksAsynchronously: true); private bool TrySetResult(object result, bool dispatchCallbacksAsynchronously) { Action singleCallback = null; Action[] 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); } /// /// 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. /// 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[] 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[] registeredCallbacks; lock (asyncLock) registeredCallbacks = warningCallbacks?.ToArray(); if (registeredCallbacks != null) foreach (var callback in registeredCallbacks) callback(level, message); } public void TriggerPropagation(object value) { Action[] registeredCallbacks; lock (asyncLock) registeredCallbacks = propagationCallbacks?.ToArray(); if (registeredCallbacks != null) foreach (var callback in registeredCallbacks) callback(value); } public void TriggerChunk(object value) { Action[] 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 singleCallback = null; Action[] 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( Action singleCallback, Action[] 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); } }); } }