Skip to content

Commit f9a00bc

Browse files
authored
Fix capturing ExecutionContext by timers and background tasks (#2129)
1 parent 2dc971e commit f9a00bc

27 files changed

+392
-141
lines changed

global.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"sdk": {
3-
"version": "7.0.201"
3+
"version": "7.0.201",
4+
"rollForward": "latestFeature"
45
}
56
}

src/Grpc.AspNetCore.Server/Grpc.AspNetCore.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<Compile Include="..\Shared\Server\UnaryServerMethodInvoker.cs" Link="Model\Internal\UnaryServerMethodInvoker.cs" />
3131
<Compile Include="..\Shared\NullableAttributes.cs" Link="Internal\NullableAttributes.cs" />
3232
<Compile Include="..\Shared\CodeAnalysisAttributes.cs" Link="Internal\CodeAnalysisAttributes.cs" />
33+
<Compile Include="..\Shared\NonCapturingTimer.cs" Link="Internal\NonCapturingTimer.cs" />
3334
</ItemGroup>
3435

3536
<ItemGroup>

src/Grpc.AspNetCore.Server/Internal/ServerCallDeadlineManager.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -91,12 +91,12 @@ public ServerCallDeadlineManager(HttpContextServerCallContext serverCallContext,
9191
// Ensures there is no weird situation where the timer triggers
9292
// before the field is set. Shouldn't happen because only long deadlines
9393
// will take this path but better to be safe than sorry.
94-
_longDeadlineTimer = new Timer(DeadlineExceededLongDelegate, (this, maxTimerDueTime), Timeout.Infinite, Timeout.Infinite);
94+
_longDeadlineTimer = NonCapturingTimer.Create(DeadlineExceededLongDelegate, (this, maxTimerDueTime), Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
9595
_longDeadlineTimer.Change(timerMilliseconds, Timeout.Infinite);
9696
}
9797
else
9898
{
99-
_longDeadlineTimer = new Timer(DeadlineExceededDelegate, this, timerMilliseconds, Timeout.Infinite);
99+
_longDeadlineTimer = NonCapturingTimer.Create(DeadlineExceededDelegate, this, TimeSpan.FromMilliseconds(timerMilliseconds), Timeout.InfiniteTimeSpan);
100100
}
101101
}
102102

src/Grpc.Net.Client/Balancer/DnsResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ protected override void OnStarted()
6868

6969
if (_refreshInterval != Timeout.InfiniteTimeSpan)
7070
{
71-
_timer = new Timer(OnTimerCallback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
71+
_timer = NonCapturingTimer.Create(OnTimerCallback, state: null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
7272
_timer.Change(_refreshInterval, _refreshInterval);
7373
}
7474
}

src/Grpc.Net.Client/Balancer/Internal/SocketConnectivitySubchannelTransport.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -77,7 +77,7 @@ public SocketConnectivitySubchannelTransport(
7777
ConnectTimeout = connectTimeout;
7878
_socketConnect = socketConnect ?? OnConnect;
7979
_activeStreams = new List<ActiveStream>();
80-
_socketConnectedTimer = new Timer(OnCheckSocketConnection, state: null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
80+
_socketConnectedTimer = NonCapturingTimer.Create(OnCheckSocketConnection, state: null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
8181
}
8282

8383
private object Lock => _subchannel.Lock;

src/Grpc.Net.Client/Balancer/PollingResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -86,7 +86,7 @@ protected PollingResolver(ILoggerFactory loggerFactory, IBackoffPolicyFactory? b
8686
/// </para>
8787
/// </summary>
8888
/// <param name="listener">The callback used to receive updates on the target.</param>
89-
public override sealed void Start(Action<ResolverResult> listener)
89+
public sealed override void Start(Action<ResolverResult> listener)
9090
{
9191
if (listener == null)
9292
{

src/Grpc.Net.Client/Balancer/Subchannel.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -233,7 +233,21 @@ public void RequestConnection()
233233
}
234234
}
235235

236-
_ = ConnectTransportAsync();
236+
// Don't capture the current ExecutionContext and its AsyncLocals onto the connect
237+
bool restoreFlow = false;
238+
if (!ExecutionContext.IsFlowSuppressed())
239+
{
240+
ExecutionContext.SuppressFlow();
241+
restoreFlow = true;
242+
}
243+
244+
_ = Task.Run(ConnectTransportAsync);
245+
246+
// Restore the current ExecutionContext
247+
if (restoreFlow)
248+
{
249+
ExecutionContext.RestoreFlow();
250+
}
237251
}
238252

239253
private void CancelInProgressConnect()

src/Grpc.Net.Client/Grpc.Net.Client.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>.NET client for gRPC</Description>
@@ -35,6 +35,7 @@
3535
<Compile Include="..\Shared\NullableAttributes.cs" Link="Internal\NullableAttributes.cs" />
3636
<Compile Include="..\Shared\Http2ErrorCode.cs" Link="Internal\Http2ErrorCode.cs" />
3737
<Compile Include="..\Shared\Http3ErrorCode.cs" Link="Internal\Http3ErrorCode.cs" />
38+
<Compile Include="..\Shared\NonCapturingTimer.cs" Link="Internal\NonCapturingTimer.cs" />
3839
<Compile Include="..\Shared\NonDisposableMemoryStream.cs" Link="Internal\NonDisposableMemoryStream.cs" />
3940
</ItemGroup>
4041

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
5454
private readonly Dictionary<MethodKey, MethodConfig>? _serviceConfigMethods;
5555
private readonly bool _isSecure;
5656
private readonly List<CallCredentials>? _callCredentials;
57-
// Internal for testing
58-
internal readonly HashSet<IDisposable> ActiveCalls;
57+
private readonly HashSet<IDisposable> _activeCalls;
5958

6059
internal Uri Address { get; }
6160
internal HttpMessageInvoker HttpInvoker { get; }
@@ -165,7 +164,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
165164
ThrowOperationCanceledOnCancellation = channelOptions.ThrowOperationCanceledOnCancellation;
166165
UnsafeUseInsecureChannelCallCredentials = channelOptions.UnsafeUseInsecureChannelCallCredentials;
167166
_createMethodInfoFunc = CreateMethodInfo;
168-
ActiveCalls = new HashSet<IDisposable>();
167+
_activeCalls = new HashSet<IDisposable>();
169168
if (channelOptions.ServiceConfig is { } serviceConfig)
170169
{
171170
RetryThrottling = serviceConfig.RetryThrottling != null ? CreateChannelRetryThrottling(serviceConfig.RetryThrottling) : null;
@@ -490,15 +489,15 @@ internal void RegisterActiveCall(IDisposable grpcCall)
490489
throw new ObjectDisposedException(nameof(GrpcChannel));
491490
}
492491

493-
ActiveCalls.Add(grpcCall);
492+
_activeCalls.Add(grpcCall);
494493
}
495494
}
496495

497496
internal void FinishActiveCall(IDisposable grpcCall)
498497
{
499498
lock (_lock)
500499
{
501-
ActiveCalls.Remove(grpcCall);
500+
_activeCalls.Remove(grpcCall);
502501
}
503502
}
504503

@@ -749,9 +748,9 @@ public void Dispose()
749748
return;
750749
}
751750

752-
if (ActiveCalls.Count > 0)
751+
if (_activeCalls.Count > 0)
753752
{
754-
activeCallsCopy = ActiveCalls.ToArray();
753+
activeCallsCopy = _activeCalls.ToArray();
755754
}
756755

757756
Disposed = true;
@@ -807,6 +806,15 @@ internal int GetRandomNumber(int minValue, int maxValue)
807806
}
808807
}
809808

809+
// Internal for testing
810+
internal IDisposable[] GetActiveCalls()
811+
{
812+
lock (_lock)
813+
{
814+
return _activeCalls.ToArray();
815+
}
816+
}
817+
810818
#if SUPPORT_LOAD_BALANCING
811819
private sealed class SubChannelTransportFactory : ISubchannelTransportFactory
812820
{

src/Grpc.Net.Client/Internal/GrpcCall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ public Exception CreateFailureStatusException(Status status)
838838
GrpcCallLog.StartingDeadlineTimeout(Logger, timeout.Value);
839839

840840
var dueTime = CommonGrpcProtocolHelpers.GetTimerDueTime(timeout.Value, Channel.MaxTimerDueTime);
841-
_deadlineTimer = new Timer(DeadlineExceededCallback, null, dueTime, Timeout.Infinite);
841+
_deadlineTimer = NonCapturingTimer.Create(DeadlineExceededCallback, state: null, TimeSpan.FromMilliseconds(dueTime), Timeout.InfiniteTimeSpan);
842842
}
843843
}
844844

0 commit comments

Comments
 (0)