diff --git a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index 2ccf688ca0..c8a2ebd559 100644 --- a/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Client.Internal; using Microsoft.AspNetCore.SignalR.Protocol; @@ -47,6 +48,7 @@ namespace Microsoft.AspNetCore.SignalR.Client private long _nextActivationServerTimeout; private long _nextActivationSendPing; private bool _disposed; + private bool _hasInherentKeepAlive; private readonly ConnectionLogScope _logScope; @@ -302,6 +304,7 @@ namespace Microsoft.AspNetCore.SignalR.Client // Start the connection var connection = await _connectionFactory.ConnectAsync(_protocol.TransferFormat); var startingConnectionState = new ConnectionState(connection, this); + _hasInherentKeepAlive = connection.Features.Get()?.HasInherentKeepAlive ?? false; // From here on, if an error occurs we need to shut down the connection because // we still own it. @@ -898,19 +901,25 @@ namespace Microsoft.AspNetCore.SignalR.Client // await returns True until `timer.Stop()` is called in the `finally` block of `ReceiveLoop` while (await timer) { - if (DateTime.UtcNow.Ticks > Volatile.Read(ref _nextActivationServerTimeout)) - { - OnServerTimeout(); - } - - if (DateTime.UtcNow.Ticks > Volatile.Read(ref _nextActivationSendPing)) - { - await PingServer(); - } + await RunTimerActions(); } } } + // Internal for testing + internal async Task RunTimerActions() + { + if (!_hasInherentKeepAlive && DateTime.UtcNow.Ticks > Volatile.Read(ref _nextActivationServerTimeout)) + { + OnServerTimeout(); + } + + if (DateTime.UtcNow.Ticks > Volatile.Read(ref _nextActivationSendPing)) + { + await PingServer(); + } + } + private void OnServerTimeout() { if (Debugger.IsAttached) diff --git a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index 4951413baf..7fcea1d199 100644 --- a/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.AspNetCore.SignalR.Tests; using Microsoft.Extensions.DependencyInjection; @@ -119,6 +120,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Assert.Equal($"Server timeout ({hubConnection.ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server.", exception.Message); } + [Fact] + public async Task ServerTimeoutIsDisabledWhenUsingTransportWithInherentKeepAlive() + { + using (StartVerifiableLog(out var loggerFactory)) + { + var testConnection = new TestConnection(); + testConnection.Features.Set(new TestKeepAliveFeature() { HasInherentKeepAlive = true }); + var hubConnection = CreateHubConnection(testConnection, loggerFactory: loggerFactory); + hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(1); + + await hubConnection.StartAsync().OrTimeout(); + + var closeTcs = new TaskCompletionSource(); + hubConnection.Closed += ex => + { + closeTcs.TrySetResult(ex); + return Task.CompletedTask; + }; + + await hubConnection.RunTimerActions().OrTimeout(); + + Assert.False(closeTcs.Task.IsCompleted); + + await hubConnection.DisposeAsync().OrTimeout(); + } + } + [Fact] public async Task PendingInvocationsAreTerminatedIfServerTimeoutIntervalElapsesWithNoMessages() { @@ -145,6 +173,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + private struct TestKeepAliveFeature : IConnectionInherentKeepAliveFeature + { + public bool HasInherentKeepAlive { get; set; } + } + // Moq really doesn't handle out parameters well, so to make these tests work I added a manual mock -anurse private class MockHubProtocol : IHubProtocol {