Skip to content

Commit 1e6ed7d

Browse files
authored
Fix rare memory 'leak' with WhenAll when it is combined with Task.Delay (#985)
1 parent d927507 commit 1e6ed7d

File tree

4 files changed

+53
-4
lines changed

4 files changed

+53
-4
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#if !NET6_0_OR_GREATER
2+
namespace GraphQL.Server.Transports.AspNetCore;
3+
4+
internal static class TaskExtensions
5+
{
6+
/// <summary>
7+
/// Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes,
8+
/// when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.
9+
/// </summary>
10+
/// <exception cref="OperationCanceledException"></exception>
11+
/// <exception cref="TimeoutException"></exception>
12+
public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout, CancellationToken cancellationToken)
13+
{
14+
var millisecondsTimeout = (int)timeout.TotalMilliseconds;
15+
if (millisecondsTimeout < -1)
16+
throw new ArgumentOutOfRangeException(nameof(timeout));
17+
if (task.IsCompleted || (millisecondsTimeout == -1 && !cancellationToken.CanBeCanceled))
18+
return task;
19+
if (millisecondsTimeout == 0)
20+
return Task.FromException<TResult>(new TimeoutException());
21+
if (cancellationToken.IsCancellationRequested)
22+
return Task.FromCanceled<TResult>(cancellationToken);
23+
24+
return TimeoutAfter(task, millisecondsTimeout, cancellationToken);
25+
26+
static async Task<TResult> TimeoutAfter(Task<TResult> task, int millisecondsDelay, CancellationToken cancellationToken)
27+
{
28+
// the CTS here ensures that the Task.Delay gets 'disposed' if the task finishes before the delay
29+
using var timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
30+
var completedTask = await Task.WhenAny(task, Task.Delay(millisecondsDelay, timeoutCancellationTokenSource.Token)).ConfigureAwait(false);
31+
if (completedTask == task)
32+
{
33+
// discontinue the Task.Delay
34+
timeoutCancellationTokenSource.Cancel();
35+
return await task.ConfigureAwait(false); // Very important in order to propagate exceptions
36+
}
37+
else
38+
{
39+
// was the cancellation token was signaled?
40+
cancellationToken.ThrowIfCancellationRequested();
41+
// or did it timeout?
42+
throw new TimeoutException();
43+
}
44+
}
45+
}
46+
}
47+
#endif

src/Transports.AspNetCore/Transports.AspNetCore.csproj

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

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
55
<Description>HTTP middleware for GraphQL</Description>
66
<PackageTags>GraphQL;middleware</PackageTags>
77
</PropertyGroup>

src/Transports.AspNetCore/WebSockets/WebSocketConnection.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ public virtual async Task ExecuteAsync(IOperationMessageProcessor operationMessa
123123
// queue the closure
124124
_ = CloseAsync();
125125
// wait until the close has been sent
126-
await Task.WhenAny(
127-
_outputClosed.Task,
128-
Task.Delay(_closeTimeout, RequestAborted));
126+
try
127+
{
128+
await _outputClosed.Task.WaitAsync(_closeTimeout, RequestAborted);
129+
}
130+
catch (TimeoutException) { }
129131
}
130132
// quit after the close request was fulfilled
131133
return;

0 commit comments

Comments
 (0)