From 99b8249b63cd62ed154f8940e9dee7d625cb5fd0 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 31 Dec 2024 19:12:49 -0500 Subject: [PATCH 1/2] Added `Promise().WaitAsync` APIs with timeout parameter. --- Package/Core/InternalShared/PoolInternal.cs | 2 +- .../Internal/CallbackHelperInternal.cs | 120 ++++- .../Core/Promises/Internal/CancelInternal.cs | 62 --- .../Core/Promises/Internal/DelayInternal.cs | 82 ++- .../Internal/PromiseFieldsInternal.cs | 55 +- .../PromiseSynchronousWaiterInternal.cs | 6 + .../Promises/Internal/WaitAsyncInternal.cs | 405 +++++++++++++++ Package/Core/Promises/Promise.cs | 50 ++ Package/Core/Promises/PromiseStatic.cs | 10 +- Package/Core/Promises/PromiseT.cs | 50 ++ Package/Tests/CoreTests/APIs/DelayTests.cs | 118 ++--- .../Tests/CoreTests/APIs/WaitAsyncTests.cs | 474 +++++++++++++++++- 12 files changed, 1218 insertions(+), 216 deletions(-) create mode 100644 Package/Core/Promises/Internal/WaitAsyncInternal.cs diff --git a/Package/Core/InternalShared/PoolInternal.cs b/Package/Core/InternalShared/PoolInternal.cs index f076b5bd..84ec9830 100644 --- a/Package/Core/InternalShared/PoolInternal.cs +++ b/Package/Core/InternalShared/PoolInternal.cs @@ -317,7 +317,7 @@ internal static void AssertAllObjectsReleased() { // Some objects could be used on a ThreadPool thread that we can't control, like Promise.Delay using system timer. // Wait a bit of time for it to settle before asserting. - System.Threading.SpinWait.SpinUntil(() => s_inUseObjects.Count == 0, TimeSpan.FromSeconds(Environment.ProcessorCount)); + System.Threading.SpinWait.SpinUntil(() => s_inUseObjects.Count == 0, TimeSpan.FromSeconds(1)); lock (s_pooledObjects) { if (s_inUseObjects.Count > 0) diff --git a/Package/Core/Promises/Internal/CallbackHelperInternal.cs b/Package/Core/Promises/Internal/CallbackHelperInternal.cs index e8eb857c..17e2570e 100644 --- a/Package/Core/Promises/Internal/CallbackHelperInternal.cs +++ b/Package/Core/Promises/Internal/CallbackHelperInternal.cs @@ -6,9 +6,11 @@ #pragma warning disable IDE0074 // Use compound assignment +using Proto.Timers; using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; namespace Proto.Promises { @@ -272,23 +274,61 @@ internal static Promise Duplicate(Promise _this) internal static Promise WaitAsync(Promise _this, CancelationToken cancelationToken) { - if (_this._ref == null) + if (cancelationToken.IsCancelationRequested) { - return cancelationToken.IsCancelationRequested - ? Promise.Canceled() - : _this; + return Canceled(_this._ref, _this._id); } - PromiseRef promise; - if (cancelationToken.CanBeCanceled) + if (_this._ref?.State != Promise.State.Pending || !cancelationToken.CanBeCanceled) { - var p = PromiseDuplicateCancel.GetOrCreate(); - promise = _this._ref.HookupCancelablePromise(p, _this._id, cancelationToken, ref p._cancelationHelper); + return Duplicate(_this); } - else + var promise = WaitAsyncWithCancelationPromise.GetOrCreate(); + _this._ref.HookupCancelablePromise(promise, _this._id, cancelationToken, ref promise._cancelationHelper); + return new Promise(promise, promise.Id, _this._result); + } + + internal static Promise WaitAsync(Promise _this, TimeSpan timeout, TimerFactory timerFactory) + { + if (_this._ref?.State != Promise.State.Pending || timeout == Timeout.InfiniteTimeSpan) { - promise = _this._ref.GetDuplicateT(_this._id); + return Duplicate(_this); } - return new Promise(promise, promise.Id, _this._result); + if (timeout == TimeSpan.Zero) + { + _this._ref?.MaybeMarkAwaitedAndDispose(_this._id); + return Promise.Rejected(new TimeoutException()); + } + var promise = WaitAsyncWithTimeoutPromise.GetOrCreate(timeout, timerFactory); + _this._ref.HookupNewPromise(_this._id, promise); + return new Promise(promise, promise.Id); + } + + internal static Promise WaitAsync(Promise _this, TimeSpan timeout, TimerFactory timerFactory, CancelationToken cancelationToken) + { + if (!cancelationToken.CanBeCanceled) + { + return WaitAsync(_this, timeout, timerFactory); + } + if (timeout == Timeout.InfiniteTimeSpan) + { + return WaitAsync(_this, cancelationToken); + } + + if (cancelationToken.IsCancelationRequested) + { + return Canceled(_this._ref, _this._id); + } + if (_this._ref?.State != Promise.State.Pending) + { + return Duplicate(_this); + } + if (timeout == TimeSpan.Zero) + { + _this._ref?.MaybeMarkAwaitedAndDispose(_this._id); + return Promise.Rejected(new TimeoutException()); + } + var promise = WaitAsyncWithTimeoutAndCancelationPromise.GetOrCreateAndHookup(_this._ref, _this._id, timeout, timerFactory, cancelationToken); + return new Promise(promise, promise.Id); } internal static Promise ConfigureContinuation(Promise _this, ContinuationOptions continuationOptions) @@ -778,22 +818,60 @@ internal static Promise Duplicate(Promise _this) internal static Promise WaitAsync(Promise _this, CancelationToken cancelationToken) { - if (_this._ref == null) + if (cancelationToken.IsCancelationRequested) { - return cancelationToken.IsCancelationRequested - ? Promise.Canceled() - : _this; + return Canceled(_this._ref, _this._id); } - PromiseRefBase promise; - if (cancelationToken.CanBeCanceled) + if (_this._ref?.State != Promise.State.Pending || !cancelationToken.CanBeCanceled) { - var p = PromiseDuplicateCancel.GetOrCreate(); - promise = _this._ref.HookupCancelablePromise(p, _this._id, cancelationToken, ref p._cancelationHelper); + return Duplicate(_this); } - else + var promise = WaitAsyncWithCancelationPromise.GetOrCreate(); + _this._ref.HookupCancelablePromise(promise, _this._id, cancelationToken, ref promise._cancelationHelper); + return new Promise(promise, promise.Id); + } + + internal static Promise WaitAsync(Promise _this, TimeSpan timeout, TimerFactory timerFactory) + { + if (_this._ref?.State != Promise.State.Pending || timeout == Timeout.InfiniteTimeSpan) + { + return Duplicate(_this); + } + if (timeout == TimeSpan.Zero) + { + _this._ref?.MaybeMarkAwaitedAndDispose(_this._id); + return Promise.Rejected(new TimeoutException()); + } + var promise = WaitAsyncWithTimeoutPromise.GetOrCreate(timeout, timerFactory); + _this._ref.HookupNewPromise(_this._id, promise); + return new Promise(promise, promise.Id); + } + + internal static Promise WaitAsync(Promise _this, TimeSpan timeout, TimerFactory timerFactory, CancelationToken cancelationToken) + { + if (!cancelationToken.CanBeCanceled) + { + return WaitAsync(_this, timeout, timerFactory); + } + if (timeout == Timeout.InfiniteTimeSpan) + { + return WaitAsync(_this, cancelationToken); + } + + if (cancelationToken.IsCancelationRequested) + { + return Canceled(_this._ref, _this._id); + } + if (_this._ref?.State != Promise.State.Pending) + { + return Duplicate(_this); + } + if (timeout == TimeSpan.Zero) { - promise = _this._ref.GetDuplicate(_this._id); + _this._ref?.MaybeMarkAwaitedAndDispose(_this._id); + return Promise.Rejected(new TimeoutException()); } + var promise = WaitAsyncWithTimeoutAndCancelationPromise.GetOrCreateAndHookup(_this._ref, _this._id, timeout, timerFactory, cancelationToken); return new Promise(promise, promise.Id); } diff --git a/Package/Core/Promises/Internal/CancelInternal.cs b/Package/Core/Promises/Internal/CancelInternal.cs index d3cc8a5f..812a45cb 100644 --- a/Package/Core/Promises/Internal/CancelInternal.cs +++ b/Package/Core/Promises/Internal/CancelInternal.cs @@ -93,68 +93,6 @@ internal void ReleaseOne() => _retainCounter = 1; } -#if !PROTO_PROMISE_DEVELOPER_MODE - [DebuggerNonUserCode, StackTraceHidden] -#endif - internal sealed partial class PromiseDuplicateCancel : PromiseSingleAwait, ICancelable - { - private PromiseDuplicateCancel() { } - - internal override void MaybeDispose() - { - if (_cancelationHelper.TryRelease()) - { - Dispose(); - _cancelationHelper = default; - ObjectPool.MaybeRepool(this); - } - } - - [MethodImpl(InlineOption)] - private static PromiseDuplicateCancel GetOrCreateInstance() - { - var obj = ObjectPool.TryTakeOrInvalid>(); - return obj == InvalidAwaitSentinel.s_instance - ? new PromiseDuplicateCancel() - : obj.UnsafeAs>(); - } - - [MethodImpl(InlineOption)] - internal static PromiseDuplicateCancel GetOrCreate() - { - var promise = GetOrCreateInstance(); - promise.Reset(); - promise._cancelationHelper.Reset(); - return promise; - } - - internal override void Handle(PromiseRefBase handler, Promise.State state) - { - ThrowIfInPool(this); - handler.SetCompletionState(state); - if (_cancelationHelper.TrySetCompleted()) - { - _cancelationHelper.UnregisterAndWait(); - _cancelationHelper.ReleaseOne(); - HandleSelf(handler, state); - } - else - { - MaybeDispose(); - handler.MaybeReportUnhandledAndDispose(state); - } - } - - void ICancelable.Cancel() - { - ThrowIfInPool(this); - if (_cancelationHelper.TrySetCompleted()) - { - HandleNextInternal(Promise.State.Canceled); - } - } - } - #if !PROTO_PROMISE_DEVELOPER_MODE [DebuggerNonUserCode, StackTraceHidden] #endif diff --git a/Package/Core/Promises/Internal/DelayInternal.cs b/Package/Core/Promises/Internal/DelayInternal.cs index 17f2b4ef..ec5b4171 100644 --- a/Package/Core/Promises/Internal/DelayInternal.cs +++ b/Package/Core/Promises/Internal/DelayInternal.cs @@ -21,30 +21,6 @@ partial class PromiseRefBase #endif internal sealed partial class DelayPromise : PromiseSingleAwait { - // Use ITimerSource and int directly instead of the Timer struct - // so that the fields can be packed efficiently without extra padding. - private ITimerSource _timerSource; - private int _timerToken; - // The timer callback can be invoked before the fields are actually assigned, - // so we use an Interlocked counter to ensure it is disposed properly. - private int _timerUseCounter; - - private void MaybeDisposeTimer() - { - ThrowIfInPool(this); - if (InterlockedAddWithUnsignedOverflowCheck(ref _timerUseCounter, -1) == 0) - { - _timerSource.DisposeAsync(_timerToken).Forget(); - } - } - - internal override void MaybeDispose() - { - Dispose(); - _timerSource = null; - ObjectPool.MaybeRepool(this); - } - [MethodImpl(InlineOption)] private static DelayPromise GetFromPoolOrCreate() { @@ -73,42 +49,34 @@ internal static DelayPromise GetOrCreate(TimeSpan delay, TimerFactory timerFacto return promise; } - private void OnTimerCallback() - { - MaybeDisposeTimer(); - HandleNextInternal(Promise.State.Resolved); - } - } - -#if !PROTO_PROMISE_DEVELOPER_MODE - [DebuggerNonUserCode, StackTraceHidden] -#endif - internal sealed partial class DelayWithCancelationPromise : PromiseSingleAwait, ICancelable - { - private Timers.Timer _timer; - // The timer and cancelation callbacks can race on different threads, - // and can be invoked before the fields are actually assigned; - // we use CancelationHelper to make sure they are used and disposed properly. - private CancelationHelper _cancelationHelper; - - private void MaybeDisposeFields() + private void MaybeDisposeTimer() { ThrowIfInPool(this); - if (_cancelationHelper.TryRelease()) + if (InterlockedAddWithUnsignedOverflowCheck(ref _timerUseCounter, -1) == 0) { - _cancelationHelper.UnregisterAndWait(); - _timer.DisposeAsync().Forget(); + _timerSource.DisposeAsync(_timerToken).Forget(); } } internal override void MaybeDispose() { Dispose(); - _cancelationHelper = default; - _timer = default; + _timerSource = null; ObjectPool.MaybeRepool(this); } + private void OnTimerCallback() + { + MaybeDisposeTimer(); + HandleNextInternal(Promise.State.Resolved); + } + } + +#if !PROTO_PROMISE_DEVELOPER_MODE + [DebuggerNonUserCode, StackTraceHidden] +#endif + internal sealed partial class DelayWithCancelationPromise : PromiseSingleAwait, ICancelable + { [MethodImpl(InlineOption)] private static DelayWithCancelationPromise GetFromPoolOrCreate() { @@ -138,6 +106,24 @@ internal static DelayWithCancelationPromise GetOrCreate(TimeSpan delay, TimerFac return promise; } + private void MaybeDisposeFields() + { + ThrowIfInPool(this); + if (_cancelationHelper.TryRelease()) + { + _cancelationHelper.UnregisterAndWait(); + _timer.DisposeAsync().Forget(); + } + } + + internal override void MaybeDispose() + { + Dispose(); + _cancelationHelper = default; + _timer = default; + ObjectPool.MaybeRepool(this); + } + private void OnTimerCallback() { ThrowIfInPool(this); diff --git a/Package/Core/Promises/Internal/PromiseFieldsInternal.cs b/Package/Core/Promises/Internal/PromiseFieldsInternal.cs index 84df349c..8e33c580 100644 --- a/Package/Core/Promises/Internal/PromiseFieldsInternal.cs +++ b/Package/Core/Promises/Internal/PromiseFieldsInternal.cs @@ -18,6 +18,7 @@ #pragma warning disable IDE0090 // Use 'new(...)' using Proto.Promises.Collections; +using Proto.Timers; using System; using System.Collections.Generic; using System.Diagnostics; @@ -84,12 +85,6 @@ partial class HandleablePromiseBase partial class PromiseSynchronousWaiter : HandleablePromiseBase { - private const int InitialState = 0; - private const int WaitingState = 1; - private const int CompletedState = 2; - private const int WaitedSuccessState = 3; - private const int WaitedFailedState = 4; - volatile private int _waitState; // int for Interlocked. } @@ -161,11 +156,57 @@ partial class PromiseDuplicate : PromiseSingleAwait { } - partial class PromiseDuplicateCancel : PromiseSingleAwait + partial class WaitAsyncWithCancelationPromise : PromiseSingleAwait { internal CancelationHelper _cancelationHelper; } + partial class WaitAsyncWithTimeoutPromise : PromiseSingleAwait + { + private Timers.Timer _timer; + // We're waiting on a promise and a timer, + // so we use an Interlocked counter to ensure this is disposed properly. + private int _retainCounter; + // The timer callback can be invoked before the field is actually assigned, + // and the promise can be completed and the timer fired concurrently from different threads, + // so we use an Interlocked state to ensure this is completed and the timer is disposed properly. + private int _waitState; + } + + partial class WaitAsyncWithTimeoutAndCancelationPromise : PromiseSingleAwait + { + private Timers.Timer _timer; + private CancelationRegistration _cancelationRegistration; + // We're waiting on a promise, a timer, and a cancelation token, + // so we use an Interlocked counter to ensure this is disposed properly. + private int _retainCounter; + // The awaited promise, the timer, and the cancelation can race on different threads, + // and can be invoked before the fields are actually assigned, + // so we use an Interlocked state to ensure this is completed and the timer + // and cancelation registration are used and disposed properly. + private int _waitState; + } + + partial class DelayPromise : PromiseSingleAwait + { + // Use ITimerSource and int directly instead of the Timer struct + // so that the fields can be packed efficiently without extra padding. + private ITimerSource _timerSource; + private int _timerToken; + // The timer callback can be invoked before the fields are actually assigned, + // so we use an Interlocked counter to ensure it is disposed properly. + private int _timerUseCounter; + } + + partial class DelayWithCancelationPromise : PromiseSingleAwait + { + private Timers.Timer _timer; + // The timer and cancelation callbacks can race on different threads, + // and can be invoked before the fields are actually assigned; + // we use CancelationHelper to make sure they are used and disposed properly. + private CancelationHelper _cancelationHelper; + } + partial class ConfiguredPromise : PromiseSingleAwait { private SynchronizationContext _synchronizationContext; diff --git a/Package/Core/Promises/Internal/PromiseSynchronousWaiterInternal.cs b/Package/Core/Promises/Internal/PromiseSynchronousWaiterInternal.cs index 25b1d92d..0f976cd9 100644 --- a/Package/Core/Promises/Internal/PromiseSynchronousWaiterInternal.cs +++ b/Package/Core/Promises/Internal/PromiseSynchronousWaiterInternal.cs @@ -18,6 +18,12 @@ partial class Internal #endif internal sealed partial class PromiseSynchronousWaiter : HandleablePromiseBase { + private const int InitialState = 0; + private const int WaitingState = 1; + private const int CompletedState = 2; + private const int WaitedSuccessState = 3; + private const int WaitedFailedState = 4; + private PromiseSynchronousWaiter() { } [MethodImpl(InlineOption)] diff --git a/Package/Core/Promises/Internal/WaitAsyncInternal.cs b/Package/Core/Promises/Internal/WaitAsyncInternal.cs new file mode 100644 index 00000000..a7359323 --- /dev/null +++ b/Package/Core/Promises/Internal/WaitAsyncInternal.cs @@ -0,0 +1,405 @@ +#if PROTO_PROMISE_DEBUG_ENABLE || (!PROTO_PROMISE_DEBUG_DISABLE && DEBUG) +#define PROMISE_DEBUG +#else +#undef PROMISE_DEBUG +#endif + +using Proto.Timers; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Proto.Promises +{ + partial class Internal + { + partial class PromiseRefBase + { +#if !PROTO_PROMISE_DEVELOPER_MODE + [DebuggerNonUserCode, StackTraceHidden] +#endif + internal sealed partial class WaitAsyncWithCancelationPromise : PromiseSingleAwait, ICancelable + { + private WaitAsyncWithCancelationPromise() { } + + [MethodImpl(InlineOption)] + private static WaitAsyncWithCancelationPromise GetOrCreateInstance() + { + var obj = ObjectPool.TryTakeOrInvalid>(); + return obj == InvalidAwaitSentinel.s_instance + ? new WaitAsyncWithCancelationPromise() + : obj.UnsafeAs>(); + } + + [MethodImpl(InlineOption)] + internal static WaitAsyncWithCancelationPromise GetOrCreate() + { + var promise = GetOrCreateInstance(); + promise.Reset(); + promise._cancelationHelper.Reset(); + return promise; + } + + internal override void MaybeDispose() + { + if (_cancelationHelper.TryRelease()) + { + Dispose(); + _cancelationHelper = default; + ObjectPool.MaybeRepool(this); + } + } + + internal override void Handle(PromiseRefBase handler, Promise.State state) + { + ThrowIfInPool(this); + handler.SetCompletionState(state); + if (_cancelationHelper.TrySetCompleted()) + { + _cancelationHelper.UnregisterAndWait(); + _cancelationHelper.ReleaseOne(); + HandleSelf(handler, state); + } + else + { + MaybeDispose(); + handler.MaybeReportUnhandledAndDispose(state); + } + } + + void ICancelable.Cancel() + { + ThrowIfInPool(this); + if (_cancelationHelper.TrySetCompleted()) + { + HandleNextInternal(Promise.State.Canceled); + } + } + } + + // A non-generic class to hold the constants so that they won't need to be duplicated in the generic types in the runtime. + // This also acts as a pseudo-enum (since old runtimes don't support Interlocked operations on enums). +#if !PROTO_PROMISE_DEVELOPER_MODE + [DebuggerNonUserCode, StackTraceHidden] +#endif + private static class WaitAsyncState + { + internal const int Initial = 0; + internal const int Waiting = 1; + internal const int Completed = 2; + + } + +#if !PROTO_PROMISE_DEVELOPER_MODE + [DebuggerNonUserCode, StackTraceHidden] +#endif + internal sealed partial class WaitAsyncWithTimeoutPromise : PromiseSingleAwait + { + private WaitAsyncWithTimeoutPromise() { } + + [MethodImpl(InlineOption)] + private static WaitAsyncWithTimeoutPromise GetOrCreateInstance() + { + var obj = ObjectPool.TryTakeOrInvalid>(); + return obj == InvalidAwaitSentinel.s_instance + ? new WaitAsyncWithTimeoutPromise() + : obj.UnsafeAs>(); + } + + [MethodImpl(InlineOption)] + internal static WaitAsyncWithTimeoutPromise GetOrCreate(TimeSpan timeout, TimerFactory timerFactory) + { + var promise = GetOrCreateInstance(); + promise.Reset(); + promise._retainCounter = 2; + promise._waitState = WaitAsyncState.Initial; + // IMPORTANT - must hookup callback after promise is fully setup. + using (SuppressExecutionContextFlow()) + { + promise._timer = timerFactory.CreateTimer(obj => obj.UnsafeAs>().OnTimerCallback(), promise, timeout, Timeout.InfiniteTimeSpan); + } + // In a rare race condition, the timer callback could be invoked before the field is assigned. + // To avoid an invalid timer disposal, we Interlocked.CompareExchange the wait state, and dispose if it was already invoked. + if (Interlocked.CompareExchange(ref promise._waitState, WaitAsyncState.Waiting, WaitAsyncState.Initial) != WaitAsyncState.Initial + && !promise.TryDisposeTimer(out var exception)) + { + ReportRejection(exception, promise); + } + return promise; + } + + internal override void MaybeDispose() + { + if (InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, -1) == 0) + { + Dispose(); + _timer = default; + ObjectPool.MaybeRepool(this); + } + } + + internal override void Handle(PromiseRefBase handler, Promise.State state) + { + ThrowIfInPool(this); + handler.SetCompletionState(state); + if (Interlocked.Exchange(ref _waitState, WaitAsyncState.Completed) != WaitAsyncState.Waiting) + { + // This already completed and this is being called from the timer's dispose promise, + // or this was timed out and this is being called from the awaited promise. + MaybeDispose(); + handler.MaybeReportUnhandledAndDispose(state); + return; + } + + // The _timer field is assigned before this is hooked up to the awaited promise, so we know it's valid here. + if (TryDisposeTimer(out var exception)) + { + HandleSelf(handler, state); + return; + } + + RejectContainer = CreateRejectContainer(exception, int.MinValue, null, this); + handler.MaybeReportUnhandledAndDispose(state); + HandleNextInternal(Promise.State.Rejected); + } + + private void OnTimerCallback() + { + ThrowIfInPool(this); + int previousState = Interlocked.Exchange(ref _waitState, WaitAsyncState.Completed); + if (previousState == WaitAsyncState.Completed) + { + return; + } + + // Handle will be called twice, once from the awaited promise, and again from the timer's dispose promise. + // We add another retain count here before attempting to dispose the timer and handling the next promise. + InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, 1); + + if (previousState == WaitAsyncState.Waiting + && !TryDisposeTimer(out var exception)) + { + RejectContainer = CreateRejectContainer(exception, int.MinValue, null, this); + HandleNextInternal(Promise.State.Rejected); + return; + } + + RejectContainer = CreateRejectContainer(new TimeoutException(), int.MinValue, null, this); + HandleNextInternal(Promise.State.Rejected); + } + + private bool TryDisposeTimer(out Exception exception) + { + // Dispose the timer and hook it up to this. Handle will be called again (possibly recursively), + // and the second time it will enter the other branch to complete disposal. + Promise timerDisposePromise; + try + { + timerDisposePromise = _timer.DisposeAsync(); + } + catch (Exception e) + { + exception = e; + return false; + } + + try + { + if (timerDisposePromise._ref == null || timerDisposePromise._ref.State != Promise.State.Pending) + { + timerDisposePromise._ref?.MaybeMarkAwaitedAndDispose(timerDisposePromise._id); + // Same as MaybeDispose, but without an extra branch, since we know this isn't done yet. + InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, -1); + exception = null; + return true; + } + + timerDisposePromise._ref.HookupExistingWaiter(timerDisposePromise._id, this); + exception = null; + return true; + } + catch (InvalidOperationException) + { + exception = new InvalidReturnException("Timer.DisposeAsync() returned an invalid Promise.", string.Empty); + return false; + } + } + } + +#if !PROTO_PROMISE_DEVELOPER_MODE + [DebuggerNonUserCode, StackTraceHidden] +#endif + internal sealed partial class WaitAsyncWithTimeoutAndCancelationPromise : PromiseSingleAwait, ICancelable + { + private WaitAsyncWithTimeoutAndCancelationPromise() { } + + [MethodImpl(InlineOption)] + private static WaitAsyncWithTimeoutAndCancelationPromise GetOrCreate() + { + var obj = ObjectPool.TryTakeOrInvalid>(); + return obj == InvalidAwaitSentinel.s_instance + ? new WaitAsyncWithTimeoutAndCancelationPromise() + : obj.UnsafeAs>(); + } + + [MethodImpl(InlineOption)] + internal static WaitAsyncWithTimeoutAndCancelationPromise GetOrCreateAndHookup( + PromiseRefBase previous, short id, TimeSpan timeout, TimerFactory timerFactory, CancelationToken cancelationToken) + { + var promise = GetOrCreate(); + promise.Reset(); + promise._retainCounter = 2; + promise._waitState = WaitAsyncState.Initial; + promise.SetPrevious(previous); + // IMPORTANT - must hookup callbacks after promise is fully setup. + using (SuppressExecutionContextFlow()) + { + promise._timer = timerFactory.CreateTimer(obj => obj.UnsafeAs>().OnTimerCallback(), promise, timeout, Timeout.InfiniteTimeSpan); + } + // IMPORTANT - must register cancelation callback after the timer. + promise._cancelationRegistration = cancelationToken.Register(promise); + // In a rare race condition, either callback could be invoked before the fields are assigned. + // To avoid invalid disposal, we Interlocked.CompareExchange the wait state, and dispose if it was already invoked. + if (Interlocked.CompareExchange(ref promise._waitState, WaitAsyncState.Waiting, WaitAsyncState.Initial) != WaitAsyncState.Initial) + { + promise._cancelationRegistration.Dispose(); + if (!promise.TryDisposeTimer(out var exception)) + { + ReportRejection(exception, promise); + } + } + // Finally, hook up to the awaited promise. + previous.HookupNewWaiter(id, promise); + return promise; + } + + internal override void MaybeDispose() + { + if (InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, -1) == 0) + { + Dispose(); + _timer = default; + _cancelationRegistration = default; + ObjectPool.MaybeRepool(this); + } + } + + internal override void Handle(PromiseRefBase handler, Promise.State state) + { + ThrowIfInPool(this); + handler.SetCompletionState(state); + if (Interlocked.Exchange(ref _waitState, WaitAsyncState.Completed) != WaitAsyncState.Waiting) + { + // This already completed and this is being called from the timer's dispose promise, + // or this was timed out or canceled and this is being called from the awaited promise. + MaybeDispose(); + handler.MaybeReportUnhandledAndDispose(state); + return; + } + + // The _timer field is assigned before this is hooked up to the awaited promise, so we know it's valid here. + if (TryDisposeTimer(out var exception)) + { + HandleSelf(handler, state); + return; + } + + RejectContainer = CreateRejectContainer(exception, int.MinValue, null, this); + handler.MaybeReportUnhandledAndDispose(state); + HandleNextInternal(Promise.State.Rejected); + } + + private void OnTimerCallback() + { + ThrowIfInPool(this); + int previousState = Interlocked.Exchange(ref _waitState, WaitAsyncState.Completed); + if (previousState == WaitAsyncState.Completed) + { + return; + } + + // Handle will be called twice, once from the awaited promise, and again from the timer's dispose promise. + // We add another retain count here before attempting to dispose the timer and handling the next promise. + InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, 1); + + if (previousState == WaitAsyncState.Waiting) + { + _cancelationRegistration.Dispose(); + if (!TryDisposeTimer(out var exception)) + { + RejectContainer = CreateRejectContainer(exception, int.MinValue, null, this); + HandleNextInternal(Promise.State.Rejected); + return; + } + } + + RejectContainer = CreateRejectContainer(new TimeoutException(), int.MinValue, null, this); + HandleNextInternal(Promise.State.Rejected); + } + + void ICancelable.Cancel() + { + ThrowIfInPool(this); + int previousState = Interlocked.Exchange(ref _waitState, WaitAsyncState.Completed); + if (previousState == WaitAsyncState.Completed) + { + return; + } + + // Handle will be called twice, once from the awaited promise, and again from the timer's dispose promise. + // We add another retain count here before attempting to dispose the timer and handling the next promise. + InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, 1); + + // We don't dispose the cancelation registration here, as it's pointless because it's being invoked now. + if (previousState == WaitAsyncState.Waiting + && !TryDisposeTimer(out var exception)) + { + RejectContainer = CreateRejectContainer(exception, int.MinValue, null, this); + HandleNextInternal(Promise.State.Rejected); + return; + } + + HandleNextInternal(Promise.State.Canceled); + } + + private bool TryDisposeTimer(out Exception exception) + { + // Dispose the timer and hook it up to this. Handle will be called again (possibly recursively), + // and the second time it will enter the other branch to complete disposal. + Promise timerDisposePromise; + try + { + timerDisposePromise = _timer.DisposeAsync(); + } + catch (Exception e) + { + exception = e; + return false; + } + + try + { + if (timerDisposePromise._ref == null || timerDisposePromise._ref.State != Promise.State.Pending) + { + timerDisposePromise._ref?.MaybeMarkAwaitedAndDispose(timerDisposePromise._id); + // Same as MaybeDispose, but without an extra branch, since we know this isn't done yet. + InterlockedAddWithUnsignedOverflowCheck(ref _retainCounter, -1); + exception = null; + return true; + } + + timerDisposePromise._ref.HookupExistingWaiter(timerDisposePromise._id, this); + exception = null; + return true; + } + catch (InvalidOperationException) + { + exception = new InvalidReturnException("Timer.DisposeAsync() returned an invalid Promise.", string.Empty); + return false; + } + } + } + } // class PromiseRefBase + } // class Internal +} // namespace Proto.Promises \ No newline at end of file diff --git a/Package/Core/Promises/Promise.cs b/Package/Core/Promises/Promise.cs index 8f2b7508..581ae0b7 100644 --- a/Package/Core/Promises/Promise.cs +++ b/Package/Core/Promises/Promise.cs @@ -6,6 +6,7 @@ #endif using Proto.Promises.CompilerServices; +using Proto.Timers; using System; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -120,6 +121,7 @@ public bool TryWaitNoThrow(TimeSpan timeout, out ResultContainer resultContainer /// /// Returns a new that inherits the state of this, or will be canceled if/when the is canceled before this is complete. /// + /// The to monitor for a cancelation request. [MethodImpl(Internal.InlineOption)] public Promise WaitAsync(CancelationToken cancelationToken) { @@ -127,6 +129,54 @@ public Promise WaitAsync(CancelationToken cancelationToken) return Internal.PromiseRefBase.CallbackHelperVoid.WaitAsync(this, cancelationToken); } + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout) + => WaitAsync(timeout, TimerFactory.System); + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed, or will be canceled if/when the is canceled, before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The to monitor for a cancelation request. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, CancelationToken cancelationToken) + => WaitAsync(timeout, TimerFactory.System, cancelationToken); + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The with which to interpet . + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, TimerFactory timerFactory) + { + ValidateOperation(1); + ValidateArgument(timerFactory, nameof(timerFactory), 1); + return Internal.PromiseRefBase.CallbackHelperVoid.WaitAsync(this, timeout, timerFactory); + } + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed, or will be canceled if/when the is canceled, before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The with which to interpet . + /// The to monitor for a cancelation request. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, TimerFactory timerFactory, CancelationToken cancelationToken) + { + ValidateOperation(1); + ValidateArgument(timerFactory, nameof(timerFactory), 1); + return Internal.PromiseRefBase.CallbackHelperVoid.WaitAsync(this, timeout, timerFactory, cancelationToken); + } + /// /// Configure the next continuation. /// Returns a new that will adopt the state of this and be completed according to the provided . diff --git a/Package/Core/Promises/PromiseStatic.cs b/Package/Core/Promises/PromiseStatic.cs index 6db47ffb..8bf1997b 100644 --- a/Package/Core/Promises/PromiseStatic.cs +++ b/Package/Core/Promises/PromiseStatic.cs @@ -480,7 +480,7 @@ public static Promise Delay(TimeSpan delay) /// A that represents the time delay. /// The argument is . /// - /// The returned may be resolved on a background thread. If you need to ensure + /// The returned may be completed on a background thread. If you need to ensure /// the continuation executes on a particular context, append . /// public static Promise Delay(TimeSpan delay, TimerFactory timerFactory) @@ -502,9 +502,9 @@ public static Promise Delay(TimeSpan delay, TimerFactory timerFactory) /// A that represents the time delay. /// /// If the is canceled before the specified time delay, - /// the returned will be canceled. Otherwise, the wil be resolved. + /// the returned will be canceled. Otherwise, the will be resolved. /// - /// The returned may be resolved on a background thread. If you need to ensure + /// The returned may be completed on a background thread. If you need to ensure /// the continuation executes on a particular context, append . /// public static Promise Delay(TimeSpan delay, CancelationToken cancelationToken) @@ -520,9 +520,9 @@ public static Promise Delay(TimeSpan delay, CancelationToken cancelationToken) /// The argument is . /// /// If the is canceled before the specified time delay, - /// the returned will be canceled. Otherwise, the wil be resolved. + /// the returned will be canceled. Otherwise, the will be resolved. /// - /// The returned may be resolved on a background thread. If you need to ensure + /// The returned may be completed on a background thread. If you need to ensure /// the continuation executes on a particular context, append . /// public static Promise Delay(TimeSpan delay, TimerFactory timerFactory, CancelationToken cancelationToken) diff --git a/Package/Core/Promises/PromiseT.cs b/Package/Core/Promises/PromiseT.cs index e1d8e1cb..eafc86e4 100644 --- a/Package/Core/Promises/PromiseT.cs +++ b/Package/Core/Promises/PromiseT.cs @@ -7,6 +7,7 @@ #pragma warning disable IDE0090 // Use 'new(...)' using Proto.Promises.CompilerServices; +using Proto.Timers; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -155,6 +156,7 @@ public bool TryWaitForResultNoThrow(TimeSpan timeout, out ResultContainer result /// /// Returns a new that inherits the state of this, or will be canceled if/when the is canceled before this is complete. /// + /// The to monitor for a cancelation request. [MethodImpl(Internal.InlineOption)] public Promise WaitAsync(CancelationToken cancelationToken) { @@ -162,6 +164,54 @@ public Promise WaitAsync(CancelationToken cancelationToken) return Internal.PromiseRefBase.CallbackHelperResult.WaitAsync(this, cancelationToken); } + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout) + => WaitAsync(timeout, TimerFactory.System); + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed, or will be canceled if/when the is canceled, before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The to monitor for a cancelation request. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, CancelationToken cancelationToken) + => WaitAsync(timeout, TimerFactory.System, cancelationToken); + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The with which to interpet . + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, TimerFactory timerFactory) + { + ValidateOperation(1); + ValidateArgument(timerFactory, nameof(timerFactory), 1); + return Internal.PromiseRefBase.CallbackHelperResult.WaitAsync(this, timeout, timerFactory); + } + + /// + /// Returns a new that inherits the state of this, or will be rejected with a + /// if/when the has elapsed, or will be canceled if/when the is canceled, before this is complete. + /// + /// The timeout after which the returned should be rejected with a if it hasn't otherwise completed. + /// The with which to interpet . + /// The to monitor for a cancelation request. + [MethodImpl(Internal.InlineOption)] + public Promise WaitAsync(TimeSpan timeout, TimerFactory timerFactory, CancelationToken cancelationToken) + { + ValidateOperation(1); + ValidateArgument(timerFactory, nameof(timerFactory), 1); + return Internal.PromiseRefBase.CallbackHelperResult.WaitAsync(this, timeout, timerFactory, cancelationToken); + } + /// /// Configure the next continuation. /// Returns a new that will adopt the state of this and be completed according to the provided . diff --git a/Package/Tests/CoreTests/APIs/DelayTests.cs b/Package/Tests/CoreTests/APIs/DelayTests.cs index 11133bfc..45f181ed 100644 --- a/Package/Tests/CoreTests/APIs/DelayTests.cs +++ b/Package/Tests/CoreTests/APIs/DelayTests.cs @@ -12,91 +12,71 @@ namespace ProtoPromiseTests.APIs { - public class DelayTests + public enum TimerFactoryType { - [SetUp] - public void Setup() - { - TestHelper.Setup(); - } - - [TearDown] - public void Teardown() - { - TestHelper.Cleanup(); - } - - public enum TimerFactoryType - { #if !UNITY_WEBGL - System = 0, + System = 0, #endif - FakeDelayed = 1, - FakeImmediate = 2 - } + FakeDelayed = 1, + FakeImmediate = 2 + } + + public abstract class FakeTimerFactory : TimerFactory + { + internal virtual void Invoke() { } + } + + public class FakeDelayedTimerFactory : FakeTimerFactory, ITimerSource + { + private TimerCallback _callback; + private object _state; - private abstract class FakeTimerFactory : TimerFactory + public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { - internal virtual void Invoke() { } + _callback = callback; + _state = state; + return new Proto.Timers.Timer(this, 0); } - private class FakeDelayedTimerFactory : FakeTimerFactory - { - private class FakeTimerSource : ITimerSource - { - internal static FakeTimerSource s_instance = new FakeTimerSource(); + void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } - private TimerCallback _callback; - private object _state; + Promise ITimerSource.DisposeAsync(int token) + { + _callback = null; + _state = null; + return Promise.Resolved(); + } - internal static FakeTimerSource Create(TimerCallback callback, object state) - { - s_instance._callback = callback; - s_instance._state = state; - return s_instance; - } + internal override void Invoke() + => _callback?.Invoke(_state); + } - void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } - - Promise ITimerSource.DisposeAsync(int token) - { - _callback = null; - _state = null; - return Promise.Resolved(); - } + public class FakeImmediateTimerFactory : FakeTimerFactory, ITimerSource + { + public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + callback.Invoke(state); + return new Proto.Timers.Timer(this, 0); + } - internal void Invoke() - => _callback?.Invoke(_state); - } + void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } - public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) - { - var fakeTimer = FakeTimerSource.Create(callback, state); - return new Proto.Timers.Timer(fakeTimer, 0); - } + Promise ITimerSource.DisposeAsync(int token) + => Promise.Resolved(); + } - internal override void Invoke() - => FakeTimerSource.s_instance.Invoke(); + public class DelayTests + { + [SetUp] + public void Setup() + { + TestHelper.Setup(); } - private class FakeImmediateTimerFactory : FakeTimerFactory + [TearDown] + public void Teardown() { - private class FakeTimerSource : ITimerSource - { - internal static FakeTimerSource s_instance = new FakeTimerSource(); - - void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } - - Promise ITimerSource.DisposeAsync(int token) - => Promise.Resolved(); - } - - public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) - { - callback.Invoke(state); - var fakeTimer = FakeTimerSource.s_instance; - return new Proto.Timers.Timer(fakeTimer, 0); - } + TestHelper.Cleanup(); } [Test] diff --git a/Package/Tests/CoreTests/APIs/WaitAsyncTests.cs b/Package/Tests/CoreTests/APIs/WaitAsyncTests.cs index 22637155..c90285c4 100644 --- a/Package/Tests/CoreTests/APIs/WaitAsyncTests.cs +++ b/Package/Tests/CoreTests/APIs/WaitAsyncTests.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using Proto.Promises; +using Proto.Timers; using ProtoPromiseTests.Concurrency; using System; using System.Collections.Generic; @@ -44,7 +45,7 @@ public void Teardown() TestHelper.s_expectedUncaughtRejectValue = null; } - private static IEnumerable GetArgs() + private static IEnumerable GetArgs_CancelationToken() { foreach (CompleteType completeType in Enum.GetValues(typeof(CompleteType))) foreach (WaitAsyncCancelType cancelType in Enum.GetValues(typeof(WaitAsyncCancelType))) @@ -56,7 +57,7 @@ private static IEnumerable GetArgs() } } - [Test, TestCaseSource(nameof(GetArgs))] + [Test, TestCaseSource(nameof(GetArgs_CancelationToken))] public void WaitAsync_CancelationToken_void(CompleteType completeType, WaitAsyncCancelType cancelType, bool alreadyComplete) { var cancelationSource = CancelationSource.New(); @@ -87,7 +88,7 @@ public void WaitAsync_CancelationToken_void(CompleteType completeType, WaitAsync cancelationSource.Dispose(); } - [Test, TestCaseSource(nameof(GetArgs))] + [Test, TestCaseSource(nameof(GetArgs_CancelationToken))] public void WaitAsync_CancelationToken_T(CompleteType completeType, WaitAsyncCancelType cancelType, bool alreadyComplete) { var cancelationSource = CancelationSource.New(); @@ -122,5 +123,472 @@ public void WaitAsync_CancelationToken_T(CompleteType completeType, WaitAsyncCan promise.WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); cancelationSource.Dispose(); } + + public enum CompleteTime + { + None, + Immediate, + Delayed + } + + private static IEnumerable GetArgs_Timeout() + { + foreach (CompleteType completeType in Enum.GetValues(typeof(CompleteType))) + foreach (int milliseconds in new[] { 0, 200, -1 }) + foreach (CompleteTime completeTime in Enum.GetValues(typeof(CompleteTime))) + { + if (completeTime == CompleteTime.None && milliseconds == -1) continue; + + yield return new TestCaseData(completeType, milliseconds, completeTime); + } + } + + [Test, TestCaseSource(nameof(GetArgs_Timeout))] + public void WaitAsync_Timeout_void(CompleteType completeType, int milliseconds, CompleteTime completeTime) + { + bool expectedTimeout = + completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0; + var expectedCompleteState = expectedTimeout ? Promise.State.Rejected : (Promise.State) completeType; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds)); + + if (completeTime == CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + } + + [Test, TestCaseSource(nameof(GetArgs_Timeout))] + public void WaitAsync_Timeout_T(CompleteType completeType, int milliseconds, CompleteTime completeTime) + { + bool expectedTimeout = + completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0; + var expectedCompleteState = expectedTimeout ? Promise.State.Rejected : (Promise.State) completeType; + const int resolveValue = 1; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, resolveValue, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds)); + + if (completeTime == CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedCompleteState == Promise.State.Resolved) + { + Assert.AreEqual(resolveValue, container.Value); + } + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + } + + private static IEnumerable GetArgs_TimeoutFactory() + { + foreach (CompleteType completeType in Enum.GetValues(typeof(CompleteType))) + foreach (int milliseconds in new[] { 0, 200, -1 }) + foreach (TimerFactoryType timerFactoryType in Enum.GetValues(typeof(TimerFactoryType))) + foreach (CompleteTime completeTime in Enum.GetValues(typeof(CompleteTime))) + { + if (milliseconds == -1 + && (completeTime == CompleteTime.None || timerFactoryType == TimerFactoryType.FakeImmediate)) continue; + + yield return new TestCaseData(completeType, milliseconds, timerFactoryType, completeTime); + } + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutFactory))] + public void WaitAsync_TimeoutFactory_void(CompleteType completeType, int milliseconds, TimerFactoryType timerFactoryType, CompleteTime completeTime) + { + FakeTimerFactory fakeFactory = timerFactoryType == TimerFactoryType.FakeDelayed + ? new FakeDelayedTimerFactory() + : (FakeTimerFactory) new FakeImmediateTimerFactory(); + + bool expectedTimeout = + completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0 || timerFactoryType == TimerFactoryType.FakeImmediate; + var expectedCompleteState = expectedTimeout ? Promise.State.Rejected : (Promise.State) completeType; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), timerFactoryType == 0 ? TimerFactory.System : fakeFactory); + + if (completeTime == CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + fakeFactory.Invoke(); + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutFactory))] + public void WaitAsync_TimeoutFactory_T(CompleteType completeType, int milliseconds, TimerFactoryType timerFactoryType, CompleteTime completeTime) + { + FakeTimerFactory fakeFactory = timerFactoryType == TimerFactoryType.FakeDelayed + ? new FakeDelayedTimerFactory() + : (FakeTimerFactory) new FakeImmediateTimerFactory(); + + bool expectedTimeout = + completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0 || timerFactoryType == TimerFactoryType.FakeImmediate; + var expectedCompleteState = expectedTimeout ? Promise.State.Rejected : (Promise.State) completeType; + const int resolveValue = 1; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, resolveValue, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), timerFactoryType == 0 ? TimerFactory.System : fakeFactory); + + if (completeTime == CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + fakeFactory.Invoke(); + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedCompleteState == Promise.State.Resolved) + { + Assert.AreEqual(resolveValue, container.Value); + } + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + } + + private static IEnumerable GetArgs_TimeoutCancelationToken() + { + foreach (CompleteType completeType in Enum.GetValues(typeof(CompleteType))) + foreach (int milliseconds in new[] { 0, 200, -1 }) + foreach (CompleteTime completeTime in Enum.GetValues(typeof(CompleteTime))) + foreach (WaitAsyncCancelType cancelType in Enum.GetValues(typeof(WaitAsyncCancelType))) + { + if (milliseconds == -1 && completeTime == CompleteTime.None) continue; + if (completeTime == CompleteTime.Immediate && cancelType == WaitAsyncCancelType.CancelBeforeComplete) continue; + + yield return new TestCaseData(completeType, milliseconds, completeTime, cancelType); + } + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutCancelationToken))] + public void WaitAsync_TimeoutCancelationToken_void(CompleteType completeType, int milliseconds, CompleteTime completeTime, WaitAsyncCancelType cancelType) + { + var cancelationSource = CancelationSource.New(); + var cancelationToken = cancelType == WaitAsyncCancelType.DefaultToken ? default(CancelationToken) + : cancelType == WaitAsyncCancelType.AlreadyCanceled ? CancelationToken.Canceled() + : cancelationSource.Token; + + bool expectedCanceled = + cancelType == WaitAsyncCancelType.AlreadyCanceled ? true + : cancelType == WaitAsyncCancelType.DefaultToken || cancelType == WaitAsyncCancelType.CancelableToken_NoCancel || cancelType == WaitAsyncCancelType.CancelAfterComplete ? false + // CancelBeforeComplete, expect canceled only if timeout is not immediate. + : milliseconds != 0; + bool expectedTimeout = + expectedCanceled ? false + : completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0; + var expectedCompleteState = + expectedCanceled ? Promise.State.Canceled + : expectedTimeout ? Promise.State.Rejected + : (Promise.State) completeType; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), cancelationToken); + + if (cancelType == WaitAsyncCancelType.CancelBeforeComplete) + { + cancelationSource.Cancel(); + } + if (completeTime == CompleteTime.Delayed) + { + tryCompleter(); + if (cancelType == WaitAsyncCancelType.CancelAfterComplete) + { + cancelationSource.Cancel(); + } + } + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + cancelationSource.Dispose(); + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutCancelationToken))] + public void WaitAsync_TimeoutCancelationToken_T(CompleteType completeType, int milliseconds, CompleteTime completeTime, WaitAsyncCancelType cancelType) + { + var cancelationSource = CancelationSource.New(); + var cancelationToken = cancelType == WaitAsyncCancelType.DefaultToken ? default(CancelationToken) + : cancelType == WaitAsyncCancelType.AlreadyCanceled ? CancelationToken.Canceled() + : cancelationSource.Token; + + bool expectedCanceled = + cancelType == WaitAsyncCancelType.AlreadyCanceled ? true + : cancelType == WaitAsyncCancelType.DefaultToken || cancelType == WaitAsyncCancelType.CancelableToken_NoCancel || cancelType == WaitAsyncCancelType.CancelAfterComplete ? false + // CancelBeforeComplete, expect canceled only if timeout is not immediate. + : milliseconds != 0; + bool expectedTimeout = + expectedCanceled ? false + : completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0; + var expectedCompleteState = + expectedCanceled ? Promise.State.Canceled + : expectedTimeout ? Promise.State.Rejected + : (Promise.State) completeType; + const int resolveValue = 1; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, resolveValue, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), cancelationToken); + + if (cancelType == WaitAsyncCancelType.CancelBeforeComplete) + { + cancelationSource.Cancel(); + } + if (completeTime == CompleteTime.Delayed) + { + tryCompleter(); + if (cancelType == WaitAsyncCancelType.CancelAfterComplete) + { + cancelationSource.Cancel(); + } + } + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedCompleteState == Promise.State.Resolved) + { + Assert.AreEqual(resolveValue, container.Value); + } + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + cancelationSource.Dispose(); + } + + private static IEnumerable GetArgs_TimeoutFactoryCancelationToken() + { + foreach (CompleteType completeType in Enum.GetValues(typeof(CompleteType))) + foreach (int milliseconds in new[] { 0, 200, -1 }) + foreach (TimerFactoryType timerFactoryType in Enum.GetValues(typeof(TimerFactoryType))) + foreach (CompleteTime completeTime in Enum.GetValues(typeof(CompleteTime))) + foreach (WaitAsyncCancelType cancelType in Enum.GetValues(typeof(WaitAsyncCancelType))) + { + if (milliseconds == -1 + && (completeTime == CompleteTime.None || timerFactoryType == TimerFactoryType.FakeImmediate)) continue; + if (completeTime == CompleteTime.Immediate && cancelType == WaitAsyncCancelType.CancelBeforeComplete) continue; + + yield return new TestCaseData(completeType, milliseconds, timerFactoryType, completeTime, cancelType); + } + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutFactoryCancelationToken))] + public void WaitAsync_TimeoutFactoryCancelationToken_void(CompleteType completeType, int milliseconds, TimerFactoryType timerFactoryType, CompleteTime completeTime, WaitAsyncCancelType cancelType) + { + var cancelationSource = CancelationSource.New(); + var cancelationToken = cancelType == WaitAsyncCancelType.DefaultToken ? default(CancelationToken) + : cancelType == WaitAsyncCancelType.AlreadyCanceled ? CancelationToken.Canceled() + : cancelationSource.Token; + + FakeTimerFactory fakeFactory = timerFactoryType == TimerFactoryType.FakeDelayed + ? new FakeDelayedTimerFactory() + : (FakeTimerFactory) new FakeImmediateTimerFactory(); + + bool expectedCanceled = + cancelType == WaitAsyncCancelType.AlreadyCanceled ? true + : cancelType == WaitAsyncCancelType.DefaultToken || cancelType == WaitAsyncCancelType.CancelableToken_NoCancel || cancelType == WaitAsyncCancelType.CancelAfterComplete ? false + // CancelBeforeComplete, expect canceled only if timeout is not immediate. + : milliseconds != 0 && timerFactoryType != TimerFactoryType.FakeImmediate; + bool expectedTimeout = + expectedCanceled ? false + : completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0 || timerFactoryType == TimerFactoryType.FakeImmediate; + var expectedCompleteState = + expectedCanceled ? Promise.State.Canceled + : expectedTimeout ? Promise.State.Rejected + : (Promise.State) completeType; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), timerFactoryType == 0 ? TimerFactory.System : fakeFactory, cancelationToken); + + if (cancelType == WaitAsyncCancelType.CancelBeforeComplete) + { + cancelationSource.Cancel(); + } + if (completeTime == CompleteTime.Delayed) + { + tryCompleter(); + if (cancelType == WaitAsyncCancelType.CancelAfterComplete) + { + cancelationSource.Cancel(); + } + } + fakeFactory.Invoke(); + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + cancelationSource.Dispose(); + } + + [Test, TestCaseSource(nameof(GetArgs_TimeoutFactoryCancelationToken))] + public void WaitAsync_TimeoutFactoryCancelationToken_T(CompleteType completeType, int milliseconds, TimerFactoryType timerFactoryType, CompleteTime completeTime, WaitAsyncCancelType cancelType) + { + var cancelationSource = CancelationSource.New(); + var cancelationToken = cancelType == WaitAsyncCancelType.DefaultToken ? default(CancelationToken) + : cancelType == WaitAsyncCancelType.AlreadyCanceled ? CancelationToken.Canceled() + : cancelationSource.Token; + + FakeTimerFactory fakeFactory = timerFactoryType == TimerFactoryType.FakeDelayed + ? new FakeDelayedTimerFactory() + : (FakeTimerFactory) new FakeImmediateTimerFactory(); + + bool expectedCanceled = + cancelType == WaitAsyncCancelType.AlreadyCanceled ? true + : cancelType == WaitAsyncCancelType.DefaultToken || cancelType == WaitAsyncCancelType.CancelableToken_NoCancel || cancelType == WaitAsyncCancelType.CancelAfterComplete ? false + // CancelBeforeComplete, expect canceled only if timeout is not immediate. + : milliseconds != 0 && timerFactoryType != TimerFactoryType.FakeImmediate; + bool expectedTimeout = + expectedCanceled ? false + : completeTime == CompleteTime.None ? true + : completeTime == CompleteTime.Immediate ? false + : milliseconds == 0 || timerFactoryType == TimerFactoryType.FakeImmediate; + var expectedCompleteState = + expectedCanceled ? Promise.State.Canceled + : expectedTimeout ? Promise.State.Rejected + : (Promise.State) completeType; + const int resolveValue = 1; + + var promise = TestHelper.BuildPromise(completeType, completeTime == CompleteTime.Immediate, resolveValue, rejectValue, out var tryCompleter) + .WaitAsync(TimeSpan.FromMilliseconds(milliseconds), timerFactoryType == 0 ? TimerFactory.System : fakeFactory, cancelationToken); + + if (cancelType == WaitAsyncCancelType.CancelBeforeComplete) + { + cancelationSource.Cancel(); + } + if (completeTime == CompleteTime.Delayed) + { + tryCompleter(); + if (cancelType == WaitAsyncCancelType.CancelAfterComplete) + { + cancelationSource.Cancel(); + } + } + fakeFactory.Invoke(); + + promise + .ContinueWith(container => + { + Assert.AreEqual(expectedCompleteState, container.State); + if (expectedCompleteState == Promise.State.Resolved) + { + Assert.AreEqual(resolveValue, container.Value); + } + if (expectedTimeout) + { + Assert.IsInstanceOf(container.Reason); + } + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + if (completeTime != CompleteTime.Delayed) + { + tryCompleter.Invoke(); + } + cancelationSource.Dispose(); + } } } \ No newline at end of file From 975865f5e0f200beaafd4702f35375186e6b88cf Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 17 Jan 2025 21:15:35 -0500 Subject: [PATCH 2/2] Added WaitAsync with timeout concurrency tests. --- .../Promises/Internal/WaitAsyncInternal.cs | 1 - .../Concurrency/DelayConcurrencyTests.cs | 80 +-- .../Concurrency/WaitAsyncConcurrencyTests.cs | 564 +++++++++++++++++- 3 files changed, 595 insertions(+), 50 deletions(-) diff --git a/Package/Core/Promises/Internal/WaitAsyncInternal.cs b/Package/Core/Promises/Internal/WaitAsyncInternal.cs index a7359323..7d476951 100644 --- a/Package/Core/Promises/Internal/WaitAsyncInternal.cs +++ b/Package/Core/Promises/Internal/WaitAsyncInternal.cs @@ -88,7 +88,6 @@ private static class WaitAsyncState internal const int Initial = 0; internal const int Waiting = 1; internal const int Completed = 2; - } #if !PROTO_PROMISE_DEVELOPER_MODE diff --git a/Package/Tests/CoreTests/Concurrency/DelayConcurrencyTests.cs b/Package/Tests/CoreTests/Concurrency/DelayConcurrencyTests.cs index 4bc21ed6..560700eb 100644 --- a/Package/Tests/CoreTests/Concurrency/DelayConcurrencyTests.cs +++ b/Package/Tests/CoreTests/Concurrency/DelayConcurrencyTests.cs @@ -16,6 +16,40 @@ namespace ProtoPromiseTests.Concurrency { + internal class FakeConcurrentTimerFactory : TimerFactory + { + private class FakeTimerSource : ITimerSource + { + private readonly Promise.Deferred deferred = Promise.NewDeferred(); + private TimerCallback _callback; + private object _state; + + public FakeTimerSource(TimerCallback callback, object state) + { + _callback = callback; + _state = state; + } + + void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } + + Promise ITimerSource.DisposeAsync(int token) + => deferred.Promise; + + internal void Invoke() + { + _callback.Invoke(_state); + deferred.Resolve(); + } + } + + public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + var fakeTimer = new FakeTimerSource(callback, state); + TestHelper._backgroundContext.Post(obj => obj.UnsafeAs().Invoke(), fakeTimer); + return new Proto.Timers.Timer(fakeTimer, 0); + } + } + public class DelayConcurrencyTests { [SetUp] @@ -30,42 +64,8 @@ public void Teardown() TestHelper.Cleanup(); } - private class FakeTimerFactory : TimerFactory - { - private class FakeTimerSource : ITimerSource - { - private readonly Promise.Deferred deferred = Promise.NewDeferred(); - private TimerCallback _callback; - private object _state; - - public FakeTimerSource(TimerCallback callback, object state) - { - _callback = callback; - _state = state; - } - - void ITimerSource.Change(TimeSpan dueTime, TimeSpan period, int token) { } - - Promise ITimerSource.DisposeAsync(int token) - => deferred.Promise; - - internal void Invoke() - { - _callback.Invoke(_state); - deferred.Resolve(); - } - } - - public override Proto.Timers.Timer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) - { - var fakeTimer = new FakeTimerSource(callback, state); - TestHelper._backgroundContext.Post(obj => obj.UnsafeAs().Invoke(), fakeTimer); - return new Proto.Timers.Timer(fakeTimer, 0); - } - } - [Test] - public void PromiseDelay() + public void PromiseDelay_Concurrent() { var bag = new ConcurrentBag(); // 1 thread for call to Promise.Delay, 1 thread for timer callback. @@ -87,10 +87,10 @@ public void PromiseDelay() } [Test] - public void PromiseDelay_WithFakeTimerFactory() + public void PromiseDelay_WithFakeTimerFactory_Concurrent() { var bag = new ConcurrentBag(); - var fakeTimerFactory = new FakeTimerFactory(); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); // 1 thread for call to Promise.Delay, 1 thread for timer callback. int concurrencyFactor = Environment.ProcessorCount / 2; var delayCalls = Enumerable.Repeat(() => bag.Add(Promise.Delay(TimeSpan.FromMilliseconds(1), fakeTimerFactory)), concurrencyFactor); @@ -110,7 +110,7 @@ public void PromiseDelay_WithFakeTimerFactory() } [Test] - public void PromiseDelay_WithCancelationToken() + public void PromiseDelay_WithCancelationToken_Concurrent() { var bag = new ConcurrentBag(); // 1 thread for call to Promise.Delay, 1 thread for timer callback, 1 thread for cancelation. @@ -157,10 +157,10 @@ public void PromiseDelay_WithCancelationToken() } [Test] - public void PromiseDelay_WithTimerFactoryAndCancelationToken() + public void PromiseDelay_WithTimerFactoryAndCancelationToken_Concurrent() { var bag = new ConcurrentBag(); - var fakeTimerFactory = new FakeTimerFactory(); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); // 1 thread for call to Promise.Delay, 1 thread for timer callback, 1 thread for cancelation. int concurrencyFactor = Environment.ProcessorCount / 3; var cancelationSources = new CancelationSource[concurrencyFactor]; diff --git a/Package/Tests/CoreTests/Concurrency/WaitAsyncConcurrencyTests.cs b/Package/Tests/CoreTests/Concurrency/WaitAsyncConcurrencyTests.cs index 77feb31d..8e2b09dc 100644 --- a/Package/Tests/CoreTests/Concurrency/WaitAsyncConcurrencyTests.cs +++ b/Package/Tests/CoreTests/Concurrency/WaitAsyncConcurrencyTests.cs @@ -28,7 +28,7 @@ public enum ContinuationType Await } - private static IEnumerable GetArgs() + private static IEnumerable GetArgs_CancelationToken() { var waitAsyncPlaces = new ActionPlace[] { @@ -65,10 +65,8 @@ private static IEnumerable GetContinuePlace(ActionPlace waitAsyncPl yield return ActionPlace.InTeardown; } - private readonly TimeSpan timeout = TimeSpan.FromSeconds(2); - - [Test, TestCaseSource(nameof(GetArgs))] - public void WaitAsync_Concurrent_void( + [Test, TestCaseSource(nameof(GetArgs_CancelationToken))] + public void WaitAsync_CancelationToken_Concurrent_void( ActionPlace waitAsyncSubscribePlace, ActionPlace continuePlace, bool withCancelation, @@ -170,7 +168,7 @@ async Promise Await() } TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); - TestHelper.SpinUntil(() => didContinue, timeout, $"didContinue: {didContinue}"); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); if (withCancelation) { cancelationSource.Dispose(); @@ -180,8 +178,8 @@ async Promise Await() ); } - [Test, TestCaseSource(nameof(GetArgs))] - public void WaitAsync_Concurrent_T( + [Test, TestCaseSource(nameof(GetArgs_CancelationToken))] + public void WaitAsync_CancelationToken_Concurrent_T( ActionPlace waitAsyncSubscribePlace, ActionPlace continuePlace, bool withCancelation, @@ -283,7 +281,555 @@ async Promise Await() } TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); - TestHelper.SpinUntil(() => didContinue, timeout, $"didContinue: {didContinue}"); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + if (withCancelation) + { + cancelationSource.Dispose(); + } + }, + actions: parallelActions.ToArray() + ); + } + + [Test] + public void WaitAsync_Timeout_Concurrent_void( + [Values(0, 1, -1)] int milliseconds, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new Action[] + { + () => deferred.Resolve(), + () => + { + var promise = deferred.Promise.WaitAsync(timeout); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + }, + actions: parallelActions + ); + } + + [Test] + public void WaitAsync_Timeout_Concurrent_T( + [Values(0, 1, -1)] int milliseconds, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new Action[] + { + () => deferred.Resolve(1), + () => + { + var promise = deferred.Promise.WaitAsync(timeout); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + }, + actions: parallelActions + ); + } + + [Test] + public void WaitAsync_TimeoutFactory_Concurrent_void( + [Values(0, 1, -1)] int milliseconds, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); + + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new Action[] + { + () => deferred.Resolve(), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, fakeTimerFactory); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + }, + actions: parallelActions + ); + } + + [Test] + public void WaitAsync_TimeoutFactory_Concurrent_T( + [Values(0, 1, -1)] int milliseconds, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); + + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new Action[] + { + () => deferred.Resolve(1), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, fakeTimerFactory); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + }, + actions: parallelActions + ); + } + + [Test] + public void WaitAsync_Timeout_CancelationToken_Concurrent_void( + [Values(0, 1, -1)] int milliseconds, + [Values] bool withCancelation, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + + var cancelationSource = default(CancelationSource); + var cancelationToken = default(CancelationToken); + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new List(3) + { + () => deferred.Resolve(), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, cancelationToken); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + catch (OperationCanceledException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + if (withCancelation) + { + parallelActions.Add(() => cancelationSource.Cancel()); + } + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + if (withCancelation) + { + cancelationSource = CancelationSource.New(); + cancelationToken = cancelationSource.Token; + } + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + if (withCancelation) + { + cancelationSource.Dispose(); + } + }, + actions: parallelActions.ToArray() + ); + } + + [Test] + public void WaitAsync_Timeout_CancelationToken_Concurrent_T( + [Values(0, 1, -1)] int milliseconds, + [Values] bool withCancelation, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + + var cancelationSource = default(CancelationSource); + var cancelationToken = default(CancelationToken); + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new List(3) + { + () => deferred.Resolve(1), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, cancelationToken); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + catch (OperationCanceledException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + if (withCancelation) + { + parallelActions.Add(() => cancelationSource.Cancel()); + } + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + if (withCancelation) + { + cancelationSource = CancelationSource.New(); + cancelationToken = cancelationSource.Token; + } + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + if (withCancelation) + { + cancelationSource.Dispose(); + } + }, + actions: parallelActions.ToArray() + ); + } + + [Test] + public void WaitAsync_TimeoutFactory_CancelationToken_Concurrent_void( + [Values(0, 1, -1)] int milliseconds, + [Values] bool withCancelation, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); + + var cancelationSource = default(CancelationSource); + var cancelationToken = default(CancelationToken); + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new List(3) + { + () => deferred.Resolve(), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, fakeTimerFactory, cancelationToken); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + catch (OperationCanceledException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + if (withCancelation) + { + parallelActions.Add(() => cancelationSource.Cancel()); + } + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + if (withCancelation) + { + cancelationSource = CancelationSource.New(); + cancelationToken = cancelationSource.Token; + } + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); + if (withCancelation) + { + cancelationSource.Dispose(); + } + }, + actions: parallelActions.ToArray() + ); + } + + [Test] + public void WaitAsync_TimeoutFactory_CancelationToken_Concurrent_T( + [Values(0, 1, -1)] int milliseconds, + [Values] bool withCancelation, + [Values] ContinuationType continuationType) + { + var foregroundThread = Thread.CurrentThread; + var timeout = TimeSpan.FromMilliseconds(milliseconds); + var fakeTimerFactory = new FakeConcurrentTimerFactory(); + + var cancelationSource = default(CancelationSource); + var cancelationToken = default(CancelationToken); + var deferred = default(Promise.Deferred); + bool didContinue = false; + + var parallelActions = new List(3) + { + () => deferred.Resolve(1), + () => + { + var promise = deferred.Promise.WaitAsync(timeout, fakeTimerFactory, cancelationToken); + if (continuationType == ContinuationType.Await) + { + Await().Forget(); + + async Promise Await() + { + try + { + await promise; + } + catch (TimeoutException) { } + catch (OperationCanceledException) { } + finally + { + didContinue = true; + } + } + } + else + { + promise + .ContinueWith(_ => didContinue = true) + .Forget(); + } + } + }; + + if (withCancelation) + { + parallelActions.Add(() => cancelationSource.Cancel()); + } + + var threadHelper = new ThreadHelper(); + threadHelper.ExecuteParallelActionsWithOffsets(false, + setup: () => + { + didContinue = false; + if (withCancelation) + { + cancelationSource = CancelationSource.New(); + cancelationToken = cancelationSource.Token; + } + deferred = Promise.NewDeferred(); + }, + teardown: () => + { + TestHelper.ExecuteForegroundCallbacksAndWaitForThreadsToComplete(); + TestHelper.SpinUntil(() => didContinue, TimeSpan.FromSeconds(1), $"didContinue: {didContinue}"); if (withCancelation) { cancelationSource.Dispose();