Skip to content

Commit

Permalink
Handle ResetRuntimeContext called concurrently.
Browse files Browse the repository at this point in the history
  • Loading branch information
timcassell committed Nov 15, 2023
1 parent 7e37d51 commit 454b5fa
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 62 deletions.
108 changes: 50 additions & 58 deletions Package/Core/InternalShared/DebugInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#undef PROMISE_DEBUG
#endif

#pragma warning disable IDE0019 // Use pattern matching
#pragma warning disable IDE0031 // Use null propagation
#pragma warning disable IDE0074 // Use compound assignment
#pragma warning disable IDE0090 // Use 'new(...)'
Expand Down Expand Up @@ -594,9 +595,8 @@ partial interface IFinalizable
}

// Linked-list of weak references.
// Using sentinel object for branchless algorithm.
// Using sentinel object for branchless algorithm and lock.
private static readonly WeakNode s_trackers = WeakNode.CreateSentinel();
private static SpinLocker s_trackersLock;

static partial void Discard(IFinalizable waste)
{
Expand All @@ -618,72 +618,73 @@ static partial void TrackFinalizable(IFinalizable finalizable)

var newNode = WeakNode.GetOrCreate(finalizable);
finalizable.Tracker = newNode;
s_trackersLock.Enter();
newNode.AddToList(s_trackers);
s_trackersLock.Exit();
lock (s_trackers)
{
newNode.AddToList(s_trackers);
}
}

internal static void SuppressAndUntrackFinalizable(IFinalizable finalizable)
{
GC.SuppressFinalize(finalizable);
var node = UntrackFinalizable(finalizable);
finalizable.Tracker = null;
node.Target = null;
WeakNode.Repool(node);
lock (s_trackers)
{
var node = finalizable.Tracker;
if (node == null)
{
return;
}
finalizable.Tracker = null;
node.RemoveFromList();
node.Target = null;
WeakNode.Repool(node);
}
}

internal static WeakNode UntrackFinalizable(IFinalizable finalizable)
{
// This is called from finalizers, so we don't touch the WeakReference, as it can cause a crash. (See comments in https://github.com/timcassell/ProtoPromise/pull/303)
var node = finalizable.Tracker;
s_trackersLock.Enter();
node.RemoveFromList();
s_trackersLock.Exit();
return node;
lock (s_trackers)
{
var node = finalizable.Tracker;
if (node != null)
{
finalizable.Tracker = null;
node.RemoveFromList();
}
return node;
}
}

internal static void SuppressAllFinalizables()
{
s_trackersLock.Enter();
var first = s_trackers._next;
var last = s_trackers._previous;
s_trackers.PointToSelf();

if (first == s_trackers)
var nodes = new List<WeakNode>();
lock (s_trackers)
{
s_trackersLock.Exit();
return;
}
var first = s_trackers._next;
var last = s_trackers._previous;

// Make the chain circular so we can pick out already-GC'd items.
first._previous = last;
last._next = first;
s_trackersLock.Exit();

var node = first;
do
{
var thisNode = node;
// We have to lock around getting the next because a node could be removed from the list on another thread.
s_trackersLock.Enter();
node = node._next;
s_trackersLock.Exit();

var target = thisNode.Target;
if (target == null)
// Copy all to a new list to be processed.
for (var node = first; node != s_trackers; node = node._next)
{
s_trackersLock.Enter();
thisNode.RemoveFromList();
s_trackersLock.Exit();
nodes.Add(node);
}
else

// Remove all from the current trackers list, and
// make the chain circular so each item can be processed the same as usual.
s_trackers.PointToSelf();
first._previous = last;
last._next = first;
}

foreach (var node in nodes)
{
var target = node.Target as IFinalizable;
if (target != null)
{
thisNode.Target = null;
GC.SuppressFinalize(target);
SuppressAndUntrackFinalizable(target);
}
} while (node != first);

WeakNode.Repool(first, last);
}

#if PROTO_PROMISE_DEVELOPER_MODE
lock (s_pooledObjects)
Expand Down Expand Up @@ -737,16 +738,6 @@ internal static void Repool(WeakNode node)
s_pooledNodesLock.Exit();
}

internal static void Repool(WeakNode first, WeakNode last)
{
s_pooledNodesLock.Enter();
last._next = s_pooledNodes;
first._previous = s_pooledNodes._previous;
s_pooledNodes._previous._next = first;
s_pooledNodes._previous = last;
s_pooledNodesLock.Exit();
}

internal void PointToSelf()
{
_next = this;
Expand All @@ -757,6 +748,7 @@ internal void RemoveFromList()
{
_previous._next = _next;
_next._previous = _previous;
PointToSelf();
}

internal void AddToList(WeakNode head)
Expand Down
43 changes: 39 additions & 4 deletions Package/Tests/CoreTests/APIs/MiscellaneousTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

using NUnit.Framework;
using Proto.Promises;
using Proto.Promises.Threading;
using ProtoPromiseTests.Concurrency;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -940,22 +941,56 @@ async Promise Await()
}
#endif // CSHARP_7_3_OR_NEWER

#pragma warning disable CS0219 // The variable is assigned but its value is never used.
[MethodImpl(MethodImplOptions.NoInlining)]
private void CreatePromiseWithoutAwait(Promise.Deferred deferred)
private void CreatePromiseWithoutAwaitAndResetRuntimeContext()
{
deferred.Promise.Then(() => { });
var deferred = Promise.NewDeferred();
var promise = deferred.Promise.Then(() => { });
deferred.Resolve();
Promise.Manager.ResetRuntimeContext();
}

[Test]
public void ResetRuntimeContext_SuppressesUnobservedPromiseException()
{
CreatePromiseWithoutAwaitAndResetRuntimeContext();

TestHelper.GcCollectAndWaitForFinalizers();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateAndDontHandleMultipleObjectsAndResetRuntimeContext()
{
var deferred = Promise.NewDeferred();
CreatePromiseWithoutAwait(deferred);
deferred.Resolve();
var promise = deferred.Promise;
promise.Then(() => { });
try
{
promise.Then(() => { });
}
catch (System.InvalidOperationException)
{
}

var cancelationSource = CancelationSource.New();

#if UNITY_2021_2_OR_NEWER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP
var asyncLock = new AsyncLock();
var key = asyncLock.Lock();
var keyPromise = asyncLock.LockAsync();
#endif

Promise.Manager.ResetRuntimeContext();
}

[Test]
public void ResetRuntimeContext_SuppressesMultipleUnhandledExceptions()
{
CreateAndDontHandleMultipleObjectsAndResetRuntimeContext();
TestHelper.GcCollectAndWaitForFinalizers();
}
#pragma warning restore CS0219 // The variable is assigned but its value is never used.

[Test]
public void PromiseMayBeResolvedWithNullable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#if !UNITY_WEBGL

using NUnit.Framework;
using Proto.Promises;
using Proto.Promises.Threading;
using System;
using System.Collections.Generic;
using System.Threading;

#pragma warning disable CS0219 // The variable is assigned but its value is never used.

namespace ProtoPromiseTests.Concurrency
{
public class MiscellaneousAsyncConcurrencyTests
{
[SetUp]
public void Setup()
{
TestHelper.Setup();
}

[TearDown]
public void Teardown()
{
TestHelper.Cleanup();
}

public enum ContinuationType
{
ContinueWith,
Await
}

[Test]
public void ResetRuntimeContext_SuppressesUnhandledExceptions_Concurrent()
{
var threadHelper = new ThreadHelper();
threadHelper.ExecuteParallelActionsWithOffsets(true,
//setup:
() => { },
//teardown:
() => { },
// actions:
#if UNITY_2021_2_OR_NEWER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP
() =>
{
var asyncLock = new AsyncLock();
var key = asyncLock.Lock();
var keyPromise = asyncLock.LockAsync();
Promise.Manager.ResetRuntimeContext();
},
#endif
() =>
{
var cancelationSource = CancelationSource.New();
Promise.Manager.ResetRuntimeContext();
},
() => CancelationSource.New().Dispose(),
() =>
{
var deferred = Promise.NewDeferred();
deferred.Resolve();
var promise = deferred.Promise;
promise.Then(() => { }).Forget();
try
{
promise.Then(() => { });
}
catch (System.InvalidOperationException)
{
}
},
() =>
{
var deferred = Promise.NewDeferred();
var promise = deferred.Promise.Then(() => { });
Promise.Manager.ResetRuntimeContext();
},
() => Promise.Manager.ResetRuntimeContext()
);
}
}
}

#endif // !UNITY_WEBGL

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 454b5fa

Please sign in to comment.