diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index 153ad047..9852b798 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, @@ -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.26f1] devMode: - { name: devMode, @@ -95,7 +95,7 @@ jobs: } - { testMode: { name: Editor }, - unityVersion: 2021.3.29f1 + 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. - { @@ -119,7 +119,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Rewrite ProjectSettings run: | @@ -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 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/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/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..4dfd1164 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(10); + [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) { diff --git a/Package/Tests/Helpers/BackgroundSynchronizationContext.cs b/Package/Tests/Helpers/BackgroundSynchronizationContext.cs index 43d9bc1e..5cd7f98e 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 @@ -11,13 +12,16 @@ 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(); + private static readonly HashSet s_runningThreads = new HashSet(); + volatile private int _runningActionCount; + public volatile bool NeverCompleted; 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; @@ -25,15 +29,20 @@ 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; - 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 +51,7 @@ public static void Run(BackgroundSynchronizationContext owner, SendOrPostCallbac } lock (threadRunner._locker) { + threadRunner._pool = pool; threadRunner._owner = owner; threadRunner._callback = callback; threadRunner._state = state; @@ -61,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; @@ -70,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) { @@ -94,6 +113,31 @@ public void WaitForAllThreadsToComplete() TimeSpan timeout = TimeSpan.FromSeconds(runningActions); if (!SpinWait.SpinUntil(() => _runningActionCount == 0, timeout)) { + s_pool = new Stack(); + _runningActionCount = 0; + NeverCompleted = true; + +#if !NETCOREAPP && (!UNITY_5_5_OR_NEWER || NET_LEGACY) + List exceptions = new List(); + lock (s_runningThreads) + { + 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 Proto.Promises.AggregateException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount, exceptions); + } +#endif throw new TimeoutException("WaitForAllThreadsToComplete timed out after " + timeout + ", _runningActionCount: " + _runningActionCount); } } diff --git a/Package/Tests/Helpers/TestHelper.cs b/Package/Tests/Helpers/TestHelper.cs index 1791dfc8..1b816716 100644 --- a/Package/Tests/Helpers/TestHelper.cs +++ b/Package/Tests/Helpers/TestHelper.cs @@ -124,11 +124,37 @@ 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 > 100) + { + Kill(); } 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 Kill() + { + Kill(); + } + + 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) @@ -172,7 +198,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() 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