Skip to content
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

Wait to dispose CTS for IIS #9389

Merged
merged 14 commits into from
Apr 17, 2019
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;
jkotalik marked this conversation as resolved.
Show resolved Hide resolved

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)
halter73 marked this conversation as resolved.
Show resolved Hide resolved
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.
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
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);
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
}
}

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)]
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
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;
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
}, LoggerFactory))
{
using (var connection = testServer.CreateConnection())
{
await SendContentLength1Post(connection);
await connection.WaitForConnectionClose();
}
}
}

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