Plumb a clock interface through SignalR for testing (#19311)
This commit is contained in:
parent
d1d9b97f77
commit
58db57be4c
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue