Skip to content
This repository has been archived by the owner on Dec 19, 2018. It is now read-only.

Request aborted #335

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
56653a5
Updating NuGet.config
pranavkm Jul 27, 2015
ad7b030
Updating json files to pin versions and build.cmd to pin KoreBuild an…
pranavkm Jul 28, 2015
e7ade98
Add support to cancel HttpContext.RequestAborted by disposing HttpRes…
tmds Aug 18, 2015
2cd343e
Implement PipelineCompleteAsync and fix implementation to pass Client…
tmds Aug 20, 2015
96972bd
Updating NuGet.config
pranavkm Jul 27, 2015
a1743cc
Updating json files to pin versions and build.cmd to pin KoreBuild an…
pranavkm Jul 28, 2015
a12863a
Add support to cancel HttpContext.RequestAborted by disposing HttpRes…
tmds Aug 18, 2015
ba46918
Implement PipelineCompleteAsync and fix implementation to pass Client…
tmds Aug 20, 2015
9afb17e
Update project.json files to match dev
tmds Aug 20, 2015
12d7182
bugfix, assign _pipelinefinished otherwise requestAborted is always c…
tmds Aug 20, 2015
19a9b86
remove project.lock.json files
tmds Aug 20, 2015
fad9dc9
Merge branch 'request_aborted' of https://github.com/tmds/Hosting int…
tmds Aug 20, 2015
17535b8
match dev branch
tmds Aug 20, 2015
696c905
Remove PipelineCompleteAsync
tmds Aug 24, 2015
677a7cb
Remove PipelineCompleteAsync cont.
tmds Aug 24, 2015
72906a9
RequestState can be private again
tmds Aug 24, 2015
e88c503
Code cleanup: reorder usings
tmds Sep 2, 2015
fc4c331
Add ClientCancellationAbortsRequest test, use ThrowsAnyAsync as a der…
tmds Sep 2, 2015
cfd11dc
Code cleanup: remove CancellationToken from RequestState
tmds Sep 2, 2015
9be8be3
ClientCancellationAbortsRequest: Cancel after 500ms
tmds Sep 2, 2015
f4bcdb9
Code cleanup: revert method names
tmds Sep 2, 2015
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
4 changes: 2 additions & 2 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="AspNetVNext" value="https://www.myget.org/F/aspnetvnext/api/v2" />
<add key="NuGet" value="https://nuget.org/api/v2/" />
<add key="AspNetVNext" value="https://www.myget.org/F/aspnetlitedev/api/v2" />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's still something odd with your rebase if you are including changes in this file.

</packageSources>
</configuration>
55 changes: 35 additions & 20 deletions src/Microsoft.AspNet.TestHost/ClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,41 +62,44 @@ protected override async Task<HttpResponseMessage> SendAsync(
body.Seek(0, SeekOrigin.Begin);
}
state.HttpContext.Request.Body = body;
var registration = cancellationToken.Register(state.Abort);
var registration = cancellationToken.Register(state.AbortRequest);

// Async offload, don't let the test code block the caller.
var offload = Task.Factory.StartNew(async () =>
{
try
{
await _next(state.FeatureCollection);
state.CompleteResponse();
state.PipelineComplete();
}
catch (Exception ex)
{
state.Abort(ex);
state.PipelineFailed(ex);
}
finally
{
registration.Dispose();
state.Dispose();
}
});

return await state.ResponseTask;
}

private class RequestState : IDisposable
private class RequestState
{
private readonly HttpRequestMessage _request;
private TaskCompletionSource<HttpResponseMessage> _responseTcs;
private ResponseStream _responseStream;
private ResponseFeature _responseFeature;
private CancellationTokenSource _requestAbortedSource;
private bool _pipelineFinished;

internal RequestState(HttpRequestMessage request, PathString pathBase, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This incoming CT needs to be linked to _requestAbortedSource.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind, that's on line 65

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a registration in SendAsync which calls AbortRequest, which then Cancels _requestAbortedSource. AbortRequest also calls Complete on the _responseStream.

{
_request = request;
_responseTcs = new TaskCompletionSource<HttpResponseMessage>();
_requestAbortedSource = new CancellationTokenSource();
_pipelineFinished = false;

if (request.RequestUri.IsDefaultPort)
{
Expand Down Expand Up @@ -146,9 +149,10 @@ internal RequestState(HttpRequestMessage request, PathString pathBase, Cancellat
}
}

_responseStream = new ResponseStream(CompleteResponse);
_responseStream = new ResponseStream(ReturnResponseMessage, AbortRequest);
HttpContext.Response.Body = _responseStream;
HttpContext.Response.StatusCode = 200;
HttpContext.RequestAborted = _requestAbortedSource.Token;
}

public HttpContext HttpContext { get; private set; }
Expand All @@ -160,7 +164,28 @@ public Task<HttpResponseMessage> ResponseTask
get { return _responseTcs.Task; }
}

internal void CompleteResponse()
public CancellationToken RequestAborted
{
get { return _requestAbortedSource.Token; }
}

public void AbortRequest()
{
if (!_pipelineFinished)
{
_requestAbortedSource.Cancel();
}
_responseStream.Complete();
}

internal void PipelineComplete()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method names should be verbs/actions. It's not clear why you renamed most of these methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renames where part of PipelineCompleteAsync feature, which has been removed.

{
_pipelineFinished = true;
ReturnResponseMessage();
_responseStream.Complete();
}

internal void ReturnResponseMessage()
{
if (!_responseTcs.Task.IsCompleted)
{
Expand All @@ -173,7 +198,7 @@ internal void CompleteResponse()

[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope",
Justification = "HttpResposneMessage must be returned to the caller.")]
internal HttpResponseMessage GenerateResponse()
private HttpResponseMessage GenerateResponse()
{
_responseFeature.FireOnSendingHeaders();

Expand All @@ -196,22 +221,12 @@ internal HttpResponseMessage GenerateResponse()
return response;
}

internal void Abort()
{
Abort(new OperationCanceledException());
}

internal void Abort(Exception exception)
internal void PipelineFailed(Exception exception)
{
_pipelineFinished = true;
_responseStream.Abort(exception);
_responseTcs.TrySetException(exception);
}

public void Dispose()
{
_responseStream.Dispose();
// Do not dispose the request, that will be disposed by the caller.
}
}
}
}
44 changes: 25 additions & 19 deletions src/Microsoft.AspNet.TestHost/ResponseStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.TestHost
// when requested by the client.
internal class ResponseStream : Stream
{
private bool _disposed;
private bool _complete;
private bool _aborted;
private Exception _abortException;
private ConcurrentQueue<byte[]> _bufferedData;
Expand All @@ -28,11 +28,13 @@ internal class ResponseStream : Stream

private Action _onFirstWrite;
private bool _firstWrite;
private Action _abortRequest;

internal ResponseStream([NotNull] Action onFirstWrite)
internal ResponseStream([NotNull] Action onFirstWrite, [NotNull] Action abortRequest)
{
_onFirstWrite = onFirstWrite;
_firstWrite = true;
_abortRequest = abortRequest;

_readLock = new SemaphoreSlim(1, 1);
_writeLock = new SemaphoreSlim(1, 1);
Expand Down Expand Up @@ -83,7 +85,7 @@ public override void SetLength(long value)

public override void Flush()
{
CheckDisposed();
CheckNotComplete();

_writeLock.Wait();
try
Expand Down Expand Up @@ -130,7 +132,7 @@ public override int Read(byte[] buffer, int offset, int count)
byte[] topBuffer = null;
while (!_bufferedData.TryDequeue(out topBuffer))
{
if (_disposed)
if (_complete)
{
CheckAborted();
// Graceful close
Expand Down Expand Up @@ -189,7 +191,7 @@ public async override Task<int> ReadAsync(byte[] buffer, int offset, int count,
byte[] topBuffer = null;
while (!_bufferedData.TryDequeue(out topBuffer))
{
if (_disposed)
if (_complete)
{
CheckAborted();
// Graceful close
Expand Down Expand Up @@ -233,7 +235,7 @@ private void FirstWrite()
public override void Write(byte[] buffer, int offset, int count)
{
VerifyBuffer(buffer, offset, count, allowEmpty: true);
CheckDisposed();
CheckNotComplete();

_writeLock.Wait();
try
Expand Down Expand Up @@ -317,7 +319,7 @@ private Task WaitForDataAsync()
{
_readWaitingForData = new TaskCompletionSource<object>();

if (!_bufferedData.IsEmpty || _disposed)
if (!_bufferedData.IsEmpty || _complete)
{
// Race, data could have arrived before we created the TCS.
_readWaitingForData.TrySetResult(null);
Expand All @@ -337,7 +339,18 @@ internal void Abort(Exception innerException)
Contract.Requires(innerException != null);
_aborted = true;
_abortException = innerException;
Dispose();
Complete();
}

internal void Complete()
{
// Prevent race with WaitForDataAsync
lock (_signalReadLock)
{
// Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads.
_complete = true;
_readWaitingForData.TrySetResult(null);
}
}

private void CheckAborted()
Expand All @@ -354,23 +367,16 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
// Prevent race with WaitForDataAsync
lock (_signalReadLock)
{
// Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads.
_disposed = true;
_readWaitingForData.TrySetResult(null);
}
_abortRequest();
}

base.Dispose(disposing);
}

private void CheckDisposed()
private void CheckNotComplete()
{
if (_disposed)
if (_complete)
{
throw new ObjectDisposedException(GetType().FullName);
throw new IOException("The request was aborted or the pipeline has finished");
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions test/Microsoft.AspNet.TestHost.Tests/TestClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Xunit;
using System.Threading;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ordering, System goes first.

using System;

namespace Microsoft.AspNet.TestHost
{
Expand Down Expand Up @@ -111,5 +113,38 @@ public async Task PostAsyncWorks()
// Assert
Assert.Equal("Hello world POST Response", await response.Content.ReadAsStringAsync());
}

[Fact]
public async Task ClientDisposalAbortsRequest()
{
// Arrange
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
RequestDelegate appDelegate = async ctx =>
{
// Write Headers
await ctx.Response.Body.FlushAsync();

var sem = new SemaphoreSlim(0);
try
{
await sem.WaitAsync(ctx.RequestAborted);
}
catch(Exception e)
{
tcs.SetException(e);
}
};

// Act
var server = TestServer.Create(app => app.Run(appDelegate));
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:12345");
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
// Abort Request
response.Dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a separate test for aborting a request before the response is received. You could do this with a CancellationToken or just dispose the HttpClient.


// Assert
var exception = await Assert.ThrowsAsync<OperationCanceledException>(async () => await tcs.Task);
}
}
}