Synchronize the background scan and closing of connections

- It was possible for the application to be torn down during
a background scan. When that happened, the timer would be disposed
before the end of the scan and would throw an ObjectDisposedException
when timer resumption happened. This change introduces a lock that avoids that
race.

Fixes #161
This commit is contained in:
David Fowler 2017-01-26 07:50:29 +00:00
parent 162cd1fc06
commit 05cc7792df
1 changed files with 66 additions and 38 deletions

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Sockets
private readonly ConcurrentDictionary<string, ConnectionState> _connections = new ConcurrentDictionary<string, ConnectionState>(); private readonly ConcurrentDictionary<string, ConnectionState> _connections = new ConcurrentDictionary<string, ConnectionState>();
private Timer _timer; private Timer _timer;
private readonly ILogger<ConnectionManager> _logger; private readonly ILogger<ConnectionManager> _logger;
private object _executionLock = new object();
private bool _disposed;
public ConnectionManager(ILogger<ConnectionManager> logger) public ConnectionManager(ILogger<ConnectionManager> logger)
{ {
@ -25,9 +27,17 @@ namespace Microsoft.AspNetCore.Sockets
public void Start() public void Start()
{ {
if (_timer == null) lock (_executionLock)
{ {
_timer = new Timer(Scan, this, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); if (_disposed)
{
return;
}
if (_timer == null)
{
_timer = new Timer(Scan, this, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
} }
} }
@ -77,58 +87,76 @@ namespace Microsoft.AspNetCore.Sockets
private void Scan() private void Scan()
{ {
// Pause the timer while we're running lock (_executionLock)
_timer.Change(Timeout.Infinite, Timeout.Infinite);
try
{ {
// Scan the registered connections looking for ones that have timed out if (_disposed)
foreach (var c in _connections)
{ {
var status = ConnectionState.ConnectionStatus.Inactive; return;
var lastSeenUtc = DateTimeOffset.UtcNow; }
try // Pause the timer while we're running
_timer.Change(Timeout.Infinite, Timeout.Infinite);
try
{
// Scan the registered connections looking for ones that have timed out
foreach (var c in _connections)
{ {
c.Value.Lock.Wait(); var status = ConnectionState.ConnectionStatus.Inactive;
var lastSeenUtc = DateTimeOffset.UtcNow;
// Capture the connection state try
status = c.Value.Status; {
c.Value.Lock.Wait();
lastSeenUtc = c.Value.LastSeenUtc; // Capture the connection state
} status = c.Value.Status;
finally
{
c.Value.Lock.Release();
}
// Once the decision has been made to to dispose we don't check the status again lastSeenUtc = c.Value.LastSeenUtc;
if (status == ConnectionState.ConnectionStatus.Inactive && (DateTimeOffset.UtcNow - lastSeenUtc).TotalSeconds > 5) }
{ finally
var ignore = DisposeAndRemoveAsync(c.Value); {
c.Value.Lock.Release();
}
// Once the decision has been made to to dispose we don't check the status again
if (status == ConnectionState.ConnectionStatus.Inactive && (DateTimeOffset.UtcNow - lastSeenUtc).TotalSeconds > 5)
{
var ignore = DisposeAndRemoveAsync(c.Value);
}
} }
} }
} finally
finally {
{ // Resume once we finished processing all connections
// Resume once we finished processing all connections _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
_timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); }
} }
} }
public void CloseConnections() public void CloseConnections()
{ {
// Stop firing the timer lock (_executionLock)
_timer?.Dispose();
var tasks = new List<Task>();
foreach (var c in _connections)
{ {
tasks.Add(DisposeAndRemoveAsync(c.Value)); if (_disposed)
} {
return;
}
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(5)); _disposed = true;
// Stop firing the timer
_timer?.Dispose();
var tasks = new List<Task>();
foreach (var c in _connections)
{
tasks.Add(DisposeAndRemoveAsync(c.Value));
}
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(5));
}
} }
public async Task DisposeAndRemoveAsync(ConnectionState state) public async Task DisposeAndRemoveAsync(ConnectionState state)