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);
}
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 HttpTransportType TransportType { get; set; }

View File

@ -10,7 +10,6 @@ using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal;
@ -31,24 +30,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private readonly TimerAwaitable _nextHeartbeat;
private readonly ILogger<HttpConnectionManager> _logger;
private readonly ILogger<HttpConnectionContext> _connectionLogger;
private readonly bool _useSendTimeout = true;
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)
{
_logger = loggerFactory.CreateLogger<HttpConnectionManager>();
_connectionLogger = loggerFactory.CreateLogger<HttpConnectionContext>();
_nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate);
_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
appLifetime.ApplicationStarted.Register(() => Start());
@ -176,7 +165,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
else
{
if (!Debugger.IsAttached && _useSendTimeout)
if (!Debugger.IsAttached)
{
connection.TryCancelSend(utcNow.Ticks);
}

View File

@ -2347,10 +2347,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
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();
connectionOptions.DisconnectTimeout = disconnectTimeout;

View File

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

View File

@ -31,11 +31,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
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);
// Give the server socket to the transport and run it
@ -79,11 +83,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
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;
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
@ -116,7 +124,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -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);
// Give the server socket to the transport and run it
@ -169,7 +181,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -201,7 +217,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -236,7 +256,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -271,7 +295,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -311,7 +339,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{
@ -354,7 +386,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
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())
{

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.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging;
@ -34,13 +35,11 @@ namespace Microsoft.AspNetCore.SignalR
private readonly long _keepAliveInterval;
private readonly long _clientTimeoutInterval;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
private readonly bool _useAbsoluteClientTimeout;
private readonly object _receiveMessageTimeoutLock = new object();
private readonly ISystemClock _systemClock;
private StreamTracker _streamTracker;
private long _lastSendTimeStamp = DateTime.UtcNow.Ticks;
private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks;
private bool _receivedMessageThisInterval = false;
private long _lastSendTimeStamp;
private ReadOnlyMemory<byte> _cachedPingMessage;
private bool _clientTimeoutActive;
private volatile bool _connectionAborted;
@ -70,10 +69,8 @@ namespace Microsoft.AspNetCore.SignalR
HubCallerContext = new DefaultHubCallerContext(this);
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.UseAbsoluteClientTimeout", out var useAbsoluteClientTimeout))
{
_useAbsoluteClientTimeout = useAbsoluteClientTimeout;
}
_systemClock = contextOptions.SystemClock ?? new SystemClock();
_lastSendTimeStamp = _systemClock.UtcNowTicks;
}
internal StreamTracker StreamTracker
@ -558,7 +555,7 @@ namespace Microsoft.AspNetCore.SignalR
private void KeepAliveTick()
{
var currentTime = DateTime.UtcNow.Ticks;
var currentTime = _systemClock.UtcNowTicks;
// 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).
@ -597,35 +594,17 @@ namespace Microsoft.AspNetCore.SignalR
return;
}
if (_useAbsoluteClientTimeout)
lock (_receiveMessageTimeoutLock)
{
// If it's been too long since we've heard from the client, then close this
if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval)
if (_receivedMessageTimeoutEnabled)
{
if (!_receivedMessageThisInterval)
_receivedMessageElapsedTicks = _systemClock.UtcNowTicks - _receivedMessageTimestamp;
if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
{
Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval));
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()
{
// check if new timeout behavior is in use
if (!_useAbsoluteClientTimeout)
lock (_receiveMessageTimeoutLock)
{
lock (_receiveMessageTimeoutLock)
{
_receivedMessageTimeoutEnabled = true;
_receivedMessageTimestamp = DateTime.UtcNow.Ticks;
}
_receivedMessageTimeoutEnabled = true;
_receivedMessageTimestamp = _systemClock.UtcNowTicks;
}
}
internal void StopClientTimeout()
{
// check if new timeout behavior is in use
if (!_useAbsoluteClientTimeout)
lock (_receiveMessageTimeoutLock)
{
lock (_receiveMessageTimeoutLock)
{
// we received a message so stop the timer and reset it
// it will resume after the message has been processed
_receivedMessageElapsedTicks = 0;
_receivedMessageTimestamp = 0;
_receivedMessageTimeoutEnabled = false;
}
// we received a message so stop the timer and reset it
// it will resume after the message has been processed
_receivedMessageElapsedTicks = 0;
_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.
using System;
using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.SignalR
{
@ -29,5 +30,7 @@ namespace Microsoft.AspNetCore.SignalR
/// Gets or sets the maximum message size the client can send.
/// </summary>
public long? MaximumReceiveMessageSize { get; set; }
internal ISystemClock SystemClock { get; set; }
}
}

View File

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

View File

@ -14,6 +14,8 @@
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" />
<Compile Include="$(SignalRSharedSourceRoot)AsyncEnumerableAdapters.cs" Link="Internal\AsyncEnumerableAdapters.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>

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