Better handle HttpConnectionContext state transitions (#8225)
This commit is contained in:
parent
a5e20fdc90
commit
1338973212
|
|
@ -89,9 +89,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public override System.Collections.Generic.IDictionary<object, object> Items { get { throw null; } set { } }
|
public override System.Collections.Generic.IDictionary<object, object> Items { get { throw null; } set { } }
|
||||||
public System.DateTime LastSeenUtc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.DateTime LastSeenUtc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
public System.DateTime? LastSeenUtcIfInactive { get { throw null; } }
|
||||||
public System.Threading.Tasks.Task PreviousPollTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Threading.Tasks.Task PreviousPollTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Threading.SemaphoreSlim StateLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { get { throw null; } set { } }
|
|
||||||
public Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public override System.IO.Pipelines.IDuplexPipe Transport { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public override System.IO.Pipelines.IDuplexPipe Transport { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Threading.Tasks.Task TransportTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Threading.Tasks.Task TransportTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
|
@ -100,9 +100,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
public System.Threading.SemaphoreSlim WriteLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public System.Threading.SemaphoreSlim WriteLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
public System.Threading.Tasks.Task DisposeAsync(bool closeGracefully = false) { throw null; }
|
public System.Threading.Tasks.Task DisposeAsync(bool closeGracefully = false) { throw null; }
|
||||||
|
public void MarkInactive() { }
|
||||||
public void OnHeartbeat(System.Action<object> action, object state) { }
|
public void OnHeartbeat(System.Action<object> action, object state) { }
|
||||||
public void TickHeartbeat() { }
|
public void TickHeartbeat() { }
|
||||||
public bool TryChangeState(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus from, Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus to) { throw null; }
|
public bool TryActivateLongPollingConnection(Microsoft.AspNetCore.Connections.ConnectionDelegate connectionDelegate, Microsoft.AspNetCore.Http.HttpContext nonClonedContext, System.TimeSpan pollTimeout, System.Threading.Tasks.Task currentRequestTask, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Logging.ILogger dispatcherLogger) { throw null; }
|
||||||
|
public bool TryActivatePersistentConnection(Microsoft.AspNetCore.Connections.ConnectionDelegate connectionDelegate, Microsoft.AspNetCore.Http.Connections.Internal.Transports.IHttpTransport transport, Microsoft.Extensions.Logging.ILogger dispatcherLogger) { throw null; }
|
||||||
}
|
}
|
||||||
public partial class HttpConnectionDispatcher
|
public partial class HttpConnectionDispatcher
|
||||||
{
|
{
|
||||||
|
|
@ -122,8 +124,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
public System.Threading.Tasks.Task DisposeAndRemoveAsync(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection, bool closeGracefully) { throw null; }
|
public System.Threading.Tasks.Task DisposeAndRemoveAsync(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection, bool closeGracefully) { throw null; }
|
||||||
public void RemoveConnection(string id) { }
|
public void RemoveConnection(string id) { }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
public void Scan() { }
|
||||||
public System.Threading.Tasks.Task ScanAsync() { throw null; }
|
|
||||||
public void Start() { }
|
public void Start() { }
|
||||||
public bool TryGetConnection(string id, out Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection) { throw null; }
|
public bool TryGetConnection(string id, out Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection) { throw null; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
|
|
@ -12,7 +13,9 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Connections.Features;
|
using Microsoft.AspNetCore.Connections.Features;
|
||||||
using Microsoft.AspNetCore.Http.Connections.Features;
|
using Microsoft.AspNetCore.Http.Connections.Features;
|
||||||
|
using Microsoft.AspNetCore.Http.Connections.Internal.Transports;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Http.Connections.Internal
|
namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
|
|
@ -28,6 +31,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
IHttpTransportFeature,
|
IHttpTransportFeature,
|
||||||
IConnectionInherentKeepAliveFeature
|
IConnectionInherentKeepAliveFeature
|
||||||
{
|
{
|
||||||
|
private readonly object _stateLock = new object();
|
||||||
private readonly object _itemsLock = new object();
|
private readonly object _itemsLock = new object();
|
||||||
private readonly object _heartbeatLock = new object();
|
private readonly object _heartbeatLock = new object();
|
||||||
private List<(Action<object> handler, object state)> _heartbeatHandlers;
|
private List<(Action<object> handler, object state)> _heartbeatHandlers;
|
||||||
|
|
@ -35,7 +39,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
private PipeWriterStream _applicationStream;
|
private PipeWriterStream _applicationStream;
|
||||||
private IDuplexPipe _application;
|
private IDuplexPipe _application;
|
||||||
private IDictionary<object, object> _items;
|
private IDictionary<object, object> _items;
|
||||||
private int _status = (int)HttpConnectionStatus.Inactive;
|
|
||||||
|
|
||||||
// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
|
// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
|
||||||
// on the same task
|
// on the same task
|
||||||
|
|
@ -83,7 +86,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
public HttpTransportType TransportType { get; set; }
|
public HttpTransportType TransportType { get; set; }
|
||||||
|
|
||||||
public SemaphoreSlim WriteLock { get; } = new SemaphoreSlim(1, 1);
|
public SemaphoreSlim WriteLock { get; } = new SemaphoreSlim(1, 1);
|
||||||
public SemaphoreSlim StateLock { get; } = new SemaphoreSlim(1, 1);
|
|
||||||
|
|
||||||
// Used for testing only
|
// Used for testing only
|
||||||
internal Task DisposeAndRemoveTask { get; set; }
|
internal Task DisposeAndRemoveTask { get; set; }
|
||||||
|
|
@ -96,7 +98,18 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
|
|
||||||
public DateTime LastSeenUtc { get; set; }
|
public DateTime LastSeenUtc { get; set; }
|
||||||
|
|
||||||
public HttpConnectionStatus Status { get => (HttpConnectionStatus)_status; set => Interlocked.Exchange(ref _status, (int)value); }
|
public DateTime? LastSeenUtcIfInactive
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
return Status == HttpConnectionStatus.Inactive ? (DateTime?)LastSeenUtc : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpConnectionStatus Status { get; set; } = HttpConnectionStatus.Inactive;
|
||||||
|
|
||||||
public override string ConnectionId { get; set; }
|
public override string ConnectionId { get; set; }
|
||||||
|
|
||||||
|
|
@ -184,29 +197,29 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
{
|
{
|
||||||
Task disposeTask;
|
Task disposeTask;
|
||||||
|
|
||||||
await StateLock.WaitAsync();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Status == HttpConnectionStatus.Disposed)
|
lock (_stateLock)
|
||||||
{
|
{
|
||||||
disposeTask = _disposeTcs.Task;
|
if (Status == HttpConnectionStatus.Disposed)
|
||||||
}
|
{
|
||||||
else
|
disposeTask = _disposeTcs.Task;
|
||||||
{
|
}
|
||||||
Status = HttpConnectionStatus.Disposed;
|
else
|
||||||
|
{
|
||||||
|
Status = HttpConnectionStatus.Disposed;
|
||||||
|
|
||||||
Log.DisposingConnection(_logger, ConnectionId);
|
Log.DisposingConnection(_logger, ConnectionId);
|
||||||
|
|
||||||
var applicationTask = ApplicationTask ?? Task.CompletedTask;
|
var applicationTask = ApplicationTask ?? Task.CompletedTask;
|
||||||
var transportTask = TransportTask ?? Task.CompletedTask;
|
var transportTask = TransportTask ?? Task.CompletedTask;
|
||||||
|
|
||||||
disposeTask = WaitOnTasks(applicationTask, transportTask, closeGracefully);
|
disposeTask = WaitOnTasks(applicationTask, transportTask, closeGracefully);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
StateLock.Release();
|
|
||||||
|
|
||||||
Cancellation?.Dispose();
|
Cancellation?.Dispose();
|
||||||
|
|
||||||
Cancellation = null;
|
Cancellation = null;
|
||||||
|
|
@ -310,9 +323,145 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryChangeState(HttpConnectionStatus from, HttpConnectionStatus to)
|
public bool TryActivatePersistentConnection(
|
||||||
|
ConnectionDelegate connectionDelegate,
|
||||||
|
IHttpTransport transport,
|
||||||
|
ILogger dispatcherLogger)
|
||||||
{
|
{
|
||||||
return Interlocked.CompareExchange(ref _status, (int)to, (int)from) == (int)from;
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Status == HttpConnectionStatus.Inactive)
|
||||||
|
{
|
||||||
|
Status = HttpConnectionStatus.Active;
|
||||||
|
|
||||||
|
// Call into the end point passing the connection
|
||||||
|
ApplicationTask = ExecuteApplication(connectionDelegate);
|
||||||
|
|
||||||
|
// Start the transport
|
||||||
|
TransportTask = transport.ProcessRequestAsync(HttpContext, HttpContext.RequestAborted);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FailActivationUnsynchronized(HttpContext, dispatcherLogger);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryActivateLongPollingConnection(
|
||||||
|
ConnectionDelegate connectionDelegate,
|
||||||
|
HttpContext nonClonedContext,
|
||||||
|
TimeSpan pollTimeout,
|
||||||
|
Task currentRequestTask,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
ILogger dispatcherLogger)
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Status == HttpConnectionStatus.Inactive)
|
||||||
|
{
|
||||||
|
Status = HttpConnectionStatus.Active;
|
||||||
|
|
||||||
|
PreviousPollTask = currentRequestTask;
|
||||||
|
|
||||||
|
// Raise OnConnected for new connections only since polls happen all the time
|
||||||
|
if (ApplicationTask == null)
|
||||||
|
{
|
||||||
|
HttpConnectionDispatcher.Log.EstablishedConnection(dispatcherLogger);
|
||||||
|
|
||||||
|
ApplicationTask = ExecuteApplication(connectionDelegate);
|
||||||
|
|
||||||
|
nonClonedContext.Response.ContentType = "application/octet-stream";
|
||||||
|
|
||||||
|
// This request has no content
|
||||||
|
nonClonedContext.Response.ContentLength = 0;
|
||||||
|
|
||||||
|
// On the first poll, we flush the response immediately to mark the poll as "initialized" so future
|
||||||
|
// requests can be made safely
|
||||||
|
TransportTask = nonClonedContext.Response.Body.FlushAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HttpConnectionDispatcher.Log.ResumingConnection(dispatcherLogger);
|
||||||
|
|
||||||
|
// REVIEW: Performance of this isn't great as this does a bunch of per request allocations
|
||||||
|
Cancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var timeoutSource = new CancellationTokenSource();
|
||||||
|
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(Cancellation.Token, nonClonedContext.RequestAborted, timeoutSource.Token);
|
||||||
|
|
||||||
|
// Dispose these tokens when the request is over
|
||||||
|
nonClonedContext.Response.RegisterForDispose(timeoutSource);
|
||||||
|
nonClonedContext.Response.RegisterForDispose(tokenSource);
|
||||||
|
|
||||||
|
var longPolling = new LongPollingTransport(timeoutSource.Token, Application.Input, loggerFactory);
|
||||||
|
|
||||||
|
// Start the transport
|
||||||
|
TransportTask = longPolling.ProcessRequestAsync(nonClonedContext, tokenSource.Token);
|
||||||
|
|
||||||
|
// Start the timeout after we return from creating the transport task
|
||||||
|
timeoutSource.CancelAfter(pollTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FailActivationUnsynchronized(nonClonedContext, dispatcherLogger);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FailActivationUnsynchronized(HttpContext nonClonedContext, ILogger dispatcherLogger)
|
||||||
|
{
|
||||||
|
if (Status == HttpConnectionStatus.Active)
|
||||||
|
{
|
||||||
|
HttpConnectionDispatcher.Log.ConnectionAlreadyActive(dispatcherLogger, ConnectionId, HttpContext.TraceIdentifier);
|
||||||
|
|
||||||
|
// Reject the request with a 409 conflict
|
||||||
|
nonClonedContext.Response.StatusCode = StatusCodes.Status409Conflict;
|
||||||
|
nonClonedContext.Response.ContentType = "text/plain";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(Status == HttpConnectionStatus.Disposed);
|
||||||
|
|
||||||
|
HttpConnectionDispatcher.Log.ConnectionDisposed(dispatcherLogger, ConnectionId);
|
||||||
|
|
||||||
|
// Connection was disposed
|
||||||
|
nonClonedContext.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
nonClonedContext.Response.ContentType = "text/plain";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkInactive()
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
if (Status == HttpConnectionStatus.Active)
|
||||||
|
{
|
||||||
|
Status = HttpConnectionStatus.Inactive;
|
||||||
|
LastSeenUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteApplication(ConnectionDelegate connectionDelegate)
|
||||||
|
{
|
||||||
|
// Verify some initialization invariants
|
||||||
|
Debug.Assert(TransportType != HttpTransportType.None, "Transport has not been initialized yet");
|
||||||
|
|
||||||
|
// Jump onto the thread pool thread so blocking user code doesn't block the setup of the
|
||||||
|
// connection and transport
|
||||||
|
await AwaitableThreadPool.Yield();
|
||||||
|
|
||||||
|
// Running this in an async method turns sync exceptions into async ones
|
||||||
|
await connectionDelegate(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Log
|
private static class Log
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
{
|
{
|
||||||
public partial class HttpConnectionDispatcher
|
public partial class HttpConnectionDispatcher
|
||||||
{
|
{
|
||||||
private static class Log
|
internal static class Log
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, string, Exception> _connectionDisposed =
|
private static readonly Action<ILogger, string, Exception> _connectionDisposed =
|
||||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "ConnectionDisposed"), "Connection {TransportConnectionId} was disposed.");
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "ConnectionDisposed"), "Connection {TransportConnectionId} was disposed.");
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
@ -194,99 +193,36 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
// Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls
|
// Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls
|
||||||
var currentRequestTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var currentRequestTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
await connection.StateLock.WaitAsync();
|
using (connection.Cancellation)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (connection.Status == HttpConnectionStatus.Disposed)
|
// Cancel the previous request
|
||||||
{
|
connection.Cancellation?.Cancel();
|
||||||
Log.ConnectionDisposed(_logger, connection.ConnectionId);
|
|
||||||
|
|
||||||
// The connection was disposed
|
try
|
||||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
{
|
||||||
|
// Wait for the previous request to drain
|
||||||
|
await connection.PreviousPollTask;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Previous poll canceled due to connection closing, close this poll too
|
||||||
context.Response.ContentType = "text/plain";
|
context.Response.ContentType = "text/plain";
|
||||||
|
context.Response.StatusCode = StatusCodes.Status204NoContent;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connection.Status == HttpConnectionStatus.Active)
|
|
||||||
{
|
|
||||||
var existing = connection.GetHttpContext();
|
|
||||||
Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, existing.TraceIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (connection.Cancellation)
|
|
||||||
{
|
|
||||||
// Cancel the previous request
|
|
||||||
connection.Cancellation?.Cancel();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Wait for the previous request to drain
|
|
||||||
await connection.PreviousPollTask;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Previous poll canceled due to connection closing, close this poll too
|
|
||||||
context.Response.ContentType = "text/plain";
|
|
||||||
context.Response.StatusCode = StatusCodes.Status204NoContent;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.PreviousPollTask = currentRequestTcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the connection as active
|
|
||||||
connection.TryChangeState(from: HttpConnectionStatus.Inactive, to: HttpConnectionStatus.Active);
|
|
||||||
|
|
||||||
// Raise OnConnected for new connections only since polls happen all the time
|
|
||||||
if (connection.ApplicationTask == null)
|
|
||||||
{
|
|
||||||
Log.EstablishedConnection(_logger);
|
|
||||||
|
|
||||||
connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection);
|
|
||||||
|
|
||||||
context.Response.ContentType = "application/octet-stream";
|
|
||||||
|
|
||||||
// This request has no content
|
|
||||||
context.Response.ContentLength = 0;
|
|
||||||
|
|
||||||
// On the first poll, we flush the response immediately to mark the poll as "initialized" so future
|
|
||||||
// requests can be made safely
|
|
||||||
connection.TransportTask = context.Response.Body.FlushAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.ResumingConnection(_logger);
|
|
||||||
|
|
||||||
// REVIEW: Performance of this isn't great as this does a bunch of per request allocations
|
|
||||||
connection.Cancellation = new CancellationTokenSource();
|
|
||||||
|
|
||||||
var timeoutSource = new CancellationTokenSource();
|
|
||||||
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token);
|
|
||||||
|
|
||||||
// Dispose these tokens when the request is over
|
|
||||||
context.Response.RegisterForDispose(timeoutSource);
|
|
||||||
context.Response.RegisterForDispose(tokenSource);
|
|
||||||
|
|
||||||
var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, _loggerFactory);
|
|
||||||
|
|
||||||
// Start the transport
|
|
||||||
connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token);
|
|
||||||
|
|
||||||
// Start the timeout after we return from creating the transport task
|
|
||||||
timeoutSource.CancelAfter(options.LongPolling.PollTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (!connection.TryActivateLongPollingConnection(
|
||||||
|
connectionDelegate, context, options.LongPolling.PollTimeout,
|
||||||
|
currentRequestTcs.Task, _loggerFactory, _logger))
|
||||||
{
|
{
|
||||||
connection.StateLock.Release();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);
|
var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pollAgain = true;
|
|
||||||
|
|
||||||
// If the application ended before the transport task then we potentially need to end the connection
|
// If the application ended before the transport task then we potentially need to end the connection
|
||||||
if (resultTask == connection.ApplicationTask)
|
if (resultTask == connection.ApplicationTask)
|
||||||
{
|
{
|
||||||
|
|
@ -305,9 +241,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
// We should be able to safely dispose because there's no more data being written
|
// We should be able to safely dispose because there's no more data being written
|
||||||
// We don't need to wait for close here since we've already waited for both sides
|
// We don't need to wait for close here since we've already waited for both sides
|
||||||
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
|
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
|
||||||
|
}
|
||||||
// Don't poll again if we've removed the connection completely
|
else
|
||||||
pollAgain = false;
|
{
|
||||||
|
// Only allow repoll if we aren't removing the connection.
|
||||||
|
connection.MarkInactive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (resultTask.IsFaulted)
|
else if (resultTask.IsFaulted)
|
||||||
|
|
@ -317,23 +255,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
|
|
||||||
// transport task was faulted, we should remove the connection
|
// transport task was faulted, we should remove the connection
|
||||||
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
|
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
|
||||||
|
|
||||||
pollAgain = false;
|
|
||||||
}
|
}
|
||||||
else if (context.Response.StatusCode == StatusCodes.Status204NoContent)
|
else
|
||||||
{
|
{
|
||||||
// Don't poll if the transport task was canceled
|
// Only allow repoll if we aren't removing the connection.
|
||||||
pollAgain = false;
|
connection.MarkInactive();
|
||||||
}
|
|
||||||
|
|
||||||
if (pollAgain)
|
|
||||||
{
|
|
||||||
// Mark the connection as inactive
|
|
||||||
connection.LastSeenUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// This is done outside a lock because the next poll might be waiting in the lock already and waiting for currentRequestTcs to complete
|
|
||||||
// A DELETE request could have set the status to Disposed. If that is the case we don't want to change the state ever.
|
|
||||||
connection.TryChangeState(from: HttpConnectionStatus.Active, to: HttpConnectionStatus.Inactive);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -350,59 +276,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
HttpConnectionContext connection)
|
HttpConnectionContext connection)
|
||||||
{
|
{
|
||||||
await connection.StateLock.WaitAsync();
|
if (connection.TryActivatePersistentConnection(connectionDelegate, transport, _logger))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (connection.Status == HttpConnectionStatus.Disposed)
|
// Wait for any of them to end
|
||||||
{
|
await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);
|
||||||
Log.ConnectionDisposed(_logger, connection.ConnectionId);
|
|
||||||
|
|
||||||
// Connection was disposed
|
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: true);
|
||||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's already an active request
|
|
||||||
if (connection.Status == HttpConnectionStatus.Active)
|
|
||||||
{
|
|
||||||
Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, connection.GetHttpContext().TraceIdentifier);
|
|
||||||
|
|
||||||
// Reject the request with a 409 conflict
|
|
||||||
context.Response.StatusCode = StatusCodes.Status409Conflict;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the connection as active
|
|
||||||
connection.TryChangeState(HttpConnectionStatus.Inactive, HttpConnectionStatus.Active);
|
|
||||||
|
|
||||||
// Call into the end point passing the connection
|
|
||||||
connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection);
|
|
||||||
|
|
||||||
// Start the transport
|
|
||||||
connection.TransportTask = transport.ProcessRequestAsync(context, context.RequestAborted);
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
connection.StateLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for any of them to end
|
|
||||||
await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);
|
|
||||||
|
|
||||||
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteApplication(ConnectionDelegate connectionDelegate, HttpConnectionContext connection)
|
|
||||||
{
|
|
||||||
// Verify some initialization invariants
|
|
||||||
Debug.Assert(connection.TransportType != HttpTransportType.None, "Transport has not been initialized yet");
|
|
||||||
|
|
||||||
// Jump onto the thread pool thread so blocking user code doesn't block the setup of the
|
|
||||||
// connection and transport
|
|
||||||
await AwaitableThreadPool.Yield();
|
|
||||||
|
|
||||||
// Running this in an async method turns sync exceptions into async ones
|
|
||||||
await connectionDelegate(connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
|
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ScanAsync();
|
Scan();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -137,32 +137,18 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
Log.HeartBeatEnded(_logger);
|
Log.HeartBeatEnded(_logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScanAsync()
|
public void Scan()
|
||||||
{
|
{
|
||||||
// Scan the registered connections looking for ones that have timed out
|
// Scan the registered connections looking for ones that have timed out
|
||||||
foreach (var c in _connections)
|
foreach (var c in _connections)
|
||||||
{
|
{
|
||||||
HttpConnectionStatus status;
|
|
||||||
DateTimeOffset lastSeenUtc;
|
|
||||||
var connection = c.Value.Connection;
|
var connection = c.Value.Connection;
|
||||||
|
// Capture the connection state
|
||||||
await connection.StateLock.WaitAsync();
|
var lastSeenUtc = connection.LastSeenUtcIfInactive;
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Capture the connection state
|
|
||||||
status = connection.Status;
|
|
||||||
|
|
||||||
lastSeenUtc = connection.LastSeenUtc;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
connection.StateLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the decision has been made to dispose we don't check the status again
|
// Once the decision has been made to dispose we don't check the status again
|
||||||
// But don't clean up connections while the debugger is attached.
|
// But don't clean up connections while the debugger is attached.
|
||||||
if (!Debugger.IsAttached && status == HttpConnectionStatus.Inactive && (DateTimeOffset.UtcNow - lastSeenUtc).TotalSeconds > _disconnectTimeout.TotalSeconds)
|
if (!Debugger.IsAttached && lastSeenUtc.HasValue && (DateTimeOffset.UtcNow - lastSeenUtc.Value).TotalSeconds > _disconnectTimeout.TotalSeconds)
|
||||||
{
|
{
|
||||||
Log.ConnectionTimedOut(_logger, connection.ConnectionId);
|
Log.ConnectionTimedOut(_logger, connection.ConnectionId);
|
||||||
HttpConnectionsEventSource.Log.ConnectionTimedOut(connection.ConnectionId);
|
HttpConnectionsEventSource.Log.ConnectionTimedOut(connection.ConnectionId);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports
|
||||||
{
|
{
|
||||||
// 3 cases:
|
// 3 cases:
|
||||||
// 1 - Request aborted, the client disconnected (no response)
|
// 1 - Request aborted, the client disconnected (no response)
|
||||||
// 2 - The poll timeout is hit (204)
|
// 2 - The poll timeout is hit (200)
|
||||||
// 3 - A new request comes in and cancels this request (204)
|
// 3 - A new request comes in and cancels this request (204)
|
||||||
|
|
||||||
// Case 1
|
// Case 1
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
// The application is still running here because the poll is only killed
|
// The application is still running here because the poll is only killed
|
||||||
// by the heartbeat so we pretend to do a scan and this should force the application task to complete
|
// by the heartbeat so we pretend to do a scan and this should force the application task to complete
|
||||||
await manager.ScanAsync();
|
manager.Scan();
|
||||||
|
|
||||||
// The application task should complete gracefully
|
// The application task should complete gracefully
|
||||||
await connection.ApplicationTask.OrTimeout();
|
await connection.ApplicationTask.OrTimeout();
|
||||||
|
|
@ -1061,6 +1061,66 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MultipleRequestsToActiveConnectionId409ForLongPolling()
|
||||||
|
{
|
||||||
|
using (StartVerifiableLog())
|
||||||
|
{
|
||||||
|
var manager = CreateConnectionManager(LoggerFactory);
|
||||||
|
var connection = manager.CreateConnection();
|
||||||
|
connection.TransportType = HttpTransportType.LongPolling;
|
||||||
|
|
||||||
|
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
|
||||||
|
|
||||||
|
var context1 = MakeRequest("/foo", connection);
|
||||||
|
var context2 = MakeRequest("/foo", connection);
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<TestConnectionHandler>();
|
||||||
|
var builder = new ConnectionBuilder(services.BuildServiceProvider());
|
||||||
|
builder.UseConnectionHandler<TestConnectionHandler>();
|
||||||
|
var app = builder.Build();
|
||||||
|
var options = new HttpConnectionDispatcherOptions();
|
||||||
|
|
||||||
|
// Prime the polling. Expect any empty response showing the transport is initialized.
|
||||||
|
var request1 = dispatcher.ExecuteAsync(context1, options, app);
|
||||||
|
Assert.True(request1.IsCompleted);
|
||||||
|
|
||||||
|
// Manually control PreviousPollTask instead of using a real PreviousPollTask, because a real
|
||||||
|
// PreviousPollTask might complete too early when the second request cancels it.
|
||||||
|
var lastPollTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
connection.PreviousPollTask = lastPollTcs.Task;
|
||||||
|
|
||||||
|
request1 = dispatcher.ExecuteAsync(context1, options, app);
|
||||||
|
var request2 = dispatcher.ExecuteAsync(context2, options, app);
|
||||||
|
|
||||||
|
Assert.False(request1.IsCompleted);
|
||||||
|
Assert.False(request2.IsCompleted);
|
||||||
|
|
||||||
|
lastPollTcs.SetResult(null);
|
||||||
|
|
||||||
|
var completedTask = await Task.WhenAny(request1, request2).OrTimeout();
|
||||||
|
|
||||||
|
if (completedTask == request1)
|
||||||
|
{
|
||||||
|
Assert.Equal(StatusCodes.Status409Conflict, context1.Response.StatusCode);
|
||||||
|
Assert.False(request2.IsCompleted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(StatusCodes.Status409Conflict, context2.Response.StatusCode);
|
||||||
|
Assert.False(request1.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(HttpConnectionStatus.Active, connection.Status);
|
||||||
|
|
||||||
|
manager.CloseConnections();
|
||||||
|
|
||||||
|
await request1.OrTimeout();
|
||||||
|
await request2.OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(HttpTransportType.ServerSentEvents)]
|
[InlineData(HttpTransportType.ServerSentEvents)]
|
||||||
[InlineData(HttpTransportType.LongPolling)]
|
[InlineData(HttpTransportType.LongPolling)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue