Fix TimerAwaitable rooting (#20937)
* Fix TimerAwaitable rooting - This fixes an issue where components that use timer awaitable currently need to be explicitly stopped and disposed for it to be unrooted. This makes it possible to write a finalizer.
This commit is contained in:
parent
54408ae900
commit
812dfb97d3
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public class TimerAwaitableTests
|
||||
{
|
||||
[Fact]
|
||||
public void FinalizerRunsIfTimerAwaitableReferencesObject()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
UseTimerAwaitableAndUnref(tcs);
|
||||
|
||||
// Make sure it *really* cleans up
|
||||
for (int i = 0; i < 5 && !tcs.Task.IsCompleted; i++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
}
|
||||
|
||||
// Make sure the finalizer runs
|
||||
Assert.True(tcs.Task.IsCompleted);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void UseTimerAwaitableAndUnref(TaskCompletionSource<object> tcs)
|
||||
{
|
||||
_ = new ObjectWithTimerAwaitable(tcs).Start();
|
||||
}
|
||||
}
|
||||
|
||||
// This object holds onto a TimerAwaitable referencing the callback (the async continuation is the callback)
|
||||
// it also has a finalizer that triggers a tcs so callers can be notified when this object is being cleaned up.
|
||||
public class ObjectWithTimerAwaitable
|
||||
{
|
||||
private readonly TimerAwaitable _timer;
|
||||
private readonly TaskCompletionSource<object> _tcs;
|
||||
private int _count;
|
||||
|
||||
public ObjectWithTimerAwaitable(TaskCompletionSource<object> tcs)
|
||||
{
|
||||
_tcs = tcs;
|
||||
_timer = new TimerAwaitable(TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
using (_timer)
|
||||
{
|
||||
while (await _timer)
|
||||
{
|
||||
_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~ObjectWithTimerAwaitable()
|
||||
{
|
||||
_tcs.TrySetResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,20 @@ namespace Microsoft.AspNetCore.Internal
|
|||
restoreFlow = true;
|
||||
}
|
||||
|
||||
_timer = new Timer(state => ((TimerAwaitable)state).Tick(), this, _dueTime, _period);
|
||||
// This fixes the cycle by using a WeakReference to the state object. The object graph now looks like this:
|
||||
// Timer -> TimerHolder -> TimerQueueTimer -> WeakReference<TimerAwaitable> -> Timer -> ...
|
||||
// If TimerAwaitable falls out of scope, the timer should be released.
|
||||
_timer = new Timer(state =>
|
||||
{
|
||||
var weakRef = (WeakReference<TimerAwaitable>)state;
|
||||
if (weakRef.TryGetTarget(out var thisRef))
|
||||
{
|
||||
thisRef.Tick();
|
||||
}
|
||||
},
|
||||
new WeakReference<TimerAwaitable>(this),
|
||||
_dueTime,
|
||||
_period);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -125,4 +138,4 @@ namespace Microsoft.AspNetCore.Internal
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue