Plumb a clock interface through SignalR for testing (#19311)

This commit is contained in:
Brennan 2020-03-31 13:52:10 -07:00 committed by GitHub
parent d1d9b97f77
commit 58db57be4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 224 additions and 109 deletions

View File

@ -84,13 +84,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Features.Set<IConnectionInherentKeepAliveFeature>(this); Features.Set<IConnectionInherentKeepAliveFeature>(this);
} }
internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
: this(id, null, logger)
{
Transport = transport;
Application = application;
}
public CancellationTokenSource Cancellation { get; set; } public CancellationTokenSource Cancellation { get; set; }
public HttpTransportType TransportType { get; set; } public HttpTransportType TransportType { get; set; }

View File

@ -10,7 +10,6 @@ using System.IO.Pipelines;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
@ -31,24 +30,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private readonly TimerAwaitable _nextHeartbeat; private readonly TimerAwaitable _nextHeartbeat;
private readonly ILogger<HttpConnectionManager> _logger; private readonly ILogger<HttpConnectionManager> _logger;
private readonly ILogger<HttpConnectionContext> _connectionLogger; private readonly ILogger<HttpConnectionContext> _connectionLogger;
private readonly bool _useSendTimeout = true;
private readonly TimeSpan _disconnectTimeout; private readonly TimeSpan _disconnectTimeout;
public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime)
: this(loggerFactory, appLifetime, Options.Create(new ConnectionOptions() { DisconnectTimeout = ConnectionOptionsSetup.DefaultDisconectTimeout }))
{
}
public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime, IOptions<ConnectionOptions> connectionOptions) public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime, IOptions<ConnectionOptions> connectionOptions)
{ {
_logger = loggerFactory.CreateLogger<HttpConnectionManager>(); _logger = loggerFactory.CreateLogger<HttpConnectionManager>();
_connectionLogger = loggerFactory.CreateLogger<HttpConnectionContext>(); _connectionLogger = loggerFactory.CreateLogger<HttpConnectionContext>();
_nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate); _nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate);
_disconnectTimeout = connectionOptions.Value.DisconnectTimeout ?? ConnectionOptionsSetup.DefaultDisconectTimeout; _disconnectTimeout = connectionOptions.Value.DisconnectTimeout ?? ConnectionOptionsSetup.DefaultDisconectTimeout;
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Http.Connections.DoNotUseSendTimeout", out var timeoutDisabled))
{
_useSendTimeout = !timeoutDisabled;
}
// Register these last as the callbacks could run immediately // Register these last as the callbacks could run immediately
appLifetime.ApplicationStarted.Register(() => Start()); appLifetime.ApplicationStarted.Register(() => Start());
@ -176,7 +165,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
} }
else else
{ {
if (!Debugger.IsAttached && _useSendTimeout) if (!Debugger.IsAttached)
{ {
connection.TryCancelSend(utcNow.Ticks); connection.TryCancelSend(utcNow.Ticks);
} }

View File

@ -2347,10 +2347,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory) private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory)
{ {
return new HttpConnectionManager(loggerFactory ?? new LoggerFactory(), new EmptyApplicationLifetime()); return CreateConnectionManager(loggerFactory, null);
} }
private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan disconnectTimeout) private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan? disconnectTimeout)
{ {
var connectionOptions = new ConnectionOptions(); var connectionOptions = new ConnectionOptions();
connectionOptions.DisconnectTimeout = disconnectTimeout; connectionOptions.DisconnectTimeout = disconnectTimeout;

View File

@ -7,6 +7,7 @@ using System.IO.Pipelines;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Connections.Internal; using Microsoft.AspNetCore.Http.Connections.Internal;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Tests; using Microsoft.AspNetCore.SignalR.Tests;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -411,7 +412,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime = null) private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime = null)
{ {
lifetime = lifetime ?? new EmptyApplicationLifetime(); lifetime = lifetime ?? new EmptyApplicationLifetime();
return new HttpConnectionManager(loggerFactory, lifetime); return new HttpConnectionManager(loggerFactory, lifetime, Options.Create(new ConnectionOptions()));
} }
[Flags] [Flags]

View File

@ -31,11 +31,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
// Give the server socket to the transport and run it // Give the server socket to the transport and run it
@ -79,11 +83,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
connectionContext.ActiveFormat = transferFormat; connectionContext.ActiveFormat = transferFormat;
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
@ -116,7 +124,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1")); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -139,7 +151,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
} }
} }
var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2")); var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory); var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
// Give the server socket to the transport and run it // Give the server socket to the transport and run it
@ -169,7 +181,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -201,7 +217,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -236,7 +256,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -271,7 +295,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -311,7 +339,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {
@ -354,7 +386,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application); var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
{
Transport = pair.Transport,
Application = pair.Application,
};
using (var feature = new TestWebSocketConnectionFeature()) using (var feature = new TestWebSocketConnectionFeature())
{ {

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Internal
{
internal interface ISystemClock
{
/// <summary>
/// Retrieves the current UTC system time.
/// </summary>
DateTimeOffset UtcNow { get; }
/// <summary>
/// Retrieves ticks for the current UTC system time.
/// </summary>
long UtcNowTicks { get; }
/// <summary>
/// Retrieves the current UTC system time.
/// This is only safe to use from code called by the Heartbeat.
/// </summary>
DateTimeOffset UtcNowUnsynchronized { get; }
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Internal
{
/// <summary>
/// Provides access to the normal system clock.
/// </summary>
internal class SystemClock : ISystemClock
{
/// <summary>
/// Retrieves the current UTC system time.
/// </summary>
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
/// <summary>
/// Retrieves ticks for the current UTC system time.
/// </summary>
public long UtcNowTicks => DateTimeOffset.UtcNow.Ticks;
/// <summary>
/// Retrieves the current UTC system time.
/// </summary>
public DateTimeOffset UtcNowUnsynchronized => DateTimeOffset.UtcNow;
}
}

View File

@ -13,6 +13,7 @@ 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.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -34,13 +35,11 @@ namespace Microsoft.AspNetCore.SignalR
private readonly long _keepAliveInterval; private readonly long _keepAliveInterval;
private readonly long _clientTimeoutInterval; private readonly long _clientTimeoutInterval;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1); private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
private readonly bool _useAbsoluteClientTimeout;
private readonly object _receiveMessageTimeoutLock = new object(); private readonly object _receiveMessageTimeoutLock = new object();
private readonly ISystemClock _systemClock;
private StreamTracker _streamTracker; private StreamTracker _streamTracker;
private long _lastSendTimeStamp = DateTime.UtcNow.Ticks; private long _lastSendTimeStamp;
private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks;
private bool _receivedMessageThisInterval = false;
private ReadOnlyMemory<byte> _cachedPingMessage; private ReadOnlyMemory<byte> _cachedPingMessage;
private bool _clientTimeoutActive; private bool _clientTimeoutActive;
private volatile bool _connectionAborted; private volatile bool _connectionAborted;
@ -70,10 +69,8 @@ namespace Microsoft.AspNetCore.SignalR
HubCallerContext = new DefaultHubCallerContext(this); HubCallerContext = new DefaultHubCallerContext(this);
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.UseAbsoluteClientTimeout", out var useAbsoluteClientTimeout)) _systemClock = contextOptions.SystemClock ?? new SystemClock();
{ _lastSendTimeStamp = _systemClock.UtcNowTicks;
_useAbsoluteClientTimeout = useAbsoluteClientTimeout;
}
} }
internal StreamTracker StreamTracker internal StreamTracker StreamTracker
@ -558,7 +555,7 @@ namespace Microsoft.AspNetCore.SignalR
private void KeepAliveTick() private void KeepAliveTick()
{ {
var currentTime = DateTime.UtcNow.Ticks; var currentTime = _systemClock.UtcNowTicks;
// Implements the keep-alive tick behavior // Implements the keep-alive tick behavior
// Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks). // Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks).
@ -597,35 +594,17 @@ namespace Microsoft.AspNetCore.SignalR
return; return;
} }
if (_useAbsoluteClientTimeout) lock (_receiveMessageTimeoutLock)
{ {
// If it's been too long since we've heard from the client, then close this if (_receivedMessageTimeoutEnabled)
if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval)
{ {
if (!_receivedMessageThisInterval) _receivedMessageElapsedTicks = _systemClock.UtcNowTicks - _receivedMessageTimestamp;
if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
{ {
Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval)); Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval));
AbortAllowReconnect(); AbortAllowReconnect();
} }
_receivedMessageThisInterval = false;
Volatile.Write(ref _lastReceivedTimeStamp, DateTime.UtcNow.Ticks);
}
}
else
{
lock (_receiveMessageTimeoutLock)
{
if (_receivedMessageTimeoutEnabled)
{
_receivedMessageElapsedTicks = DateTime.UtcNow.Ticks - _receivedMessageTimestamp;
if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
{
Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval));
AbortAllowReconnect();
}
}
} }
} }
} }
@ -670,37 +649,24 @@ namespace Microsoft.AspNetCore.SignalR
} }
} }
internal void ResetClientTimeout()
{
_receivedMessageThisInterval = true;
}
internal void BeginClientTimeout() internal void BeginClientTimeout()
{ {
// check if new timeout behavior is in use lock (_receiveMessageTimeoutLock)
if (!_useAbsoluteClientTimeout)
{ {
lock (_receiveMessageTimeoutLock) _receivedMessageTimeoutEnabled = true;
{ _receivedMessageTimestamp = _systemClock.UtcNowTicks;
_receivedMessageTimeoutEnabled = true;
_receivedMessageTimestamp = DateTime.UtcNow.Ticks;
}
} }
} }
internal void StopClientTimeout() internal void StopClientTimeout()
{ {
// check if new timeout behavior is in use lock (_receiveMessageTimeoutLock)
if (!_useAbsoluteClientTimeout)
{ {
lock (_receiveMessageTimeoutLock) // we received a message so stop the timer and reset it
{ // it will resume after the message has been processed
// we received a message so stop the timer and reset it _receivedMessageElapsedTicks = 0;
// it will resume after the message has been processed _receivedMessageTimestamp = 0;
_receivedMessageElapsedTicks = 0; _receivedMessageTimeoutEnabled = false;
_receivedMessageTimestamp = 0;
_receivedMessageTimeoutEnabled = false;
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.SignalR namespace Microsoft.AspNetCore.SignalR
{ {
@ -29,5 +30,7 @@ namespace Microsoft.AspNetCore.SignalR
/// Gets or sets the maximum message size the client can send. /// Gets or sets the maximum message size the client can send.
/// </summary> /// </summary>
public long? MaximumReceiveMessageSize { get; set; } public long? MaximumReceiveMessageSize { get; set; }
internal ISystemClock SystemClock { get; set; }
} }
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -31,6 +32,9 @@ namespace Microsoft.AspNetCore.SignalR
private readonly bool _enableDetailedErrors; private readonly bool _enableDetailedErrors;
private readonly long? _maximumMessageSize; private readonly long? _maximumMessageSize;
// Internal for testing
internal ISystemClock SystemClock { get; set; } = new SystemClock();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HubConnectionHandler{THub}"/> class. /// Initializes a new instance of the <see cref="HubConnectionHandler{THub}"/> class.
/// </summary> /// </summary>
@ -98,6 +102,7 @@ namespace Microsoft.AspNetCore.SignalR
ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval, ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval,
StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity, StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity,
MaximumReceiveMessageSize = _maximumMessageSize, MaximumReceiveMessageSize = _maximumMessageSize,
SystemClock = SystemClock,
}; };
Log.ConnectedStarting(_logger); Log.ConnectedStarting(_logger);
@ -223,8 +228,6 @@ namespace Microsoft.AspNetCore.SignalR
var result = await input.ReadAsync(); var result = await input.ReadAsync();
var buffer = result.Buffer; var buffer = result.Buffer;
connection.ResetClientTimeout();
try try
{ {
if (result.IsCanceled) if (result.IsCanceled)

View File

@ -14,6 +14,8 @@
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" /> <Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" />
<Compile Include="$(SignalRSharedSourceRoot)AsyncEnumerableAdapters.cs" Link="Internal\AsyncEnumerableAdapters.cs" /> <Compile Include="$(SignalRSharedSourceRoot)AsyncEnumerableAdapters.cs" Link="Internal\AsyncEnumerableAdapters.cs" />
<Compile Include="$(SignalRSharedSourceRoot)TaskCache.cs" Link="Internal\TaskCache.cs" /> <Compile Include="$(SignalRSharedSourceRoot)TaskCache.cs" Link="Internal\TaskCache.cs" />
<Compile Include="$(SignalRSharedSourceRoot)ISystemClock.cs" Link="Internal\ISystemClock.cs" />
<Compile Include="$(SignalRSharedSourceRoot)SystemClock.cs" Link="Internal\SystemClock.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.SignalR.Tests
{
public class MockSystemClock : ISystemClock
{
private static Random _random = new Random();
private long _utcNowTicks;
public MockSystemClock()
{
// Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely.
// Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock.
_utcNowTicks = NextLong(DateTimeOffset.MinValue.Ticks, DateTimeOffset.MaxValue.Ticks - TimeSpan.FromDays(1).Ticks);
}
public DateTimeOffset UtcNow
{
get
{
UtcNowCalled++;
return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero);
}
set
{
Interlocked.Exchange(ref _utcNowTicks, value.Ticks);
}
}
public long UtcNowTicks => UtcNow.Ticks;
public DateTimeOffset UtcNowUnsynchronized => UtcNow;
public int UtcNowCalled { get; private set; }
private long NextLong(long minValue, long maxValue)
{
return (long)(_random.NextDouble() * (maxValue - minValue) + minValue);
}
}
}

View File

@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections.Features; using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
@ -2659,24 +2660,25 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var interval = 100;
var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure<HubOptions>(options => services.Configure<HubOptions>(options =>
options.KeepAliveInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); options.KeepAliveInterval = TimeSpan.FromMilliseconds(interval)), LoggerFactory);
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>(); var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{ {
var connectionHandlerTask = await client.ConnectAsync(connectionHandler); var connectionHandlerTask = await client.ConnectAsync(connectionHandler);
await client.Connected.OrTimeout(); await client.Connected.OrTimeout();
// Wait 500 ms, but make sure to yield some time up to unblock concurrent threads // Trigger multiple keep alives
// This is useful on AppVeyor because it's slow enough to end up with no time var heartbeatCount = 5;
// being available for the endpoint to run. for (var i = 0; i < heartbeatCount; i++)
for (var i = 0; i < 50; i += 1)
{ {
clock.UtcNow = clock.UtcNow.AddMilliseconds(interval + 1);
client.TickHeartbeat(); client.TickHeartbeat();
await Task.Yield();
await Task.Delay(10);
} }
// Shut down // Shut down
@ -2710,7 +2712,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
break; break;
} }
} }
Assert.InRange(pingCounter, 1, Int32.MaxValue); Assert.Equal(heartbeatCount, pingCounter);
} }
} }
} }
@ -2720,10 +2722,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var timeout = 100;
var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure<HubOptions>(options => services.Configure<HubOptions>(options =>
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>(); var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{ {
@ -2731,9 +2736,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests
await client.Connected.OrTimeout(); await client.Connected.OrTimeout();
// This is a fake client -- it doesn't auto-ping to signal // This is a fake client -- it doesn't auto-ping to signal
// We go over the 100 ms timeout interval... // We go over the 100 ms timeout interval multiple times
await Task.Delay(120); for (var i = 0; i < 3; i++)
client.TickHeartbeat(); {
clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1);
client.TickHeartbeat();
}
// Invoke a Hub method and wait for the result to reliably test if the connection is still active
var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout();
var result = await client.ReadAsync().OrTimeout();
// but client should still be open, since it never pinged to activate the timeout checking // but client should still be open, since it never pinged to activate the timeout checking
Assert.False(connectionHandlerTask.IsCompleted); Assert.False(connectionHandlerTask.IsCompleted);
@ -2746,10 +2758,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var timeout = 100;
var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure<HubOptions>(options => services.Configure<HubOptions>(options =>
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory); options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>(); var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{ {
@ -2757,10 +2772,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
await client.Connected.OrTimeout(); await client.Connected.OrTimeout();
await client.SendHubMessageAsync(PingMessage.Instance); await client.SendHubMessageAsync(PingMessage.Instance);
await Task.Delay(300); clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1);
client.TickHeartbeat();
await Task.Delay(300);
client.TickHeartbeat(); client.TickHeartbeat();
await connectionHandlerTask.OrTimeout(); await connectionHandlerTask.OrTimeout();
@ -2774,10 +2786,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{ {
using (StartVerifiableLog()) using (StartVerifiableLog())
{ {
var timeout = 300;
var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure<HubOptions>(options => services.Configure<HubOptions>(options =>
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(300)), LoggerFactory); options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>(); var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{ {
@ -2787,11 +2802,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
await Task.Delay(100); clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout - 1);
client.TickHeartbeat(); client.TickHeartbeat();
await client.SendHubMessageAsync(PingMessage.Instance); await client.SendHubMessageAsync(PingMessage.Instance);
} }
// Invoke a Hub method and wait for the result to reliably test if the connection is still active
var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout();
var result = await client.ReadAsync().OrTimeout();
Assert.IsType<CompletionMessage>(result);
Assert.False(connectionHandlerTask.IsCompleted); Assert.False(connectionHandlerTask.IsCompleted);
} }
} }