Skip to content

Commit e04c79b

Browse files
authored
Wait to dispose RequestAborted CTS (#9333)
1 parent 0684498 commit e04c79b

File tree

3 files changed

+50
-22
lines changed

3 files changed

+50
-22
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal abstract partial class HttpProtocol : IDefaultHttpContextContainer, IHt
3535
private Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
3636

3737
private object _abortLock = new object();
38-
private volatile bool _requestAborted;
38+
private volatile bool _connectionAborted;
3939
private bool _preventRequestAbortedCancellation;
4040
private CancellationTokenSource _abortedCts;
4141
private CancellationToken? _manuallySetRequestAbortToken;
@@ -257,7 +257,7 @@ public CancellationToken RequestAborted
257257
return new CancellationToken(false);
258258
}
259259

260-
if (_requestAborted)
260+
if (_connectionAborted)
261261
{
262262
return new CancellationToken(true);
263263
}
@@ -375,18 +375,19 @@ public void Reset()
375375
Scheme = _scheme;
376376

377377
_manuallySetRequestAbortToken = null;
378-
_preventRequestAbortedCancellation = false;
379378

380-
// Lock to prevent CancelRequestAbortedToken from attempting to cancel an disposed CTS.
379+
// Lock to prevent CancelRequestAbortedToken from attempting to cancel a disposed CTS.
380+
CancellationTokenSource localAbortCts = null;
381+
381382
lock (_abortLock)
382383
{
383-
if (!_requestAborted)
384-
{
385-
_abortedCts?.Dispose();
386-
_abortedCts = null;
387-
}
384+
_preventRequestAbortedCancellation = false;
385+
localAbortCts = _abortedCts;
386+
_abortedCts = null;
388387
}
389388

389+
localAbortCts?.Dispose();
390+
390391
if (_wrapperObjectsToDispose != null)
391392
{
392393
foreach (var disposable in _wrapperObjectsToDispose)
@@ -442,9 +443,20 @@ private void CancelRequestAbortedToken()
442443
{
443444
try
444445
{
445-
_abortedCts.Cancel();
446-
_abortedCts.Dispose();
447-
_abortedCts = null;
446+
CancellationTokenSource localAbortCts = null;
447+
448+
lock (_abortLock)
449+
{
450+
if (_abortedCts != null && !_preventRequestAbortedCancellation)
451+
{
452+
localAbortCts = _abortedCts;
453+
_abortedCts = null;
454+
}
455+
}
456+
457+
// If we cancel the cts, we don't dispose as people may still be using
458+
// the cts. It also isn't necessary to dispose a canceled cts.
459+
localAbortCts?.Cancel();
448460
}
449461
catch (Exception ex)
450462
{
@@ -454,17 +466,20 @@ private void CancelRequestAbortedToken()
454466

455467
protected void AbortRequest()
456468
{
469+
var shouldScheduleCancellation = false;
470+
457471
lock (_abortLock)
458472
{
459-
if (_requestAborted)
473+
if (_connectionAborted)
460474
{
461475
return;
462476
}
463477

464-
_requestAborted = true;
478+
shouldScheduleCancellation = _abortedCts != null && !_preventRequestAbortedCancellation;
479+
_connectionAborted = true;
465480
}
466481

467-
if (_abortedCts != null)
482+
if (shouldScheduleCancellation)
468483
{
469484
// Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
470485
ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state).CancelRequestAbortedToken(), this);
@@ -481,14 +496,12 @@ private void PreventRequestAbortedCancellation()
481496
{
482497
lock (_abortLock)
483498
{
484-
if (_requestAborted)
499+
if (_connectionAborted)
485500
{
486501
return;
487502
}
488503

489504
_preventRequestAbortedCancellation = true;
490-
_abortedCts?.Dispose();
491-
_abortedCts = null;
492505
}
493506
}
494507

@@ -594,7 +607,7 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
594607
// Run the application code for this request
595608
await application.ProcessRequestAsync(context);
596609

597-
if (!_requestAborted)
610+
if (!_connectionAborted)
598611
{
599612
VerifyResponseContentLength();
600613
}
@@ -630,7 +643,7 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
630643
// 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down.
631644
if (_requestRejectedException == null)
632645
{
633-
if (!_requestAborted)
646+
if (!_connectionAborted)
634647
{
635648
// Call ProduceEnd() before consuming the rest of the request body to prevent
636649
// delaying clients waiting for the chunk terminator:
@@ -662,7 +675,7 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
662675
application.DisposeContext(context, _applicationException);
663676

664677
// Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
665-
if (!_requestAborted && _requestRejectedException == null && !messageBody.IsEmpty)
678+
if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
666679
{
667680
await messageBody.ConsumeAsync();
668681
}
@@ -964,7 +977,7 @@ private void VerifyInitializeState(int firstWriteByteCount)
964977
protected Task TryProduceInvalidRequestResponse()
965978
{
966979
// If _requestAborted is set, the connection has already been closed.
967-
if (_requestRejectedException != null && !_requestAborted)
980+
if (_requestRejectedException != null && !_connectionAborted)
968981
{
969982
return ProduceEnd();
970983
}

src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,20 @@ public async Task RequestAbortedTokenIsResetBeforeLastWriteWithChunkedEncoding()
729729
Assert.False(_http1Connection.RequestAborted.IsCancellationRequested);
730730
}
731731

732+
[Fact]
733+
public void RequestAbortedTokenIsFullyUsableAfterCancellation()
734+
{
735+
var originalToken = _http1Connection.RequestAborted;
736+
var originalRegistration = originalToken.Register(() => { });
737+
738+
_http1Connection.Abort(new ConnectionAbortedException());
739+
740+
Assert.True(originalToken.WaitHandle.WaitOne(TestConstants.DefaultTimeout));
741+
Assert.True(_http1Connection.RequestAborted.WaitHandle.WaitOne(TestConstants.DefaultTimeout));
742+
743+
Assert.Equal(originalToken, originalRegistration.Token);
744+
}
745+
732746
[Fact]
733747
public void RequestAbortedTokenIsUsableAfterCancellation()
734748
{

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null,
217217
}
218218

219219
[Fact]
220+
[Repeat(20)]
220221
public async Task DoesNotThrowObjectDisposedExceptionFromWriteAsyncAfterConnectionIsAborted()
221222
{
222223
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);

0 commit comments

Comments
 (0)