From 37b23bd024d7ee7f722dd3abb6c2b73885cdd38e Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 16 Sep 2023 20:14:23 -0400 Subject: [PATCH 01/20] =?UTF-8?q?=EF=BB=BFWrap=20SpinWait=20to=20try=20to?= =?UTF-8?q?=20find=20where=20it's=20getting=20stuck=20in=20CI.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Internal/CancelationInternal.cs | 4 +-- .../InternalShared/SpinWaitWithTimeout.cs | 36 +++++++++++++++++++ .../ValueCollectionsInternal.cs | 2 +- Package/Core/Promises/Config.cs | 2 ++ .../Progress/ProgressMergeInternal.cs | 2 +- .../Progress/ProgressSingleAwaitInternal.cs | 2 +- .../Core/Promises/Internal/PromiseInternal.cs | 6 ++-- .../Internal/AsyncAutoResetEventInternal.cs | 4 +-- .../Internal/AsyncCountdownEventInternal.cs | 4 +-- .../Threading/Internal/AsyncLockInternal.cs | 6 ++-- .../Internal/AsyncManualResetEventInternal.cs | 4 +-- .../Internal/AsyncReaderWriterLockInternal.cs | 24 ++++++------- .../Internal/AsyncSemaphoreInternal.cs | 4 +-- 13 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 Package/Core/InternalShared/SpinWaitWithTimeout.cs diff --git a/Package/Core/Cancelations/Internal/CancelationInternal.cs b/Package/Core/Cancelations/Internal/CancelationInternal.cs index 6f16be7d..494c1e05 100644 --- a/Package/Core/Cancelations/Internal/CancelationInternal.cs +++ b/Package/Core/Cancelations/Internal/CancelationInternal.cs @@ -869,7 +869,7 @@ internal void UnhookAndDispose() } // Spin until this has been disposed. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_parent != null) { spinner.SpinOnce(); @@ -1075,7 +1075,7 @@ internal static void TryUnregisterOrWaitForCallbackToComplete(CancelationRef par if (idsMatch & parentIsCanceling & parent._executingThread != Thread.CurrentThread) { - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); // _this._nodeId will be incremented when the callback is complete and this is disposed. // parent.TokenId will be incremented when all callbacks are complete and it is disposed. // We really only need to compare the nodeId, the tokenId comparison is just for a little extra safety in case of thread starvation and node re-use. diff --git a/Package/Core/InternalShared/SpinWaitWithTimeout.cs b/Package/Core/InternalShared/SpinWaitWithTimeout.cs new file mode 100644 index 00000000..9e10dca8 --- /dev/null +++ b/Package/Core/InternalShared/SpinWaitWithTimeout.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; + +namespace Proto.Promises +{ + internal static partial class Internal + { + internal struct SpinWaitWithTimeout + { + private SpinWait _spinWait; + private ValueStopwatch _stopwatch; + private TimeSpan _timeout; + + internal bool NextSpinWillYield + { + get { return _spinWait.NextSpinWillYield; } + } + + internal SpinWaitWithTimeout(TimeSpan timeout) + { + _timeout = timeout; + _spinWait = new SpinWait(); + _stopwatch = ValueStopwatch.StartNew(); + } + + internal void SpinOnce() + { + if (NextSpinWillYield && _stopwatch.GetElapsedTime() > _timeout) + { + throw new TimeoutException("SpinWait exceeded timeout " + _timeout); + } + _spinWait.SpinOnce(); + } + } + } +} \ No newline at end of file diff --git a/Package/Core/InternalShared/ValueCollectionsInternal.cs b/Package/Core/InternalShared/ValueCollectionsInternal.cs index 7e63e638..63c675c7 100644 --- a/Package/Core/InternalShared/ValueCollectionsInternal.cs +++ b/Package/Core/InternalShared/ValueCollectionsInternal.cs @@ -259,7 +259,7 @@ internal void Enter() private void EnterCore() { // Spin until we successfully get lock. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); do { spinner.SpinOnce(); diff --git a/Package/Core/Promises/Config.cs b/Package/Core/Promises/Config.cs index afa8ec85..0abdb2b3 100644 --- a/Package/Core/Promises/Config.cs +++ b/Package/Core/Promises/Config.cs @@ -64,6 +64,8 @@ public enum PoolType : byte #endif public static class Config { + internal static readonly TimeSpan SpinTimeout = TimeSpan.FromSeconds(1); + [Obsolete("Use ProgressPrecision to get the precision of progress reports."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly int ProgressDecimalBits = 32; diff --git a/Package/Core/Promises/Internal/Progress/ProgressMergeInternal.cs b/Package/Core/Promises/Internal/Progress/ProgressMergeInternal.cs index 26cc329f..2d010aec 100644 --- a/Package/Core/Promises/Internal/Progress/ProgressMergeInternal.cs +++ b/Package/Core/Promises/Internal/Progress/ProgressMergeInternal.cs @@ -282,7 +282,7 @@ internal void Handle(float oldProgress, float maxProgress, PromiseRefBase handle [MethodImpl(MethodImplOptions.NoInlining)] private void WaitForHookup() { - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_retainCounter == 0) { spinner.SpinOnce(); diff --git a/Package/Core/Promises/Internal/Progress/ProgressSingleAwaitInternal.cs b/Package/Core/Promises/Internal/Progress/ProgressSingleAwaitInternal.cs index dd0a299b..91588800 100644 --- a/Package/Core/Promises/Internal/Progress/ProgressSingleAwaitInternal.cs +++ b/Package/Core/Promises/Internal/Progress/ProgressSingleAwaitInternal.cs @@ -222,7 +222,7 @@ private void WaitForSecondPreviousAssignment() [MethodImpl(MethodImplOptions.NoInlining)] private void WaitForSecondPreviousAssignmentCore() { - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_waitState == WaitState.SettingSecond) { spinner.SpinOnce(); diff --git a/Package/Core/Promises/Internal/PromiseInternal.cs b/Package/Core/Promises/Internal/PromiseInternal.cs index 2c983a4c..081c6f07 100644 --- a/Package/Core/Promises/Internal/PromiseInternal.cs +++ b/Package/Core/Promises/Internal/PromiseInternal.cs @@ -178,7 +178,7 @@ private static bool TryWaitForCompletion(PromiseRefBase promise, short promiseId private bool TryWaitForCompletion(PromiseRefBase promise, TimeSpan timeout, ValueStopwatch stopwatch) { // We do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); if (timeout.Milliseconds == Timeout.Infinite) { while (promise.State == Promise.State.Pending & !spinner.NextSpinWillYield) @@ -259,7 +259,7 @@ internal override void Handle(PromiseRefBase handler, object rejectContainer, Pr } // Wait until we're sure the other thread has continued. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (waitState <= CompletedState) { spinner.SpinOnce(); @@ -512,7 +512,7 @@ internal void WaitUntilStateIsNotPending() private void WaitUntilStateIsNotPendingCore() { - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (State == Promise.State.Pending) { spinner.SpinOnce(); diff --git a/Package/Core/Threading/Internal/AsyncAutoResetEventInternal.cs b/Package/Core/Threading/Internal/AsyncAutoResetEventInternal.cs index 8c35ff17..28ed4fb8 100644 --- a/Package/Core/Threading/Internal/AsyncAutoResetEventInternal.cs +++ b/Package/Core/Threading/Internal/AsyncAutoResetEventInternal.cs @@ -158,7 +158,7 @@ internal Promise TryWaitAsync(CancelationToken cancelationToken) internal void Wait() { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (!_isSet & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -183,7 +183,7 @@ internal void Wait() internal bool TryWait(CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (!_isSet & !isCanceled & !spinner.NextSpinWillYield) { diff --git a/Package/Core/Threading/Internal/AsyncCountdownEventInternal.cs b/Package/Core/Threading/Internal/AsyncCountdownEventInternal.cs index 272f1e57..1fdc9166 100644 --- a/Package/Core/Threading/Internal/AsyncCountdownEventInternal.cs +++ b/Package/Core/Threading/Internal/AsyncCountdownEventInternal.cs @@ -165,7 +165,7 @@ internal Promise TryWaitAsync(CancelationToken cancelationToken) internal void Wait() { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isSet = _currentCount == 0; while (!isSet & !spinner.NextSpinWillYield) { @@ -197,7 +197,7 @@ internal void Wait() internal bool TryWait(CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isSet = _currentCount == 0; bool isCanceled = cancelationToken.IsCancelationRequested; while (!isSet & !isCanceled & !spinner.NextSpinWillYield) diff --git a/Package/Core/Threading/Internal/AsyncLockInternal.cs b/Package/Core/Threading/Internal/AsyncLockInternal.cs index a5419d3d..d5d6e0a5 100644 --- a/Package/Core/Threading/Internal/AsyncLockInternal.cs +++ b/Package/Core/Threading/Internal/AsyncLockInternal.cs @@ -286,7 +286,7 @@ private void SetNextKey() internal AsyncLock.Key Lock() { // Since this is a synchronous lock, we do a short spinwait before entering the full lock. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (Volatile.Read(ref _currentKey) != 0 & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -315,7 +315,7 @@ internal AsyncLock.Key Lock() internal AsyncLock.Key Lock(CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (Volatile.Read(ref _currentKey) != 0 & !isCanceled & !spinner.NextSpinWillYield) { @@ -402,7 +402,7 @@ internal bool TryEnter(out AsyncLock.Key key) internal bool TryEnter(out AsyncLock.Key key, CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (Volatile.Read(ref _currentKey) != 0 & !isCanceled & !spinner.NextSpinWillYield) { diff --git a/Package/Core/Threading/Internal/AsyncManualResetEventInternal.cs b/Package/Core/Threading/Internal/AsyncManualResetEventInternal.cs index c94b6ba0..846b23a8 100644 --- a/Package/Core/Threading/Internal/AsyncManualResetEventInternal.cs +++ b/Package/Core/Threading/Internal/AsyncManualResetEventInternal.cs @@ -162,7 +162,7 @@ internal Promise TryWaitAsync(CancelationToken cancelationToken) internal void Wait() { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isSet = _isSet; while (!isSet & !spinner.NextSpinWillYield) { @@ -194,7 +194,7 @@ internal void Wait() internal bool TryWait(CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isSet = _isSet; bool isCanceled = cancelationToken.IsCancelationRequested; while (!isSet & !isCanceled & !spinner.NextSpinWillYield) diff --git a/Package/Core/Threading/Internal/AsyncReaderWriterLockInternal.cs b/Package/Core/Threading/Internal/AsyncReaderWriterLockInternal.cs index 2613e7ed..ca763360 100644 --- a/Package/Core/Threading/Internal/AsyncReaderWriterLockInternal.cs +++ b/Package/Core/Threading/Internal/AsyncReaderWriterLockInternal.cs @@ -409,7 +409,7 @@ private bool CanEnterReaderLock(AsyncReaderWriterLockType currentLockType) internal AsyncReaderWriterLock.ReaderKey ReaderLock() { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (!CanEnterReaderLock(_lockType) & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -451,7 +451,7 @@ internal AsyncReaderWriterLock.ReaderKey ReaderLock() internal AsyncReaderWriterLock.ReaderKey ReaderLock(CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (!CanEnterReaderLock(_lockType) & !isCanceled & !spinner.NextSpinWillYield) { @@ -577,7 +577,7 @@ internal bool TryEnterReaderLock(out AsyncReaderWriterLock.ReaderKey readerKey) internal bool TryEnterReaderLock(out AsyncReaderWriterLock.ReaderKey readerKey, CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (!CanEnterReaderLock(_lockType) & !isCanceled & !spinner.NextSpinWillYield) { @@ -688,7 +688,7 @@ internal bool TryEnterReaderLock(out AsyncReaderWriterLock.ReaderKey readerKey, internal AsyncReaderWriterLock.WriterKey WriterLock() { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_lockType != AsyncReaderWriterLockType.None & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -720,7 +720,7 @@ internal AsyncReaderWriterLock.WriterKey WriterLock() internal AsyncReaderWriterLock.WriterKey WriterLock(CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (_lockType != AsyncReaderWriterLockType.None & !isCanceled & !spinner.NextSpinWillYield) { @@ -821,7 +821,7 @@ internal bool TryEnterWriterLock(out AsyncReaderWriterLock.WriterKey writerKey) internal bool TryEnterWriterLock(out AsyncReaderWriterLock.WriterKey writerKey, CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (_lockType != AsyncReaderWriterLockType.None & !isCanceled & !spinner.NextSpinWillYield) { @@ -945,7 +945,7 @@ private bool CanEnterUpgradeableReaderLock(AsyncReaderWriterLockType currentLock internal AsyncReaderWriterLock.UpgradeableReaderKey UpgradeableReaderLock() { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (!CanEnterUpgradeableReaderLock(_lockType) & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -981,7 +981,7 @@ internal AsyncReaderWriterLock.UpgradeableReaderKey UpgradeableReaderLock() internal AsyncReaderWriterLock.UpgradeableReaderKey UpgradeableReaderLock(CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (!CanEnterUpgradeableReaderLock(_lockType) & !isCanceled & !spinner.NextSpinWillYield) { @@ -1093,7 +1093,7 @@ internal bool TryEnterUpgradeableReaderLock(out AsyncReaderWriterLock.Upgradeabl internal bool TryEnterUpgradeableReaderLock(out AsyncReaderWriterLock.UpgradeableReaderKey readerKey, CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (!CanEnterUpgradeableReaderLock(_lockType) & !isCanceled & !spinner.NextSpinWillYield) { @@ -1208,7 +1208,7 @@ internal bool TryEnterUpgradeableReaderLock(out AsyncReaderWriterLock.Upgradeabl internal AsyncReaderWriterLock.WriterKey UpgradeToWriterLock(AsyncReaderWriterLock.UpgradeableReaderKey readerKey) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_readerLockCount != 1 & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -1245,7 +1245,7 @@ internal AsyncReaderWriterLock.WriterKey UpgradeToWriterLock(AsyncReaderWriterLo internal AsyncReaderWriterLock.WriterKey UpgradeToWriterLock(AsyncReaderWriterLock.UpgradeableReaderKey readerKey, CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (_readerLockCount != 1 & !isCanceled & !spinner.NextSpinWillYield) { @@ -1362,7 +1362,7 @@ internal bool TryUpgradeToWriterLock(AsyncReaderWriterLock.UpgradeableReaderKey internal bool TryUpgradeToWriterLock(AsyncReaderWriterLock.UpgradeableReaderKey readerKey, out AsyncReaderWriterLock.WriterKey writerKey, CancelationToken cancelationToken) { // Since this is a synchronous lock, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (_readerLockCount != 1 & !isCanceled & !spinner.NextSpinWillYield) { diff --git a/Package/Core/Threading/Internal/AsyncSemaphoreInternal.cs b/Package/Core/Threading/Internal/AsyncSemaphoreInternal.cs index a9a4e7fa..4665ba02 100644 --- a/Package/Core/Threading/Internal/AsyncSemaphoreInternal.cs +++ b/Package/Core/Threading/Internal/AsyncSemaphoreInternal.cs @@ -165,7 +165,7 @@ internal Promise TryWaitAsync(CancelationToken cancelationToken) internal void WaitSync() { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); while (_currentCount == 0 & !spinner.NextSpinWillYield) { spinner.SpinOnce(); @@ -192,7 +192,7 @@ internal void WaitSync() internal bool TryWait(CancelationToken cancelationToken) { // Because this is a synchronous wait, we do a short spinwait before yielding the thread. - var spinner = new SpinWait(); + var spinner = new SpinWaitWithTimeout(Promise.Config.SpinTimeout); bool isCanceled = cancelationToken.IsCancelationRequested; while (_currentCount == 0 & !isCanceled & !spinner.NextSpinWillYield) { From d9275f09e7ca2e3efa3b49f634e606818ff0d546 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 19 Sep 2023 23:24:41 -0400 Subject: [PATCH 02/20] Reset the thread pool when it times out. --- .../Helpers/BackgroundSynchronizationContext.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 43d9bc1e..50d98d0b 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -11,13 +11,14 @@ namespace ProtoPromiseTests // This also allows the test runner to wait for all background actions to complete. public sealed class BackgroundSynchronizationContext : SynchronizationContext { + // Pool threads globally because creating new threads is expensive. + private static Stack s_pool = new Stack(); + volatile private int _runningActionCount; private sealed class ThreadRunner { - // Pool threads globally because creating new threads is expensive. - private static readonly Stack _pool = new Stack(); - + private Stack _pool; private BackgroundSynchronizationContext _owner; private readonly object _locker = new object(); private SendOrPostCallback _callback; @@ -28,12 +29,13 @@ public static void Run(BackgroundSynchronizationContext owner, SendOrPostCallbac Interlocked.Increment(ref owner._runningActionCount); bool reused = false; ThreadRunner threadRunner = null; - lock (_pool) + var pool = s_pool; + lock (pool) { - if (_pool.Count > 0) + if (pool.Count > 0) { reused = true; - threadRunner = _pool.Pop(); + threadRunner = pool.Pop(); } } if (!reused) @@ -42,6 +44,7 @@ public static void Run(BackgroundSynchronizationContext owner, SendOrPostCallbac } lock (threadRunner._locker) { + threadRunner._pool = pool; threadRunner._owner = owner; threadRunner._callback = callback; threadRunner._state = state; @@ -94,6 +97,8 @@ public void WaitForAllThreadsToComplete() TimeSpan timeout = TimeSpan.FromSeconds(runningActions); if (!SpinWait.SpinUntil(() => _runningActionCount == 0, timeout)) { + s_pool = new Stack(); + _runningActionCount = 0; throw new TimeoutException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount); } } From 138a503bd9ca85f878963552bf0825e5d83897b0 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 21:01:23 -0400 Subject: [PATCH 03/20] Don't allow further threads to run. --- Package/Tests/Helpers/BackgroundSynchronizationContext.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 50d98d0b..60c08fc9 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -15,6 +15,7 @@ public sealed class BackgroundSynchronizationContext : SynchronizationContext private static Stack s_pool = new Stack(); volatile private int _runningActionCount; + volatile private bool _neverCompleted; private sealed class ThreadRunner { @@ -26,6 +27,10 @@ private sealed class ThreadRunner public static void Run(BackgroundSynchronizationContext owner, SendOrPostCallback callback, object state) { + if (owner._neverCompleted) + { + throw new Exception("A previous thread never completed, not running action."); + } Interlocked.Increment(ref owner._runningActionCount); bool reused = false; ThreadRunner threadRunner = null; @@ -99,6 +104,7 @@ public void WaitForAllThreadsToComplete() { s_pool = new Stack(); _runningActionCount = 0; + _neverCompleted = true; throw new TimeoutException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount); } } From f1485c2a2cfc9997446daf9d443b750f2b7b0ca8 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 21:12:12 -0400 Subject: [PATCH 04/20] Try to capture stacktrace of deadlocked threads. --- .../BackgroundSynchronizationContext.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 60c08fc9..fb880ce5 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -1,6 +1,7 @@ using Proto.Promises; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; #pragma warning disable 0420 // A reference to a volatile field will not be treated as volatile @@ -13,6 +14,7 @@ public sealed class BackgroundSynchronizationContext : SynchronizationContext { // Pool threads globally because creating new threads is expensive. private static Stack s_pool = new Stack(); + private static readonly HashSet s_runningThreads = new HashSet(); volatile private int _runningActionCount; volatile private bool _neverCompleted; @@ -69,6 +71,10 @@ private void ThreadAction() { while (true) { + lock (s_runningThreads) + { + s_runningThreads.Add(Thread.CurrentThread); + } BackgroundSynchronizationContext owner = _owner; SendOrPostCallback callback = _callback; object state = _state; @@ -78,6 +84,11 @@ private void ThreadAction() _state = null; SetSynchronizationContext(owner); callback.Invoke(state); + + lock (s_runningThreads) + { + s_runningThreads.Remove(Thread.CurrentThread); + } Interlocked.Decrement(ref owner._runningActionCount); lock (_locker) { @@ -105,6 +116,26 @@ public void WaitForAllThreadsToComplete() s_pool = new Stack(); _runningActionCount = 0; _neverCompleted = true; + +#if !NETCOREAPP + List exceptions = new List(); + lock (s_runningThreads) + { + foreach (var thread in s_runningThreads) + { +#pragma warning disable CS0618 // Type or member is obsolete + thread.Suspend(); + var stackTrace = new StackTrace(thread, true); + exceptions.Add(new Proto.Promises.UnreleasedObjectException("Deadlocked thread", stackTrace.ToString())); +#pragma warning restore CS0618 // Type or member is obsolete + } + s_runningThreads.Clear(); + } + if (exceptions.Count > 0) + { + throw new System.AggregateException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount, exceptions); + } +#endif throw new TimeoutException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount); } } From b9231f7080d7e660ddae34bb2ccda22baf187d1b Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 21:19:38 -0400 Subject: [PATCH 05/20] Fix compilation errors --- .../Core/InternalShared/SpinWaitWithTimeout.cs.meta | 11 +++++++++++ .../Tests/Helpers/BackgroundSynchronizationContext.cs | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Package/Core/InternalShared/SpinWaitWithTimeout.cs.meta diff --git a/Package/Core/InternalShared/SpinWaitWithTimeout.cs.meta b/Package/Core/InternalShared/SpinWaitWithTimeout.cs.meta new file mode 100644 index 00000000..bbf3c347 --- /dev/null +++ b/Package/Core/InternalShared/SpinWaitWithTimeout.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd9bbc8cadd24e847940fecaadd90967 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index fb880ce5..398c6aa5 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -124,16 +124,18 @@ public void WaitForAllThreadsToComplete() foreach (var thread in s_runningThreads) { #pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0612 // Type or member is obsolete thread.Suspend(); var stackTrace = new StackTrace(thread, true); exceptions.Add(new Proto.Promises.UnreleasedObjectException("Deadlocked thread", stackTrace.ToString())); +#pragma warning restore CS0612 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete } s_runningThreads.Clear(); } if (exceptions.Count > 0) { - throw new System.AggregateException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount, exceptions); + throw new Proto.Promises.AggregateException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount, exceptions); } #endif throw new TimeoutException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount); From 3604e0c0ec8fa94ae057221b3a12233d0b59df3c Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 21:25:03 -0400 Subject: [PATCH 06/20] Added never completed check in TestHelper.Setup. --- Package/Tests/Helpers/BackgroundSynchronizationContext.cs | 6 +++--- Package/Tests/Helpers/TestHelper.cs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 398c6aa5..5aa89c49 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -17,7 +17,7 @@ public sealed class BackgroundSynchronizationContext : SynchronizationContext private static readonly HashSet s_runningThreads = new HashSet(); volatile private int _runningActionCount; - volatile private bool _neverCompleted; + public volatile bool NeverCompleted; private sealed class ThreadRunner { @@ -29,7 +29,7 @@ private sealed class ThreadRunner public static void Run(BackgroundSynchronizationContext owner, SendOrPostCallback callback, object state) { - if (owner._neverCompleted) + if (owner.NeverCompleted) { throw new Exception("A previous thread never completed, not running action."); } @@ -115,7 +115,7 @@ public void WaitForAllThreadsToComplete() { s_pool = new Stack(); _runningActionCount = 0; - _neverCompleted = true; + NeverCompleted = true; #if !NETCOREAPP List exceptions = new List(); diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index 1791dfc8..100b8f22 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -125,6 +125,10 @@ public static void Setup() _stopwatch = Stopwatch.StartNew(); } + else if (_backgroundContext.NeverCompleted) + { + throw new Exception("A background thread never completed, not running any further tests."); + } SynchronizationContext.SetSynchronizationContext(_foregroundContext); Promise.Manager.ThreadStaticSynchronizationContext = _foregroundContext; From c865122f4cc8489dc910b63a808afe9cd1237864 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 22:47:47 -0400 Subject: [PATCH 07/20] Fix compile error --- Package/Tests/Helpers/BackgroundSynchronizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 5aa89c49..5cd7f98e 100644 --- a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs +++ b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs @@ -117,7 +117,7 @@ public void WaitForAllThreadsToComplete() _runningActionCount = 0; NeverCompleted = true; -#if !NETCOREAPP +#if !NETCOREAPP && (!UNITY_5_5_OR_NEWER || NET_LEGACY) List exceptions = new List(); lock (s_runningThreads) { From 3fb065bccd4268863a81b064428d710406c0b6b3 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 22 Sep 2023 19:32:37 -0400 Subject: [PATCH 08/20] Use Assert.Inconclusive instead of throw. --- Package/Tests/Helpers/TestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index 100b8f22..ed47d4a4 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -127,7 +127,7 @@ public static void Setup() } else if (_backgroundContext.NeverCompleted) { - throw new Exception("A background thread never completed, not running any further tests."); + Assert.Inconclusive("A background thread never completed, not running any further tests."); } SynchronizationContext.SetSynchronizationContext(_foregroundContext); From cd00a479584e816df1b0bdc5116d8d61c10bb590 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 23 Sep 2023 00:26:20 -0400 Subject: [PATCH 09/20] Only test standalone. --- .github/workflows/unity-tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 153ad047..fb1507cc 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -15,11 +15,11 @@ jobs: fail-fast: false matrix: testMode: - - { - name: Editor, - value: PlayMode, - buildTargetId: 1 - } + # - { + # name: Editor, + # value: PlayMode, + # buildTargetId: 1 + # } - { name: Standalone, value: Standalone, From e1f04eb656c1c0d340ac04de5ff2200fe814c70d Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 23 Sep 2023 18:48:15 -0400 Subject: [PATCH 10/20] Try writing progress directly to console. --- Package/Tests/Helpers/TestHelper.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index ed47d4a4..56f7f36f 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -132,7 +132,16 @@ public static void Setup() SynchronizationContext.SetSynchronizationContext(_foregroundContext); Promise.Manager.ThreadStaticSynchronizationContext = _foregroundContext; - TestContext.Progress.WriteLine("Begin time: " + _stopwatch.Elapsed.ToString() + ", test: " + TestContext.CurrentContext.Test.FullName); + WriteProgress("Begin time: " + _stopwatch.Elapsed.ToString() + ", test: " + TestContext.CurrentContext.Test.FullName); + } + + private static void WriteProgress(string progress) + { +#if UNITY_5_5_OR_NEWER + Console.WriteLine(progress); +#else + TestContext.Progress.WriteLine(progress); +#endif } public static void AssertRejection(object expected, object actual) @@ -176,7 +185,7 @@ public static void Cleanup() #endif s_expectedUncaughtRejectValue = null; - TestContext.Progress.WriteLine("Success time: " + _stopwatch.Elapsed.ToString() + ", test: " + TestContext.CurrentContext.Test.FullName); + WriteProgress("Success time: " + _stopwatch.Elapsed.ToString() + ", test: " + TestContext.CurrentContext.Test.FullName); } private static void WaitForAllThreadsToCompleteAndGcCollect() From 6959f6bb1d52d9357e17a398faf74e7e8d6c58d2 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Sep 2023 06:10:27 -0400 Subject: [PATCH 11/20] Quit. --- Package/Tests/Helpers/TestHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index 56f7f36f..8346c230 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -128,6 +128,7 @@ public static void Setup() else if (_backgroundContext.NeverCompleted) { Assert.Inconclusive("A background thread never completed, not running any further tests."); + System.Environment.Exit(1); } SynchronizationContext.SetSynchronizationContext(_foregroundContext); From 4ab8330b925c925b5db110df92ffbb1c76fd8c88 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Sep 2023 19:06:51 -0400 Subject: [PATCH 12/20] Force kill. --- Package/Tests/Helpers/TestHelper.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index 8346c230..a9758ec7 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -127,8 +127,7 @@ public static void Setup() } else if (_backgroundContext.NeverCompleted) { - Assert.Inconclusive("A background thread never completed, not running any further tests."); - System.Environment.Exit(1); + Kill(); } SynchronizationContext.SetSynchronizationContext(_foregroundContext); @@ -136,6 +135,11 @@ public static void Setup() WriteProgress("Begin time: " + _stopwatch.Elapsed.ToString() + ", test: " + TestContext.CurrentContext.Test.FullName); } + private static void Kill() + { + Kill(); + } + private static void WriteProgress(string progress) { #if UNITY_5_5_OR_NEWER From beef356512bd67900206244f1eeb4c8eb385aef3 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Sep 2023 16:32:49 -0400 Subject: [PATCH 13/20] Switch Unity version to 2021.3.30 --- .github/workflows/unity-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index fb1507cc..5cd8898f 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -71,7 +71,7 @@ jobs: } # Editor uses 2018.4 to test Net3.5 and Net4.x. # Standalone uses 2019.4 and 2021.3 to test IL2CPP with netstandard 2.0 and netstandard2.1. - unityVersion: [2018.4.36f1, 2019.4.40f1, 2021.3.29f1] + unityVersion: [2018.4.36f1, 2019.4.40f1, 2021.3.30f1] devMode: - { name: devMode, @@ -95,7 +95,7 @@ jobs: } - { testMode: { name: Editor }, - unityVersion: 2021.3.29f1 + unityVersion: 2021.3.30f1 } # Standalone with IL2CPP can only be built with 2019.4+ (unity-builder docker images constraint), which doesn't support Net3.5. - { From 1e1d5329287ac1b37c667eda977d208017808646 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Sep 2023 19:11:04 -0400 Subject: [PATCH 14/20] =?UTF-8?q?=EF=BB=BFUpdate=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/unity-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 5cd8898f..d035d1f1 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -119,7 +119,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Rewrite ProjectSettings run: | From ad0bba2b3e467e5aa37aedf84d982e3cf3e6bc5d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Sep 2023 20:04:16 -0400 Subject: [PATCH 15/20] Switch Unity to 2021.3.26 --- .github/workflows/unity-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index d035d1f1..f8d8598f 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -71,7 +71,7 @@ jobs: } # Editor uses 2018.4 to test Net3.5 and Net4.x. # Standalone uses 2019.4 and 2021.3 to test IL2CPP with netstandard 2.0 and netstandard2.1. - unityVersion: [2018.4.36f1, 2019.4.40f1, 2021.3.30f1] + unityVersion: [2018.4.36f1, 2019.4.40f1, 2021.3.26f1] devMode: - { name: devMode, @@ -95,7 +95,7 @@ jobs: } - { testMode: { name: Editor }, - unityVersion: 2021.3.30f1 + unityVersion: 2021.3.26f1 } # Standalone with IL2CPP can only be built with 2019.4+ (unity-builder docker images constraint), which doesn't support Net3.5. - { From 6cfce33047954a4a30d4d1ffb93a248a262a0608 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Sep 2023 00:29:22 -0400 Subject: [PATCH 16/20] Comment out Unity tests. --- Package/Tests/UnityTests/PromiseYielderTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Package/Tests/UnityTests/PromiseYielderTests.cs b/Package/Tests/UnityTests/PromiseYielderTests.cs index 79fdacab..b4b67930 100644 --- a/Package/Tests/UnityTests/PromiseYielderTests.cs +++ b/Package/Tests/UnityTests/PromiseYielderTests.cs @@ -1,4 +1,5 @@ -#if !PROTO_PROMISE_PROGRESS_DISABLE +/* +#if !PROTO_PROMISE_PROGRESS_DISABLE #define PROMISE_PROGRESS #else #undef PROMISE_PROGRESS @@ -1488,4 +1489,5 @@ async Promise Func() #endif // CSHARP_7_3_OR_NEWER } -} \ No newline at end of file +} +*/ \ No newline at end of file From 6e2db6c4c5645451e804856ae54dad6cf1ed6602 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 10 Oct 2023 09:25:03 -0400 Subject: [PATCH 17/20] Add check for tests running too long. --- Package/Tests/Helpers/TestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index a9758ec7..cb2f360d 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -125,7 +125,7 @@ public static void Setup() _stopwatch = Stopwatch.StartNew(); } - else if (_backgroundContext.NeverCompleted) + else if (_backgroundContext.NeverCompleted || _stopwatch.Elapsed.TotalMinutes > 110) { Kill(); } From 39b35aaa823e054a3150ef10bee93675b764d12c Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 11 Oct 2023 14:22:33 -0400 Subject: [PATCH 18/20] Increase SpinWait timeout --- Package/Core/Promises/Config.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package/Core/Promises/Config.cs b/Package/Core/Promises/Config.cs index 0abdb2b3..4dfd1164 100644 --- a/Package/Core/Promises/Config.cs +++ b/Package/Core/Promises/Config.cs @@ -64,7 +64,7 @@ public enum PoolType : byte #endif public static class Config { - internal static readonly TimeSpan SpinTimeout = TimeSpan.FromSeconds(1); + internal static readonly TimeSpan SpinTimeout = TimeSpan.FromSeconds(10); [Obsolete("Use ProgressPrecision to get the precision of progress reports."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly int ProgressDecimalBits = 32; From 5e9b09b2d5214c5c3d8989931c707835eea53c72 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 15 Oct 2023 17:39:12 -0400 Subject: [PATCH 19/20] Background thread timeout. --- Package/Tests/Helpers/TestHelper.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index cb2f360d..1b816716 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -124,8 +124,16 @@ public static void Setup() Promise.Config.DebugCausalityTracer = Promise.TraceLevel.None; // Disabled because it makes the tests slow. _stopwatch = Stopwatch.StartNew(); + + // Spin up a thread to wait in the background for a timeout, then kill the app if it hasn't already ended. + new Thread(_ => + { + Thread.Sleep(new TimeSpan(1, 30, 0)); + Environment.Exit(1); + Kill(); + }){ IsBackground = true }.Start(); } - else if (_backgroundContext.NeverCompleted || _stopwatch.Elapsed.TotalMinutes > 110) + else if (_backgroundContext.NeverCompleted || _stopwatch.Elapsed.TotalMinutes > 100) { Kill(); } From 1433a0bc861c1551401fe49619b5f49be363a591 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 16 Oct 2023 01:14:43 -0400 Subject: [PATCH 20/20] Increased timeout. --- .github/workflows/unity-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index f8d8598f..9852b798 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -145,7 +145,7 @@ jobs: projectPath: ProtoPromise_Unity testMode: ${{ matrix.testMode.value }} unityVersion: ${{ matrix.unityVersion }} - timeout-minutes: 120 + timeout-minutes: 180 # Workaround for NUnit XML (see https://github.com/dorny/test-reporter/issues/98#issuecomment-867106931) - name: Install NUnit