Skip to content

Commit 6583623

Browse files
committed
Add a convenience API for using timer in the right way
1 parent 120098b commit 6583623

File tree

4 files changed

+85
-0
lines changed

4 files changed

+85
-0
lines changed

NuGetPackageVerifier.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"Microsoft.Extensions.CommandLineUtils.Sources": {},
1010
"Microsoft.Extensions.CopyOnWriteDictionary.Sources": {},
1111
"Microsoft.Extensions.HashCodeCombiner.Sources": {},
12+
"Microsoft.Extensions.NonCapturingTimer.Sources": {},
1213
"Microsoft.Extensions.ObjectMethodExecutor.Sources": {},
1314
"Microsoft.Extensions.Process.Sources": {},
1415
"Microsoft.Extensions.PropertyActivator.Sources": {},
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
7+
namespace Microsoft.Extensions.Internal
8+
{
9+
// A convenience API for interacting with System.Threading.Timer in a way
10+
// that doesn't capture the ExecutionContext. We should be using this (or equivalent)
11+
// everywhere we use timers to avoid rooting any values stored in asynclocals.
12+
internal static class NonCapturingTimer
13+
{
14+
public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
15+
{
16+
if (callback == null)
17+
{
18+
throw new ArgumentNullException(nameof(callback));
19+
}
20+
21+
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer
22+
bool restoreFlow = false;
23+
try
24+
{
25+
if (!ExecutionContext.IsFlowSuppressed())
26+
{
27+
ExecutionContext.SuppressFlow();
28+
restoreFlow = true;
29+
}
30+
31+
return new Timer(callback, state, dueTime, period);
32+
}
33+
finally
34+
{
35+
// Restore the current ExecutionContext
36+
if (restoreFlow)
37+
{
38+
ExecutionContext.RestoreFlow();
39+
}
40+
}
41+
}
42+
}
43+
}

test/Microsoft.Extensions.Internal.Test/Microsoft.Extensions.Internal.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Compile Include="..\..\shared\Microsoft.Extensions.CopyOnWriteDictionary.Sources\**\*.cs" />
1414
<Compile Include="..\..\shared\Microsoft.Extensions.DotnetToolDispatcher.Sources\**\*.cs" />
1515
<Compile Include="..\..\shared\Microsoft.Extensions.HashCodeCombiner.Sources\**\*.cs" />
16+
<Compile Include="..\..\shared\Microsoft.Extensions.NonCapturingTimer.Sources\**\*.cs" />
1617
<Compile Include="..\..\shared\Microsoft.Extensions.ObjectMethodExecutor.Sources\**\*.cs" />
1718
<Compile Include="..\..\shared\Microsoft.Extensions.ParameterDefaultValue.Sources\**\*.cs" />
1819
<Compile Include="..\..\shared\Microsoft.Extensions.Process.Sources\**\*.cs" />
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
9+
namespace Microsoft.Extensions.Internal
10+
{
11+
public class NonCapturingTimerTest
12+
{
13+
[Fact]
14+
public async Task NonCapturingTimer_DoesntCaptureExecutionContext()
15+
{
16+
// Arrange
17+
var message = new AsyncLocal<string>();
18+
message.Value = "Hey, this is a value stored in the execuion context";
19+
20+
var tcs = new TaskCompletionSource<string>();
21+
22+
// Act
23+
var timer = NonCapturingTimer.Create((_) =>
24+
{
25+
// Observe the value based on the current execution context
26+
tcs.SetResult(message.Value);
27+
}, state: null, dueTime: TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan);
28+
29+
// Assert
30+
var messageFromTimer = await tcs.Task;
31+
timer.Dispose();
32+
33+
// ExecutionContext didn't flow to timer callback
34+
Assert.Null(messageFromTimer);
35+
36+
// ExecutionContext was restored
37+
Assert.NotNull(await Task.Run(() => message.Value));
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)