Skip to content

Commit c1ec006

Browse files
Fix bugs in Reconnect logic (#157)
* Improve logging and diagnostics capabilities * Expose new properties on IWebsocketClient (TextSenderRunning, BinarySenderRunning, IsInsideLock) * Use Debug log level when receiving a close message and creating new send threads * Write a Debug log message when the sending threads are shutting down * Shorten test run times and make tests more debuggable * Fix bugs in Reconnect logic which resulted in duplicate disconnection events * Use ReconnectionType.ByServer when reconnecting in response to receiving a close message from the server * Actually start the client a 2nd time in the test for that * Implement a timeout for creating a new websocket connection * Fixes an issue where the connection factory occasionally just hangs forever * Expose a new property for configuring how long to wait before timing out (defaulted to 2 seconds) * Ignore duplicate calls to Dispose * Use try methods for closing channel writers in Dispose * Update IsRunning flag immediately within exception handler in StartClient * Skip reconnecting on error after waiting if the client is already reconnecting by user request
1 parent 1644e48 commit c1ec006

11 files changed

Lines changed: 244 additions & 113 deletions

File tree

src/Websocket.Client/IWebsocketClient.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ public interface IWebsocketClient : IDisposable
116116
/// </summary>
117117
IObservable<DisconnectionInfo> DisconnectionHappened { get; }
118118

119+
/// <summary>
120+
/// Time range for how long to wait while connecting a new client.
121+
/// Default: 2 seconds
122+
/// </summary>
123+
TimeSpan ConnectTimeout { get; set; }
124+
119125
/// <summary>
120126
/// Time range for how long to wait before reconnecting if no message comes from server.
121127
/// Set null to disable this feature.
@@ -158,6 +164,21 @@ public interface IWebsocketClient : IDisposable
158164
/// </summary>
159165
bool IsRunning { get; }
160166

167+
/// <summary>
168+
/// Returns whether text message sender is running.
169+
/// </summary>
170+
bool TextSenderRunning { get; }
171+
172+
/// <summary>
173+
/// Returns whether the binary message sender is running.
174+
/// </summary>
175+
bool BinarySenderRunning { get; }
176+
177+
/// <summary>
178+
/// Indicates whether any thread has entered the lock.
179+
/// </summary>
180+
bool IsInsideLock { get; }
181+
161182
/// <summary>
162183
/// Enable or disable text message conversion from binary to string (via 'MessageEncoding' property).
163184
/// Default: true

src/Websocket.Client/Threading/WebsocketAsyncLock.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public WebsocketAsyncLock()
3131
_releaserTask = Task.FromResult(_releaser);
3232
}
3333

34+
/// <summary>
35+
/// True if the lock is currently taken
36+
/// </summary>
37+
public bool IsLocked => _semaphore.CurrentCount == 0;
38+
3439
/// <summary>
3540
/// Use inside 'using' block
3641
/// </summary>

src/Websocket.Client/WebsocketClient.Reconnecting.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Net.WebSockets;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Microsoft.Extensions.Logging;
@@ -67,7 +68,7 @@ private async Task Reconnect(ReconnectionType type, bool failFast, Exception? ca
6768

6869
var disType = TranslateTypeToDisconnection(type);
6970
var disInfo = DisconnectionInfo.Create(disType, _client, causedException);
70-
if (type != ReconnectionType.Error)
71+
if (type != ReconnectionType.Error && _client?.State != WebSocketState.CloseReceived && _client?.State != WebSocketState.Closed)
7172
{
7273
_disconnectedSubject.OnNext(disInfo);
7374
if (disInfo.CancelReconnection)
@@ -88,7 +89,7 @@ private async Task Reconnect(ReconnectionType type, bool failFast, Exception? ca
8889
}
8990
_client?.Dispose();
9091

91-
if (!IsReconnectionEnabled || disInfo.CancelReconnection)
92+
if (type != ReconnectionType.Error && (!IsReconnectionEnabled || disInfo.CancelReconnection))
9293
{
9394
// reconnection disabled, do nothing
9495
IsStarted = false;

src/Websocket.Client/WebsocketClient.Sending.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public partial class WebsocketClient
2424

2525
private static readonly byte[] _emptyArray = { };
2626

27+
/// <inheritdoc />
28+
public bool TextSenderRunning { get; private set; }
29+
30+
/// <inheritdoc />
31+
public bool BinarySenderRunning { get; private set; }
32+
2733
/// <summary>
2834
/// Send text message to the websocket channel.
2935
/// It inserts the message to the queue and actual sending is done on another thread
@@ -159,6 +165,7 @@ public void StreamFakeMessage(ResponseMessage message)
159165

160166
private async Task SendTextFromQueue()
161167
{
168+
TextSenderRunning = true;
162169
try
163170
{
164171
while (await _messagesTextToSendQueue.Reader.WaitToReadAsync())
@@ -176,30 +183,35 @@ private async Task SendTextFromQueue()
176183
}
177184
}
178185
}
179-
catch (TaskCanceledException)
186+
catch (TaskCanceledException e)
180187
{
181188
// task was canceled, ignore
189+
_logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message);
182190
}
183-
catch (OperationCanceledException)
191+
catch (OperationCanceledException e)
184192
{
185193
// operation was canceled, ignore
194+
_logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message);
186195
}
187196
catch (Exception e)
188197
{
189198
if (_cancellationTotal?.IsCancellationRequested == true || _disposing)
190199
{
191200
// disposing/canceling, do nothing and exit
201+
_logger.LogDebug(e, L("Sending text thread failed, error: {error}. Shutting down."), Name, e.Message);
202+
TextSenderRunning = false;
192203
return;
193204
}
194205

195-
_logger.LogTrace(L("Sending text thread failed, error: {error}. Creating a new sending thread."), Name, e.Message);
206+
_logger.LogDebug(e, L("Sending text thread failed, error: {error}. Creating a new sending thread."), Name, e.Message);
196207
StartBackgroundThreadForSendingText();
197208
}
198-
209+
TextSenderRunning = false;
199210
}
200211

201212
private async Task SendBinaryFromQueue()
202213
{
214+
BinarySenderRunning = true;
203215
try
204216
{
205217
while (await _messagesBinaryToSendQueue.Reader.WaitToReadAsync())
@@ -217,26 +229,30 @@ private async Task SendBinaryFromQueue()
217229
}
218230
}
219231
}
220-
catch (TaskCanceledException)
232+
catch (TaskCanceledException e)
221233
{
222234
// task was canceled, ignore
235+
_logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message);
223236
}
224-
catch (OperationCanceledException)
237+
catch (OperationCanceledException e)
225238
{
226239
// operation was canceled, ignore
240+
_logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message);
227241
}
228242
catch (Exception e)
229243
{
230244
if (_cancellationTotal?.IsCancellationRequested == true || _disposing)
231245
{
232246
// disposing/canceling, do nothing and exit
247+
_logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Shutting down."), Name, e.Message);
248+
BinarySenderRunning = false;
233249
return;
234250
}
235251

236-
_logger.LogTrace(L("Sending binary thread failed, error: {error}. Creating a new sending thread."), Name, e.Message);
252+
_logger.LogDebug(e, L("Sending binary thread failed, error: {error}. Creating a new sending thread."), Name, e.Message);
237253
StartBackgroundThreadForSendingBinary();
238254
}
239-
255+
BinarySenderRunning = false;
240256
}
241257

242258
private void StartBackgroundThreadForSendingText()

src/Websocket.Client/WebsocketClient.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public partial class WebsocketClient : IWebsocketClient
4040
private bool _isReconnectionEnabled = true;
4141
private WebSocket? _client;
4242
private CancellationTokenSource? _cancellation;
43+
private CancellationTokenSource? _cancellationConnection;
4344
private CancellationTokenSource? _cancellationTotal;
4445

4546
private readonly Subject<ResponseMessage> _messageReceivedSubject = new Subject<ResponseMessage>();
@@ -123,6 +124,12 @@ public Uri Url
123124
/// </summary>
124125
public IObservable<DisconnectionInfo> DisconnectionHappened => _disconnectedSubject.AsObservable();
125126

127+
/// <summary>
128+
/// Time range for how long to wait while connecting a new client.
129+
/// Default: 2 seconds
130+
/// </summary>
131+
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(2);
132+
126133
/// <summary>
127134
/// Time range for how long to wait before reconnecting if no message comes from server.
128135
/// Set null to disable this feature.
@@ -189,6 +196,9 @@ public bool IsReconnectionEnabled
189196
/// Default: true
190197
/// </summary>
191198
public bool IsTextMessageConversionEnabled { get; set; } = true;
199+
200+
/// <inheritdoc />
201+
public bool IsInsideLock => _locker.IsLocked;
192202

193203
/// <summary>
194204
/// Enable or disable automatic <see cref="MemoryStream.Dispose(bool)"/> of the <see cref="MemoryStream"/>
@@ -210,19 +220,24 @@ public bool IsReconnectionEnabled
210220
/// </summary>
211221
public void Dispose()
212222
{
223+
if (_disposing)
224+
return;
225+
213226
_disposing = true;
214227
_logger.LogDebug(L("Disposing.."), Name);
215228
try
216229
{
217-
_messagesTextToSendQueue.Writer.Complete();
218-
_messagesBinaryToSendQueue.Writer.Complete();
230+
_messagesTextToSendQueue.Writer.TryComplete();
231+
_messagesBinaryToSendQueue.Writer.TryComplete();
219232
_lastChanceTimer?.Dispose();
220233
_errorReconnectTimer?.Dispose();
221234
_cancellation?.Cancel();
235+
_cancellationConnection?.Cancel();
222236
_cancellationTotal?.Cancel();
223237
_client?.Abort();
224238
_client?.Dispose();
225239
_cancellation?.Dispose();
240+
_cancellationConnection?.Dispose();
226241
_cancellationTotal?.Dispose();
227242
_messageReceivedSubject.OnCompleted();
228243
_reconnectionSubject.OnCompleted();
@@ -402,7 +417,9 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp
402417

403418
try
404419
{
405-
_client = await _connectionFactory(uri, token).ConfigureAwait(false);
420+
_cancellationConnection = CancellationTokenSource.CreateLinkedTokenSource(token);
421+
_cancellationConnection.CancelAfter(ConnectTimeout);
422+
_client = await _connectionFactory(uri, _cancellationConnection.Token).ConfigureAwait(false);
406423
_ = Listen(_client, token);
407424
IsRunning = true;
408425
IsStarted = true;
@@ -412,6 +429,7 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp
412429
}
413430
catch (Exception e)
414431
{
432+
IsRunning = _client?.State == WebSocketState.Open;
415433
var info = DisconnectionInfo.Create(DisconnectionType.Error, _client, e);
416434
_disconnectedSubject.OnNext(info);
417435

@@ -447,7 +465,9 @@ private async Task StartClient(Uri uri, CancellationToken token, ReconnectionTyp
447465

448466
private void ReconnectOnError(object? state)
449467
{
450-
// await Task.Delay(timeout, token).ConfigureAwait(false);
468+
if (_client != null && ShouldIgnoreReconnection(_client))
469+
return;
470+
451471
_ = Reconnect(ReconnectionType.Error, false, state as Exception).ConfigureAwait(false);
452472
}
453473

@@ -491,7 +511,7 @@ private async Task Listen(WebSocket client, CancellationToken token)
491511
}
492512
else if (result.MessageType == WebSocketMessageType.Close)
493513
{
494-
_logger.LogTrace(L("Received close message"), Name);
514+
_logger.LogDebug(L("Received close message"), Name);
495515

496516
if (!IsStarted || _stopping)
497517
{
@@ -512,13 +532,12 @@ private async Task Listen(WebSocket client, CancellationToken token)
512532
continue;
513533
}
514534

515-
await StopInternal(client, WebSocketCloseStatus.NormalClosure, "Closing",
516-
token, false, true);
535+
await StopInternal(client, WebSocketCloseStatus.NormalClosure, "Closing", token, false, true);
517536

518537
// reconnect if enabled
519538
if (IsReconnectionEnabled && !ShouldIgnoreReconnection(client))
520539
{
521-
_ = ReconnectSynchronized(ReconnectionType.Lost, false, null);
540+
_ = ReconnectSynchronized(ReconnectionType.ByServer, false, null);
522541
}
523542

524543
return;

0 commit comments

Comments
 (0)