diff --git a/src/Websocket.Client/IWebsocketClient.cs b/src/Websocket.Client/IWebsocketClient.cs index a4b1cc9..770fa7a 100644 --- a/src/Websocket.Client/IWebsocketClient.cs +++ b/src/Websocket.Client/IWebsocketClient.cs @@ -116,6 +116,12 @@ public interface IWebsocketClient : IDisposable /// IObservable DisconnectionHappened { get; } + /// + /// Time range for how long to wait while connecting a new client. + /// Default: 2 seconds + /// + TimeSpan ConnectTimeout { get; set; } + /// /// Time range for how long to wait before reconnecting if no message comes from server. /// Set null to disable this feature. @@ -158,6 +164,21 @@ public interface IWebsocketClient : IDisposable /// bool IsRunning { get; } + /// + /// Returns whether text message sender is running. + /// + bool TextSenderRunning { get; } + + /// + /// Returns whether the binary message sender is running. + /// + bool BinarySenderRunning { get; } + + /// + /// Indicates whether any thread has entered the lock. + /// + bool IsInsideLock { get; } + /// /// Enable or disable text message conversion from binary to string (via 'MessageEncoding' property). /// Default: true diff --git a/src/Websocket.Client/Threading/WebsocketAsyncLock.cs b/src/Websocket.Client/Threading/WebsocketAsyncLock.cs index e443909..1cc92ca 100644 --- a/src/Websocket.Client/Threading/WebsocketAsyncLock.cs +++ b/src/Websocket.Client/Threading/WebsocketAsyncLock.cs @@ -31,6 +31,11 @@ public WebsocketAsyncLock() _releaserTask = Task.FromResult(_releaser); } + /// + /// True if the lock is currently taken + /// + public bool IsLocked => _semaphore.CurrentCount == 0; + /// /// Use inside 'using' block /// diff --git a/src/Websocket.Client/WebsocketClient.Reconnecting.cs b/src/Websocket.Client/WebsocketClient.Reconnecting.cs index 60baa90..27021dd 100644 --- a/src/Websocket.Client/WebsocketClient.Reconnecting.cs +++ b/src/Websocket.Client/WebsocketClient.Reconnecting.cs @@ -1,4 +1,5 @@ using System; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -67,7 +68,7 @@ private async Task Reconnect(ReconnectionType type, bool failFast, Exception? ca var disType = TranslateTypeToDisconnection(type); var disInfo = DisconnectionInfo.Create(disType, _client, causedException); - if (type != ReconnectionType.Error) + if (type != ReconnectionType.Error && _client?.State != WebSocketState.CloseReceived && _client?.State != WebSocketState.Closed) { _disconnectedSubject.OnNext(disInfo); if (disInfo.CancelReconnection) @@ -88,7 +89,7 @@ private async Task Reconnect(ReconnectionType type, bool failFast, Exception? ca } _client?.Dispose(); - if (!IsReconnectionEnabled || disInfo.CancelReconnection) + if (type != ReconnectionType.Error && (!IsReconnectionEnabled || disInfo.CancelReconnection)) { // reconnection disabled, do nothing IsStarted = false; diff --git a/src/Websocket.Client/WebsocketClient.Sending.cs b/src/Websocket.Client/WebsocketClient.Sending.cs index 4655b90..d90e2c4 100644 --- a/src/Websocket.Client/WebsocketClient.Sending.cs +++ b/src/Websocket.Client/WebsocketClient.Sending.cs @@ -24,6 +24,12 @@ public partial class WebsocketClient private static readonly byte[] _emptyArray = { }; + /// + public bool TextSenderRunning { get; private set; } + + /// + public bool BinarySenderRunning { get; private set; } + /// /// Send text message to the websocket channel. /// It inserts the message to the queue and actual sending is done on another thread @@ -159,6 +165,7 @@ public void StreamFakeMessage(ResponseMessage message) private async Task SendTextFromQueue() { + TextSenderRunning = true; try { while (await _messagesTextToSendQueue.Reader.WaitToReadAsync()) @@ -176,30 +183,35 @@ private async Task SendTextFromQueue() } } } - catch (TaskCanceledException) + catch (TaskCanceledException e) { // task was canceled, ignore + _logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message); } - catch (OperationCanceledException) + catch (OperationCanceledException e) { // operation was canceled, ignore + _logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message); } catch (Exception e) { if (_cancellationTotal?.IsCancellationRequested == true || _disposing) { // disposing/canceling, do nothing and exit + _logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message); + TextSenderRunning = false; return; } - _logger.LogTrace(L("Sending text thread failed, error: {error}. Creating a new sending thread."), Name, e.Message); + _logger.LogDebug(e, L("Sending text thread failed, error: {error}. Creating a new sending thread."), Name, e.Message); StartBackgroundThreadForSendingText(); } - + TextSenderRunning = false; } private async Task SendBinaryFromQueue() { + BinarySenderRunning = true; try { while (await _messagesBinaryToSendQueue.Reader.WaitToReadAsync()) @@ -217,26 +229,30 @@ private async Task SendBinaryFromQueue() } } } - catch (TaskCanceledException) + catch (TaskCanceledException e) { // task was canceled, ignore + _logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message); } - catch (OperationCanceledException) + catch (OperationCanceledException e) { // operation was canceled, ignore + _logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message); } catch (Exception e) { if (_cancellationTotal?.IsCancellationRequested == true || _disposing) { // disposing/canceling, do nothing and exit + _logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message); + BinarySenderRunning = false; return; } - _logger.LogTrace(L("Sending binary thread failed, error: {error}. Creating a new sending thread."), Name, e.Message); + _logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Creating a new sending thread."), Name, e.Message); StartBackgroundThreadForSendingBinary(); } - + BinarySenderRunning = false; } private void StartBackgroundThreadForSendingText() diff --git a/src/Websocket.Client/WebsocketClient.cs b/src/Websocket.Client/WebsocketClient.cs index d6da8d5..36e35a8 100644 --- a/src/Websocket.Client/WebsocketClient.cs +++ b/src/Websocket.Client/WebsocketClient.cs @@ -40,6 +40,7 @@ public partial class WebsocketClient : IWebsocketClient private bool _isReconnectionEnabled = true; private WebSocket? _client; private CancellationTokenSource? _cancellation; + private CancellationTokenSource? _cancellationConnection; private CancellationTokenSource? _cancellationTotal; private readonly Subject _messageReceivedSubject = new Subject(); @@ -123,6 +124,12 @@ public Uri Url /// public IObservable DisconnectionHappened => _disconnectedSubject.AsObservable(); + /// + /// Time range for how long to wait while connecting a new client. + /// Default: 2 seconds + /// + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(2); + /// /// Time range for how long to wait before reconnecting if no message comes from server. /// Set null to disable this feature. @@ -189,6 +196,9 @@ public bool IsReconnectionEnabled /// Default: true /// public bool IsTextMessageConversionEnabled { get; set; } = true; + + /// + public bool IsInsideLock => _locker.IsLocked; /// /// Enable or disable automatic of the @@ -210,19 +220,24 @@ public bool IsReconnectionEnabled /// public void Dispose() { + if (_disposing) + return; + _disposing = true; _logger.LogDebug(L("Disposing.."), Name); try { - _messagesTextToSendQueue.Writer.Complete(); - _messagesBinaryToSendQueue.Writer.Complete(); + _messagesTextToSendQueue.Writer.TryComplete(); + _messagesBinaryToSendQueue.Writer.TryComplete(); _lastChanceTimer?.Dispose(); _errorReconnectTimer?.Dispose(); _cancellation?.Cancel(); + _cancellationConnection?.Cancel(); _cancellationTotal?.Cancel(); _client?.Abort(); _client?.Dispose(); _cancellation?.Dispose(); + _cancellationConnection?.Dispose(); _cancellationTotal?.Dispose(); _messageReceivedSubject.OnCompleted(); _reconnectionSubject.OnCompleted(); @@ -402,7 +417,9 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp try { - _client = await _connectionFactory(uri, token).ConfigureAwait(false); + _cancellationConnection = CancellationTokenSource.CreateLinkedTokenSource(token); + _cancellationConnection.CancelAfter(ConnectTimeout); + _client = await _connectionFactory(uri, _cancellationConnection.Token).ConfigureAwait(false); _ = Listen(_client, token); IsRunning = true; IsStarted = true; @@ -412,6 +429,7 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp } catch (Exception e) { + IsRunning = _client?.State == WebSocketState.Open; var info = DisconnectionInfo.Create(DisconnectionType.Error, _client, e); _disconnectedSubject.OnNext(info); @@ -447,7 +465,9 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp private void ReconnectOnError(object? state) { - // await Task.Delay(timeout, token).ConfigureAwait(false); + if (_client != null && ShouldIgnoreReconnection(_client)) + return; + _ = Reconnect(ReconnectionType.Error, false, state as Exception).ConfigureAwait(false); } @@ -491,7 +511,7 @@ private async Task Listen(WebSocket client, CancellationToken token) } else if (result.MessageType == WebSocketMessageType.Close) { - _logger.LogTrace(L("Received close message"), Name); + _logger.LogDebug(L("Received close message"), Name); if (!IsStarted || _stopping) { @@ -512,13 +532,12 @@ private async Task Listen(WebSocket client, CancellationToken token) continue; } - await StopInternal(client, WebSocketCloseStatus.NormalClosure, "Closing", - token, false, true); + await StopInternal(client, WebSocketCloseStatus.NormalClosure, "Closing", token, false, true); // reconnect if enabled if (IsReconnectionEnabled && !ShouldIgnoreReconnection(client)) { - _ = ReconnectSynchronized(ReconnectionType.Lost, false, null); + _ = ReconnectSynchronized(ReconnectionType.ByServer, false, null); } return; diff --git a/test/Websocket.Client.Tests/ConnectionTests.cs b/test/Websocket.Client.Tests/ConnectionTests.cs index e771098..d54b486 100644 --- a/test/Websocket.Client.Tests/ConnectionTests.cs +++ b/test/Websocket.Client.Tests/ConnectionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net.WebSockets; using System.Reactive.Linq; using System.Threading; @@ -22,6 +23,20 @@ public ConnectionTests(ITestOutputHelper output) _context = new TestContext(_output); } + private static Task WaitUntil(Func func) + { + return WaitFor(func).WaitAsync(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); + + static async Task WaitFor(Func func) + { + var stop = func(); + while (!stop) + { + await Task.Delay(200); + stop = func(); + } + } + } [Fact] public async Task StartOrFail_ValidServer_ShouldWorkAsExpected() @@ -55,7 +70,7 @@ public async Task StartOrFail_ValidServer_ShouldWorkAsExpected() client.Send("ping"); client.Send("ping"); - await Task.Delay(2000); + await Task.Delay(200); Assert.Equal(0, disconnectionCount); Assert.Equal(DisconnectionType.Exit, disconnectionType); @@ -100,8 +115,6 @@ public async Task StartOrFail_InvalidServer_ShouldThrowException() causedException = e; } - await Task.Delay(2000); - Assert.Equal(1, disconnectionCount); Assert.Equal(DisconnectionType.Error, disconnectionInfo.Type); Assert.NotNull(disconnectionInfo.Exception); @@ -111,8 +124,6 @@ public async Task StartOrFail_InvalidServer_ShouldThrowException() Assert.Null(received); } - - [Fact] public async Task Starting_MultipleTimes_ShouldWorkWithNoExceptions() { @@ -128,10 +139,11 @@ public async Task Starting_MultipleTimes_ShouldWorkWithNoExceptions() foreach (var client in clients) { + await client.Start(); client.Send("ping"); } - await Task.Delay(2000); + await Task.Delay(200); foreach (var client in clients) { @@ -139,6 +151,35 @@ public async Task Starting_MultipleTimes_ShouldWorkWithNoExceptions() } } + [Fact] + public async Task Starting_WithServerDelay_RetriesAfterConnectionTimeout() + { + var attempt = 1; + + using var client = _context.CreateClient(() => attempt++ == 1 ? 300 : 0); + client.ConnectTimeout = TimeSpan.FromMilliseconds(200); + client.ErrorReconnectTimeout = TimeSpan.FromMilliseconds(200); + client.ReconnectTimeout = null; + client.IsReconnectionEnabled = false; + + var receivedCount = 0; + var receivedEvent = new ManualResetEvent(false); + + client.MessageReceived + .Where(x => x.MessageType == WebSocketMessageType.Text) + .Subscribe(msg => + { + receivedCount++; + receivedEvent.Set(); + }); + + await client.Start(); + + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); + + Assert.Equal(1, receivedCount); + } + [Fact] public async Task Stopping_ShouldWorkCorrectly() { @@ -146,7 +187,7 @@ public async Task Stopping_ShouldWorkCorrectly() using (var client = _context.CreateClient()) { - client.ReconnectTimeout = TimeSpan.FromSeconds(7); + client.ReconnectTimeout = null; string received = null; var receivedCount = 0; @@ -178,10 +219,8 @@ public async Task Stopping_ShouldWorkCorrectly() receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); - // check that reconnection is disabled - await Task.Delay(8000); Assert.Equal(1, receivedCount); Assert.Equal(1, disconnectionCount); Assert.Equal(DisconnectionType.ByUser, disconnectionInfo.Type); @@ -201,11 +240,10 @@ public async Task Stopping_ByServer_NoReconnection_ShouldWorkCorrectly() { using var client = _context.CreateClient(); client.IsReconnectionEnabled = false; - client.ReconnectTimeout = TimeSpan.FromSeconds(7); + client.ReconnectTimeout = null; string received = null; var receivedCount = 0; - var receivedEvent = new ManualResetEvent(false); var disconnectionCount = 0; DisconnectionInfo disconnectionInfo = null; @@ -230,15 +268,12 @@ public async Task Stopping_ByServer_NoReconnection_ShouldWorkCorrectly() { await Task.Delay(200); client.Send("close-me"); - receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + await WaitUntil(() => !client.IsStarted); - // check that reconnection is disabled - await Task.Delay(8000); Assert.Equal(1, receivedCount); - Assert.InRange(disconnectionCount, 1, 2); + Assert.Equal(1, disconnectionCount); Assert.Equal(DisconnectionType.ByServer, disconnectionInfo.Type); Assert.Equal(WebSocketCloseStatus.NormalClosure, disconnectionInfo.CloseStatus); Assert.Equal("normal closure", disconnectionInfo.CloseStatusDescription); @@ -251,11 +286,10 @@ public async Task Stopping_ByServer_WithReconnection_ShouldWorkCorrectly() { using var client = _context.CreateClient(); client.IsReconnectionEnabled = true; - client.ReconnectTimeout = TimeSpan.FromSeconds(30); + client.ReconnectTimeout = null; string received = null; var receivedCount = 0; - var receivedEvent = new ManualResetEvent(false); var disconnectionCount = 0; DisconnectionInfo disconnectionInfo = null; @@ -280,16 +314,13 @@ public async Task Stopping_ByServer_WithReconnection_ShouldWorkCorrectly() { await Task.Delay(200); client.Send("close-me"); - receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + await WaitUntil(() => receivedCount == 2); - // check that reconnection is disabled - await Task.Delay(8000); Assert.Equal(2, receivedCount); - Assert.InRange(disconnectionCount, 1, 2); - Assert.Equal(DisconnectionType.Lost, disconnectionInfo.Type); + Assert.Equal(1, disconnectionCount); + Assert.Equal(DisconnectionType.ByServer, disconnectionInfo.Type); Assert.Equal(WebSocketCloseStatus.NormalClosure, disconnectionInfo.CloseStatus); Assert.Equal("normal closure", disconnectionInfo.CloseStatusDescription); Assert.True(client.IsRunning); @@ -300,12 +331,11 @@ public async Task Stopping_ByServer_WithReconnection_ShouldWorkCorrectly() public async Task Stopping_ByServer_CancelNoReconnect_ShouldNotFinishClosing() { using var client = _context.CreateClient(); - client.ReconnectTimeout = TimeSpan.FromSeconds(7); + client.ReconnectTimeout = null; client.IsReconnectionEnabled = false; string received = null; var receivedCount = 0; - var receivedEvent = new ManualResetEvent(false); var disconnectionCount = 0; DisconnectionInfo disconnectionInfo = null; @@ -331,16 +361,13 @@ public async Task Stopping_ByServer_CancelNoReconnect_ShouldNotFinishClosing() { await Task.Delay(200); client.Send("close-me"); - receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + await WaitUntil(() => !client.IsStarted); - // check that reconnection is disabled - await Task.Delay(8000); Assert.Equal(1, receivedCount); - Assert.InRange(disconnectionCount, 1, 2); - Assert.Equal(DisconnectionType.Lost, disconnectionInfo.Type); + Assert.Equal(1, disconnectionCount); + Assert.Equal(DisconnectionType.ByServer, disconnectionInfo.Type); Assert.Equal(WebSocketCloseStatus.NormalClosure, disconnectionInfo.CloseStatus); Assert.Equal("normal closure", disconnectionInfo.CloseStatusDescription); Assert.False(client.IsRunning); @@ -351,12 +378,11 @@ public async Task Stopping_ByServer_CancelNoReconnect_ShouldNotFinishClosing() public async Task Stopping_ByServer_CancelWithReconnect_ShouldNotFinishClosing() { using var client = _context.CreateClient(); - client.ReconnectTimeout = TimeSpan.FromSeconds(30); + client.ReconnectTimeout = null; client.IsReconnectionEnabled = true; string received = null; var receivedCount = 0; - var receivedEvent = new ManualResetEvent(false); var disconnectionCount = 0; DisconnectionInfo disconnectionInfo = null; @@ -382,16 +408,13 @@ public async Task Stopping_ByServer_CancelWithReconnect_ShouldNotFinishClosing() { await Task.Delay(200); client.Send("close-me"); - receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + await WaitUntil(() => receivedCount == 2); - // check that reconnection is disabled - await Task.Delay(8000); Assert.Equal(2, receivedCount); - Assert.InRange(disconnectionCount, 1, 2); - Assert.Equal(DisconnectionType.Lost, disconnectionInfo.Type); + Assert.Equal(1, disconnectionCount); + Assert.Equal(DisconnectionType.ByServer, disconnectionInfo.Type); Assert.Equal(WebSocketCloseStatus.NormalClosure, disconnectionInfo.CloseStatus); Assert.Equal("normal closure", disconnectionInfo.CloseStatusDescription); Assert.True(client.IsRunning); @@ -406,7 +429,7 @@ public async Task Stopping_ByUser_NormalClosure_ShouldntTriggerReconnect(bool re using var client = _context.CreateClient(); // independently of this config, if it is a normal expected closure by User, it shouldn't reconnect client.IsReconnectionEnabled = reconnectionEnabled; - client.ReconnectTimeout = TimeSpan.FromSeconds(7); + client.ReconnectTimeout = TimeSpan.FromMilliseconds(200); var reconnectionCount = 0; var disconnectionCount = 0; @@ -434,7 +457,7 @@ public async Task Stopping_ByUser_NormalClosure_ShouldntTriggerReconnect(bool re await client.Stop(WebSocketCloseStatus.NormalClosure, "Expected Closure"); // give some time to receive disconnection and reconnection messages - await Task.Delay(8000); + await Task.Delay(300); Assert.Equal(1, disconnectionCount); Assert.Equal(0, reconnectionCount); @@ -510,7 +533,7 @@ public async Task Dispose_ShouldWorkCorrectly() public async Task Stopping_InvalidServer_ShouldStopReconnection() { using var client = _context.CreateInvalidClient(new Uri("wss://google.com")); - client.ErrorReconnectTimeout = TimeSpan.FromSeconds(4); + client.ErrorReconnectTimeout = TimeSpan.FromMilliseconds(400); string received = null; var receivedCount = 0; @@ -533,12 +556,12 @@ public async Task Stopping_InvalidServer_ShouldStopReconnection() }); _ = client.Start(); - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromMilliseconds(100)); Assert.True(client.IsStarted, "IsStarted should be true"); await client.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); - await Task.Delay(TimeSpan.FromSeconds(6)); + await Task.Delay(TimeSpan.FromMilliseconds(600)); Assert.False(client.IsRunning, "IsRunning is true and shouldn't"); Assert.False(client.IsStarted, "IsStarted is true and shouldn't"); @@ -554,7 +577,8 @@ public async Task Stopping_InvalidServer_ShouldStopReconnection() public async Task Stopping_AfterChangingToInvalidServer_ShouldStopReconnection() { using var client = _context.CreateClient(); - client.ErrorReconnectTimeout = TimeSpan.FromSeconds(4); + client.ReconnectTimeout = null; + client.ErrorReconnectTimeout = TimeSpan.FromMilliseconds(200); string received = null; var receivedCount = 0; @@ -577,20 +601,20 @@ public async Task Stopping_AfterChangingToInvalidServer_ShouldStopReconnection() }); _ = client.Start(); - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromMilliseconds(100)); Assert.True(client.IsStarted, "IsStarted should be true"); await client.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); - await Task.Delay(TimeSpan.FromSeconds(6)); + await Task.Delay(TimeSpan.FromMilliseconds(200)); Assert.False(client.IsRunning, "IsRunning should be false"); client.Url = _context.InvalidUri; _ = client.Start(); - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromMilliseconds(100)); Assert.True(client.IsStarted, "IsStarted should be true"); await client.Stop(WebSocketCloseStatus.NormalClosure, string.Empty); - await Task.Delay(TimeSpan.FromSeconds(6)); + await Task.Delay(TimeSpan.FromMilliseconds(200)); Assert.False(client.IsRunning, "IsRunning is true and shouldn't"); Assert.False(client.IsStarted, "IsStarted is true and shouldn't"); diff --git a/test/Websocket.Client.Tests/ReconnectionTests.cs b/test/Websocket.Client.Tests/ReconnectionTests.cs index 0d5501f..02d2120 100644 --- a/test/Websocket.Client.Tests/ReconnectionTests.cs +++ b/test/Websocket.Client.Tests/ReconnectionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Net.WebSockets; using System.Reactive.Linq; using System.Threading; @@ -29,7 +30,7 @@ public async Task Reconnecting_ShouldWorkAsExpected() var receivedEvent = new ManualResetEvent(false); client.IsReconnectionEnabled = true; - client.ReconnectTimeout = TimeSpan.FromSeconds(3); + client.ReconnectTimeout = TimeSpan.FromMilliseconds(50); client.MessageReceived .Where(x => x.MessageType == WebSocketMessageType.Text) @@ -40,7 +41,7 @@ public async Task Reconnecting_ShouldWorkAsExpected() }); await client.Start(); - await Task.Delay(12000); + await Task.Delay(2200); _output.WriteLine($"Reconnected {receivedCount} times"); Assert.InRange(receivedCount, 2, 7); @@ -67,13 +68,13 @@ public async Task DisabledReconnecting_ShouldWorkAsExpected() }); await client.Start(); - await Task.Delay(3000); + await Task.Delay(300); await client.Stop(WebSocketCloseStatus.InternalServerError, "something strange happened"); - await Task.Delay(3000); + await Task.Delay(300); await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); @@ -101,10 +102,10 @@ public async Task DisabledReconnecting_OnlyNoMessage_ShouldWorkAsExpected() }); await client.Start(); - await Task.Delay(3000); + await Task.Delay(300); await client.Reconnect(); - await Task.Delay(3000); + await Task.Delay(300); receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); @@ -118,7 +119,7 @@ public async Task DisabledReconnecting_ShouldWorkAtRuntime() var receivedCount = 0; client.IsReconnectionEnabled = true; - client.ReconnectTimeout = TimeSpan.FromSeconds(1); + client.ReconnectTimeout = TimeSpan.FromMilliseconds(200); client.MessageReceived.Subscribe(msg => { @@ -129,7 +130,7 @@ public async Task DisabledReconnecting_ShouldWorkAtRuntime() }); await client.Start(); - await Task.Delay(7000); + await Task.Delay(1200); Assert.Equal(2, receivedCount); } @@ -162,16 +163,16 @@ public async Task ManualReconnect_ShouldWorkAsExpected() await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(2000); + await Task.Delay(200); _output.WriteLine($"Received message {receivedCount} times and reconnected {receivedCount} times, " + $"last: {lastReconnectionType}"); @@ -208,24 +209,24 @@ public async Task ManualReconnect_Advanced_ShouldWorkAsExpected() await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); _ = client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); await client.ReconnectOrFail(); await client.ReconnectOrFail(); - await Task.Delay(1000); + await Task.Delay(100); await client.ReconnectOrFail(); await client.Reconnect(); await client.ReconnectOrFail(); - await Task.Delay(8000); + await Task.Delay(200); _output.WriteLine($"Received message {receivedCount} times and reconnected {receivedCount} times, " + $"last: {lastReconnectionType}"); @@ -271,7 +272,7 @@ public async Task ManualReconnectOrFail_ShouldThrowException() await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); client.Url = new Uri("wss://google.com"); @@ -286,7 +287,7 @@ public async Task ManualReconnectOrFail_ShouldThrowException() causedException = e; } - await Task.Delay(1000); + await Task.Delay(100); Assert.Equal(2, disconnectionCount); Assert.Equal(DisconnectionType.Error, disconnectionInfo.Type); @@ -298,6 +299,38 @@ public async Task ManualReconnectOrFail_ShouldThrowException() Assert.Equal(ReconnectionType.Initial, lastReconnectionType); } + [Fact] + public async Task Reconnecting_WithServerDelay_RetriesAfterConnectionTimeout() + { + var attempt = 1; + + using var client = _context.CreateClient(() => attempt++ == 2 ? 300 : 0); + client.ConnectTimeout = TimeSpan.FromMilliseconds(200); + client.ErrorReconnectTimeout = TimeSpan.FromMilliseconds(200); + client.ReconnectTimeout = null; + + var receivedCount = 0; + var receivedEvent = new ManualResetEvent(false); + + client.MessageReceived + .Where(x => x.MessageType == WebSocketMessageType.Text) + .Subscribe(msg => + { + receivedCount++; + receivedEvent.Set(); + }); + + await client.Start(); + + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); + + receivedEvent.Reset(); + await client.Reconnect(); + + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); + + Assert.Equal(2, receivedCount); + } [Fact] public async Task CancelingReconnection_ViaDisconnectionStream_ShouldWork() @@ -338,16 +371,16 @@ public async Task CancelingReconnection_ViaDisconnectionStream_ShouldWork() await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); await client.Reconnect(); - await Task.Delay(1000); + await Task.Delay(100); Assert.Equal(2, disconnectionCount); Assert.Equal(DisconnectionType.ByUser, disconnectionInfo.Type); diff --git a/test/Websocket.Client.Tests/SendingTests.cs b/test/Websocket.Client.Tests/SendingTests.cs index 8c34119..d093190 100644 --- a/test/Websocket.Client.Tests/SendingTests.cs +++ b/test/Websocket.Client.Tests/SendingTests.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics; using System.Net.WebSockets; using System.Reactive.Linq; using System.Threading; @@ -158,17 +159,17 @@ public async Task SendMessageAfterDispose_ShouldDoNothing() client.Send("ping"); client.Send("ping"); - await Task.Delay(1000); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + await Task.Delay(100); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); client.Dispose(); - await Task.Delay(2000); + await Task.Delay(200); client.Send("ping"); await client.SendInstant("ping"); - await Task.Delay(1000); + await Task.Delay(100); Assert.NotNull(received); Assert.Equal(3, receivedCount); diff --git a/test/Websocket.Client.Tests/Testserver/SimpleStartup.cs b/test/Websocket.Client.Tests/Testserver/SimpleStartup.cs index 66b9050..742cda9 100644 --- a/test/Websocket.Client.Tests/Testserver/SimpleStartup.cs +++ b/test/Websocket.Client.Tests/Testserver/SimpleStartup.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -27,6 +28,9 @@ public void Configure(IApplicationBuilder app) { if (context.WebSockets.IsWebSocketRequest) { + if (context.Request.Query.TryGetValue("delay", out var delayValues) && int.TryParse(delayValues, out var delayMilliseconds)) + await Task.Delay(delayMilliseconds); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); await SendResponse(webSocket, ResponseMessage.TextMessage($"Hello, you are connected to '{nameof(SimpleStartup)}'")); diff --git a/test/Websocket.Client.Tests/Testserver/TestContext.cs b/test/Websocket.Client.Tests/Testserver/TestContext.cs index e689809..176c0b3 100644 --- a/test/Websocket.Client.Tests/Testserver/TestContext.cs +++ b/test/Websocket.Client.Tests/Testserver/TestContext.cs @@ -25,13 +25,13 @@ public TestContext(ITestOutputHelper output) public Uri InvalidUri { get; } = new("wss://invalid-url.local"); - public IWebsocketClient CreateClient() + public IWebsocketClient CreateClient(Func getDelayMilliseconds = null) { var httpClient = _factory.CreateClient(); // This is needed since _factory.Server would otherwise be null - return CreateClient(_factory.Server.BaseAddress); + return CreateClient(_factory.Server.BaseAddress, getDelayMilliseconds ?? (() => 0)); } - public IWebsocketClient CreateClient(Uri serverUrl) + private IWebsocketClient CreateClient(Uri serverUrl, Func getDelayMilliseconds) { var wsUri = new UriBuilder(serverUrl) { @@ -51,9 +51,15 @@ public IWebsocketClient CreateClient(Uri serverUrl) throw new InvalidOperationException("Connection to websocket server failed, check url"); } + var delayMilliseconds = getDelayMilliseconds(); + if (delayMilliseconds > 0) + uri = new UriBuilder(uri) + { + Query = $"delay={delayMilliseconds}" + }.Uri; + NativeTestClient = _factory.Server.CreateWebSocketClient(); var ws = await NativeTestClient.ConnectAsync(uri, token).ConfigureAwait(false); - //await Task.Delay(1000, token); return ws; }); } diff --git a/test_integration/Websocket.Client.Tests.Integration/WebsocketClientTests.cs b/test_integration/Websocket.Client.Tests.Integration/WebsocketClientTests.cs index e84bb69..fbd9bab 100644 --- a/test_integration/Websocket.Client.Tests.Integration/WebsocketClientTests.cs +++ b/test_integration/Websocket.Client.Tests.Integration/WebsocketClientTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Net.WebSockets; using System.Reactive.Linq; using System.Threading; @@ -36,7 +37,7 @@ public async Task OnStarting_ShouldGetInfoResponse() await client.Start(); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); Assert.NotNull(received); } @@ -72,7 +73,7 @@ public async Task SendMessageBeforeStart_ShouldWorkAfterStart() client.Send("ping"); client.Send("ping"); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); Assert.NotNull(received); } @@ -106,15 +107,15 @@ public async Task DisabledReconnecting_ShouldWorkAsExpected() }); await client.Start(); - await Task.Delay(5000); + await Task.Delay(500); await client.Stop(WebSocketCloseStatus.Empty, string.Empty); - await Task.Delay(5000); + await Task.Delay(500); await client.Start(); - await Task.Delay(1000); + await Task.Delay(100); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); Assert.Equal(2, receivedCount); } @@ -126,7 +127,7 @@ public async Task DisabledReconnecting_ShouldWorkAtRuntime() var receivedCount = 0; client.IsReconnectionEnabled = true; - client.ReconnectTimeout = TimeSpan.FromSeconds(5); + client.ReconnectTimeout = TimeSpan.FromMilliseconds(500); client.MessageReceived.Subscribe(msg => { @@ -136,7 +137,7 @@ public async Task DisabledReconnecting_ShouldWorkAtRuntime() }); await client.Start(); - await Task.Delay(17000); + await Task.Delay(3000); Assert.Equal(2, receivedCount); } @@ -145,7 +146,7 @@ public async Task DisabledReconnecting_ShouldWorkAtRuntime() public async Task OnClose_ShouldWorkCorrectly() { using IWebsocketClient client = new WebsocketClient(_websocketUrl); - client.ReconnectTimeout = TimeSpan.FromSeconds(5); + client.ReconnectTimeout = TimeSpan.FromMilliseconds(500); string received = null; var receivedCount = 0; @@ -169,13 +170,13 @@ public async Task OnClose_ShouldWorkCorrectly() _ = Task.Run(async () => { - await Task.Delay(2000); + await Task.Delay(200); var success = await client.Stop(WebSocketCloseStatus.InternalServerError, "server error 500"); Assert.True(success); receivedEvent.Set(); }); - receivedEvent.WaitOne(TimeSpan.FromSeconds(30)); + receivedEvent.WaitOne(TimeSpan.FromSeconds(Debugger.IsAttached ? 30 : 3)); Assert.NotNull(received); Assert.Equal(1, receivedCount); @@ -191,7 +192,7 @@ public async Task OnClose_ShouldWorkCorrectly() Assert.Equal("server error 500", nativeClient.CloseStatusDescription); // check that reconnection is disabled - await Task.Delay(7000); + await Task.Delay(2000); Assert.Equal(1, receivedCount); }