Skip to content

Commit 1338973

Browse files
authored
Better handle HttpConnectionContext state transitions (#8225)
1 parent a5e20fd commit 1338973

File tree

7 files changed

+269
-193
lines changed

7 files changed

+269
-193
lines changed

src/SignalR/common/Http.Connections/ref/Microsoft.AspNetCore.Http.Connections.netcoreapp3.0.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ public HttpConnectionContext(string id, System.IO.Pipelines.IDuplexPipe transpor
8989
public Microsoft.AspNetCore.Http.HttpContext HttpContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9090
public override System.Collections.Generic.IDictionary<object, object> Items { get { throw null; } set { } }
9191
public System.DateTime LastSeenUtc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
92+
public System.DateTime? LastSeenUtcIfInactive { get { throw null; } }
9293
public System.Threading.Tasks.Task PreviousPollTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
93-
public System.Threading.SemaphoreSlim StateLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
94-
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { get { throw null; } set { } }
94+
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9595
public Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9696
public override System.IO.Pipelines.IDuplexPipe Transport { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9797
public System.Threading.Tasks.Task TransportTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
@@ -100,9 +100,11 @@ public HttpConnectionContext(string id, System.IO.Pipelines.IDuplexPipe transpor
100100
public System.Threading.SemaphoreSlim WriteLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
101101
[System.Diagnostics.DebuggerStepThroughAttribute]
102102
public System.Threading.Tasks.Task DisposeAsync(bool closeGracefully = false) { throw null; }
103+
public void MarkInactive() { }
103104
public void OnHeartbeat(System.Action<object> action, object state) { }
104105
public void TickHeartbeat() { }
105-
public bool TryChangeState(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus from, Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus to) { throw null; }
106+
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; }
107+
public bool TryActivatePersistentConnection(Microsoft.AspNetCore.Connections.ConnectionDelegate connectionDelegate, Microsoft.AspNetCore.Http.Connections.Internal.Transports.IHttpTransport transport, Microsoft.Extensions.Logging.ILogger dispatcherLogger) { throw null; }
106108
}
107109
public partial class HttpConnectionDispatcher
108110
{
@@ -122,8 +124,7 @@ public void CloseConnections() { }
122124
[System.Diagnostics.DebuggerStepThroughAttribute]
123125
public System.Threading.Tasks.Task DisposeAndRemoveAsync(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection, bool closeGracefully) { throw null; }
124126
public void RemoveConnection(string id) { }
125-
[System.Diagnostics.DebuggerStepThroughAttribute]
126-
public System.Threading.Tasks.Task ScanAsync() { throw null; }
127+
public void Scan() { }
127128
public void Start() { }
128129
public bool TryGetConnection(string id, out Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionContext connection) { throw null; }
129130
}

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs

Lines changed: 167 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.IO.Pipelines;
89
using System.Security.Claims;
910
using System.Security.Principal;
@@ -12,7 +13,9 @@
1213
using Microsoft.AspNetCore.Connections;
1314
using Microsoft.AspNetCore.Connections.Features;
1415
using Microsoft.AspNetCore.Http.Connections.Features;
16+
using Microsoft.AspNetCore.Http.Connections.Internal.Transports;
1517
using Microsoft.AspNetCore.Http.Features;
18+
using Microsoft.AspNetCore.Internal;
1619
using Microsoft.Extensions.Logging;
1720

1821
namespace Microsoft.AspNetCore.Http.Connections.Internal
@@ -28,14 +31,14 @@ public class HttpConnectionContext : ConnectionContext,
2831
IHttpTransportFeature,
2932
IConnectionInherentKeepAliveFeature
3033
{
34+
private readonly object _stateLock = new object();
3135
private readonly object _itemsLock = new object();
3236
private readonly object _heartbeatLock = new object();
3337
private List<(Action<object> handler, object state)> _heartbeatHandlers;
3438
private readonly ILogger _logger;
3539
private PipeWriterStream _applicationStream;
3640
private IDuplexPipe _application;
3741
private IDictionary<object, object> _items;
38-
private int _status = (int)HttpConnectionStatus.Inactive;
3942

4043
// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
4144
// on the same task
@@ -83,7 +86,6 @@ public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe appli
8386
public HttpTransportType TransportType { get; set; }
8487

8588
public SemaphoreSlim WriteLock { get; } = new SemaphoreSlim(1, 1);
86-
public SemaphoreSlim StateLock { get; } = new SemaphoreSlim(1, 1);
8789

8890
// Used for testing only
8991
internal Task DisposeAndRemoveTask { get; set; }
@@ -96,7 +98,18 @@ public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe appli
9698

9799
public DateTime LastSeenUtc { get; set; }
98100

99-
public HttpConnectionStatus Status { get => (HttpConnectionStatus)_status; set => Interlocked.Exchange(ref _status, (int)value); }
101+
public DateTime? LastSeenUtcIfInactive
102+
{
103+
get
104+
{
105+
lock (_stateLock)
106+
{
107+
return Status == HttpConnectionStatus.Inactive ? (DateTime?)LastSeenUtc : null;
108+
}
109+
}
110+
}
111+
112+
public HttpConnectionStatus Status { get; set; } = HttpConnectionStatus.Inactive;
100113

101114
public override string ConnectionId { get; set; }
102115

@@ -184,29 +197,29 @@ public async Task DisposeAsync(bool closeGracefully = false)
184197
{
185198
Task disposeTask;
186199

187-
await StateLock.WaitAsync();
188200
try
189201
{
190-
if (Status == HttpConnectionStatus.Disposed)
191-
{
192-
disposeTask = _disposeTcs.Task;
193-
}
194-
else
202+
lock (_stateLock)
195203
{
196-
Status = HttpConnectionStatus.Disposed;
204+
if (Status == HttpConnectionStatus.Disposed)
205+
{
206+
disposeTask = _disposeTcs.Task;
207+
}
208+
else
209+
{
210+
Status = HttpConnectionStatus.Disposed;
197211

198-
Log.DisposingConnection(_logger, ConnectionId);
212+
Log.DisposingConnection(_logger, ConnectionId);
199213

200-
var applicationTask = ApplicationTask ?? Task.CompletedTask;
201-
var transportTask = TransportTask ?? Task.CompletedTask;
214+
var applicationTask = ApplicationTask ?? Task.CompletedTask;
215+
var transportTask = TransportTask ?? Task.CompletedTask;
202216

203-
disposeTask = WaitOnTasks(applicationTask, transportTask, closeGracefully);
217+
disposeTask = WaitOnTasks(applicationTask, transportTask, closeGracefully);
218+
}
204219
}
205220
}
206221
finally
207222
{
208-
StateLock.Release();
209-
210223
Cancellation?.Dispose();
211224

212225
Cancellation = null;
@@ -310,9 +323,145 @@ private async Task WaitOnTasks(Task applicationTask, Task transportTask, bool cl
310323
}
311324
}
312325

313-
public bool TryChangeState(HttpConnectionStatus from, HttpConnectionStatus to)
326+
public bool TryActivatePersistentConnection(
327+
ConnectionDelegate connectionDelegate,
328+
IHttpTransport transport,
329+
ILogger dispatcherLogger)
330+
{
331+
lock (_stateLock)
332+
{
333+
if (Status == HttpConnectionStatus.Inactive)
334+
{
335+
Status = HttpConnectionStatus.Active;
336+
337+
// Call into the end point passing the connection
338+
ApplicationTask = ExecuteApplication(connectionDelegate);
339+
340+
// Start the transport
341+
TransportTask = transport.ProcessRequestAsync(HttpContext, HttpContext.RequestAborted);
342+
343+
return true;
344+
}
345+
else
346+
{
347+
FailActivationUnsynchronized(HttpContext, dispatcherLogger);
348+
349+
return false;
350+
}
351+
}
352+
}
353+
354+
public bool TryActivateLongPollingConnection(
355+
ConnectionDelegate connectionDelegate,
356+
HttpContext nonClonedContext,
357+
TimeSpan pollTimeout,
358+
Task currentRequestTask,
359+
ILoggerFactory loggerFactory,
360+
ILogger dispatcherLogger)
314361
{
315-
return Interlocked.CompareExchange(ref _status, (int)to, (int)from) == (int)from;
362+
lock (_stateLock)
363+
{
364+
if (Status == HttpConnectionStatus.Inactive)
365+
{
366+
Status = HttpConnectionStatus.Active;
367+
368+
PreviousPollTask = currentRequestTask;
369+
370+
// Raise OnConnected for new connections only since polls happen all the time
371+
if (ApplicationTask == null)
372+
{
373+
HttpConnectionDispatcher.Log.EstablishedConnection(dispatcherLogger);
374+
375+
ApplicationTask = ExecuteApplication(connectionDelegate);
376+
377+
nonClonedContext.Response.ContentType = "application/octet-stream";
378+
379+
// This request has no content
380+
nonClonedContext.Response.ContentLength = 0;
381+
382+
// On the first poll, we flush the response immediately to mark the poll as "initialized" so future
383+
// requests can be made safely
384+
TransportTask = nonClonedContext.Response.Body.FlushAsync();
385+
}
386+
else
387+
{
388+
HttpConnectionDispatcher.Log.ResumingConnection(dispatcherLogger);
389+
390+
// REVIEW: Performance of this isn't great as this does a bunch of per request allocations
391+
Cancellation = new CancellationTokenSource();
392+
393+
var timeoutSource = new CancellationTokenSource();
394+
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(Cancellation.Token, nonClonedContext.RequestAborted, timeoutSource.Token);
395+
396+
// Dispose these tokens when the request is over
397+
nonClonedContext.Response.RegisterForDispose(timeoutSource);
398+
nonClonedContext.Response.RegisterForDispose(tokenSource);
399+
400+
var longPolling = new LongPollingTransport(timeoutSource.Token, Application.Input, loggerFactory);
401+
402+
// Start the transport
403+
TransportTask = longPolling.ProcessRequestAsync(nonClonedContext, tokenSource.Token);
404+
405+
// Start the timeout after we return from creating the transport task
406+
timeoutSource.CancelAfter(pollTimeout);
407+
}
408+
409+
return true;
410+
}
411+
else
412+
{
413+
FailActivationUnsynchronized(nonClonedContext, dispatcherLogger);
414+
415+
return false;
416+
}
417+
}
418+
}
419+
420+
private void FailActivationUnsynchronized(HttpContext nonClonedContext, ILogger dispatcherLogger)
421+
{
422+
if (Status == HttpConnectionStatus.Active)
423+
{
424+
HttpConnectionDispatcher.Log.ConnectionAlreadyActive(dispatcherLogger, ConnectionId, HttpContext.TraceIdentifier);
425+
426+
// Reject the request with a 409 conflict
427+
nonClonedContext.Response.StatusCode = StatusCodes.Status409Conflict;
428+
nonClonedContext.Response.ContentType = "text/plain";
429+
}
430+
else
431+
{
432+
Debug.Assert(Status == HttpConnectionStatus.Disposed);
433+
434+
HttpConnectionDispatcher.Log.ConnectionDisposed(dispatcherLogger, ConnectionId);
435+
436+
// Connection was disposed
437+
nonClonedContext.Response.StatusCode = StatusCodes.Status404NotFound;
438+
nonClonedContext.Response.ContentType = "text/plain";
439+
}
440+
}
441+
442+
public void MarkInactive()
443+
{
444+
lock (_stateLock)
445+
{
446+
if (Status == HttpConnectionStatus.Active)
447+
{
448+
Status = HttpConnectionStatus.Inactive;
449+
LastSeenUtc = DateTime.UtcNow;
450+
}
451+
}
452+
}
453+
454+
private async Task ExecuteApplication(ConnectionDelegate connectionDelegate)
455+
{
456+
// Verify some initialization invariants
457+
Debug.Assert(TransportType != HttpTransportType.None, "Transport has not been initialized yet");
458+
459+
// Jump onto the thread pool thread so blocking user code doesn't block the setup of the
460+
// connection and transport
461+
await AwaitableThreadPool.Yield();
462+
463+
// Running this in an async method turns sync exceptions into async ones
464+
await connectionDelegate(this);
316465
}
317466

318467
private static class Log

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
88
{
99
public partial class HttpConnectionDispatcher
1010
{
11-
private static class Log
11+
internal static class Log
1212
{
1313
private static readonly Action<ILogger, string, Exception> _connectionDisposed =
1414
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "ConnectionDisposed"), "Connection {TransportConnectionId} was disposed.");

0 commit comments

Comments
 (0)