Skip to content

Wait to dispose CTS for IIS #9389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Threading;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
Expand All @@ -12,6 +11,8 @@ internal partial class IISHttpContext : IHttpRequestLifetimeFeature
{
private CancellationTokenSource _abortedCts;
private CancellationToken? _manuallySetRequestAbortToken;
private object _abortLock = new object();
protected bool _requestAborted;

CancellationToken IHttpRequestLifetimeFeature.RequestAborted
{
Expand All @@ -22,16 +23,21 @@ CancellationToken IHttpRequestLifetimeFeature.RequestAborted
{
return _manuallySetRequestAbortToken.Value;
}
// Otherwise, get the abort CTS. If we have one, which would mean that someone previously
// asked for the RequestAborted token, simply return its token. If we don't,
// check to see whether we've already aborted, in which case just return an
// already canceled token. Finally, force a source into existence if we still
// don't have one, and return its token.
var cts = _abortedCts;
return
cts != null ? cts.Token :
(_requestAborted == 1) ? new CancellationToken(true) :
RequestAbortedSource.Token;

lock (_abortLock)
{
if (_requestAborted)
{
return new CancellationToken(true);
}

if (_abortedCts == null)
{
_abortedCts = new CancellationTokenSource();
}

return _abortedCts.Token;
}
}
set
{
Expand All @@ -41,28 +47,6 @@ CancellationToken IHttpRequestLifetimeFeature.RequestAborted
}
}

private CancellationTokenSource RequestAbortedSource
{
get
{
// Get the abort token, lazily-initializing it if necessary.
// Make sure it's canceled if an abort request already came in.

// EnsureInitialized can return null since _abortedCts is reset to null
// after it's already been initialized to a non-null value.
// If EnsureInitialized does return null, this property was accessed between
// requests so it's safe to return an ephemeral CancellationTokenSource.
var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource())
?? new CancellationTokenSource();

if (_requestAborted == 1)
{
cts.Cancel();
}
return cts;
}
}

void IHttpRequestLifetimeFeature.Abort()
{
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication));
Expand Down
38 changes: 30 additions & 8 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System;
using System.Buffers;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.Server.IIS.Core
Expand Down Expand Up @@ -140,7 +142,6 @@ private async Task WriteBody(bool flush = false)
var result = await _bodyOutput.Reader.ReadAsync();

var buffer = result.Buffer;

try
{
if (!buffer.IsEmpty)
Expand Down Expand Up @@ -186,9 +187,17 @@ private async Task WriteBody(bool flush = false)

internal void AbortIO(bool clientDisconnect)
{
if (Interlocked.CompareExchange(ref _requestAborted, 1, 0) != 0)
var shouldScheduleCancellation = false;

lock (_abortLock)
{
return;
if (_requestAborted)
{
return;
}

shouldScheduleCancellation = _abortedCts != null;
_requestAborted = true;
}

if (clientDisconnect)
Expand All @@ -198,20 +207,33 @@ internal void AbortIO(bool clientDisconnect)

_bodyOutput.Dispose();

var cts = _abortedCts;
if (cts != null)
if (shouldScheduleCancellation)
{
ThreadPool.QueueUserWorkItem(t =>
// Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
ThreadPool.UnsafeQueueUserWorkItem(ctx =>
{
try
{
cts.Cancel();
CancellationTokenSource localAbortCts = null;

lock (ctx._abortLock)
{
if (ctx._abortedCts != null)
{
localAbortCts = ctx._abortedCts;
ctx._abortedCts = null;
}
}

// If we cancel the cts, we don't dispose as people may still be using
// the cts. It also isn't necessary to dispose a canceled cts.
localAbortCts?.Cancel();
}
catch (Exception ex)
{
Log.ApplicationError(_logger, ((IHttpConnectionFeature)this).ConnectionId, TraceIdentifier, ex);
}
});
}, this, preferLocal: false);
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/Servers/IIS/IIS/src/Core/IISHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Principal;
Expand Down Expand Up @@ -59,7 +60,6 @@ internal abstract partial class IISHttpContext : NativeRequestContext, IThreadPo
protected Task _writeBodyTask;

private bool _wasUpgraded;
protected int _requestAborted;

protected Pipe _bodyInputPipe;
protected OutputProducer _bodyOutput;
Expand All @@ -68,7 +68,6 @@ internal abstract partial class IISHttpContext : NativeRequestContext, IThreadPo
private const string NegotiateString = "Negotiate";
private const string BasicString = "Basic";


internal unsafe IISHttpContext(
MemoryPool<byte> memoryPool,
IntPtr pInProcessHandler,
Expand Down Expand Up @@ -509,7 +508,16 @@ protected virtual void Dispose(bool disposing)
wi.Dispose();
}

_abortedCts?.Dispose();
// Lock to prevent CancelRequestAbortedToken from attempting to cancel a disposed CTS.
CancellationTokenSource localAbortCts = null;

lock (_abortLock)
{
localAbortCts = _abortedCts;
_abortedCts = null;
}

localAbortCts?.Dispose();

disposedValue = true;
}
Expand Down
8 changes: 2 additions & 6 deletions src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,8 @@ public override async Task<bool> ProcessRequestAsync()
try
{
context = _application.CreateContext(this);

await _application.ProcessRequestAsync(context);
// TODO Verification of Response
//if (Volatile.Read(ref _requestAborted) == 0)
//{
// VerifyResponseContentLength();
//}
}
catch (Exception ex)
{
Expand All @@ -59,7 +55,7 @@ public override async Task<bool> ProcessRequestAsync()
}
}

if (Volatile.Read(ref _requestAborted) == 0)
if (_requestAborted)
{
await ProduceEnd();
}
Expand Down
4 changes: 3 additions & 1 deletion src/Servers/IIS/IIS/test/IIS.Tests/ClientDisconnectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;

namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
Expand Down Expand Up @@ -52,7 +54,7 @@ public async Task WritesSucceedAfterClientDisconnect()
}

[ConditionalFact]
[Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/1831", FlakyOn.All)]
[Repeat(20)]
public async Task WritesCancelledWhenUsingAbortedToken()
{
var requestStartedCompletionSource = CreateTaskCompletionSource();
Expand Down
25 changes: 25 additions & 0 deletions src/Servers/IIS/IIS/test/IIS.Tests/ResponseAbortTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,31 @@ public async Task RequestAbortedIsTrippedAfterAbort()
Assert.True(tokenAborted);
}

[ConditionalFact]
public async Task CancellationTokenIsUsableAfterAbortingRequest()
{
using (var testServer = await TestServer.Create(async ctx =>
{
var token = ctx.RequestAborted;
var originalRegistration = token.Register(() => { });

ctx.Abort();

Assert.True(token.WaitHandle.WaitOne(10000));
Assert.True(ctx.RequestAborted.WaitHandle.WaitOne(10000));
Assert.Equal(token, originalRegistration.Token);

await Task.CompletedTask;
}, LoggerFactory))
{
using (var connection = testServer.CreateConnection())
{
await SendContentLength1Post(connection);
await connection.WaitForConnectionClose();
}
}
}

private static async Task SendContentLength1Post(TestConnection connection)
{
await connection.Send(
Expand Down