Skip to content

Commit e4f5fef

Browse files
Fix race in LongPolling causing flaky tests (#8114)
1 parent a95c4b6 commit e4f5fef

File tree

3 files changed

+14
-5
lines changed

3 files changed

+14
-5
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public HttpConnectionContext(string id, System.IO.Pipelines.IDuplexPipe transpor
9191
public System.DateTime LastSeenUtc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9292
public System.Threading.Tasks.Task PreviousPollTask { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
9393
public System.Threading.SemaphoreSlim StateLock { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
94-
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
94+
public Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus Status { get { throw null; } 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 { } }
@@ -102,6 +102,7 @@ public HttpConnectionContext(string id, System.IO.Pipelines.IDuplexPipe transpor
102102
public System.Threading.Tasks.Task DisposeAsync(bool closeGracefully = false) { throw null; }
103103
public void OnHeartbeat(System.Action<object> action, object state) { }
104104
public void TickHeartbeat() { }
105+
public bool TryChangeState(Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus from, Microsoft.AspNetCore.Http.Connections.Internal.HttpConnectionStatus to) { throw null; }
105106
}
106107
public partial class HttpConnectionDispatcher
107108
{

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class HttpConnectionContext : ConnectionContext,
3535
private PipeWriterStream _applicationStream;
3636
private IDuplexPipe _application;
3737
private IDictionary<object, object> _items;
38+
private int _status = (int)HttpConnectionStatus.Inactive;
3839

3940
// This tcs exists so that multiple calls to DisposeAsync all wait asynchronously
4041
// on the same task
@@ -95,7 +96,7 @@ public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe appli
9596

9697
public DateTime LastSeenUtc { get; set; }
9798

98-
public HttpConnectionStatus Status { get; set; } = HttpConnectionStatus.Inactive;
99+
public HttpConnectionStatus Status { get => (HttpConnectionStatus)_status; set => Interlocked.Exchange(ref _status, (int)value); }
99100

100101
public override string ConnectionId { get; set; }
101102

@@ -309,6 +310,11 @@ private async Task WaitOnTasks(Task applicationTask, Task transportTask, bool cl
309310
}
310311
}
311312

313+
public bool TryChangeState(HttpConnectionStatus from, HttpConnectionStatus to)
314+
{
315+
return Interlocked.CompareExchange(ref _status, (int)to, (int)from) == (int)from;
316+
}
317+
312318
private static class Log
313319
{
314320
private static readonly Action<ILogger, string, Exception> _disposingConnection =

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti
235235
}
236236

237237
// Mark the connection as active
238-
connection.Status = HttpConnectionStatus.Active;
238+
connection.TryChangeState(from: HttpConnectionStatus.Inactive, to: HttpConnectionStatus.Active);
239239

240240
// Raise OnConnected for new connections only since polls happen all the time
241241
if (connection.ApplicationTask == null)
@@ -331,7 +331,9 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti
331331
// Mark the connection as inactive
332332
connection.LastSeenUtc = DateTime.UtcNow;
333333

334-
connection.Status = HttpConnectionStatus.Inactive;
334+
// This is done outside a lock because the next poll might be waiting in the lock already and waiting for currentRequestTcs to complete
335+
// A DELETE request could have set the status to Disposed. If that is the case we don't want to change the state ever.
336+
connection.TryChangeState(from: HttpConnectionStatus.Active, to: HttpConnectionStatus.Inactive);
335337
}
336338
}
337339
finally
@@ -371,7 +373,7 @@ private async Task DoPersistentConnection(ConnectionDelegate connectionDelegate,
371373
}
372374

373375
// Mark the connection as active
374-
connection.Status = HttpConnectionStatus.Active;
376+
connection.TryChangeState(HttpConnectionStatus.Inactive, HttpConnectionStatus.Active);
375377

376378
// Call into the end point passing the connection
377379
connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection);

0 commit comments

Comments
 (0)