Skip to content

Commit 099b023

Browse files
authored
Pbe 4270 debug internal client web socket freeze on android after multiple reconnections (#154)
* Add more debug logs * Fix compiler error in tests * Remove obsolete code * Hide warning regarding obsolete property assignment - we keep it for backward compatibility * Fix ConnectAsync hanging on Android 14 & Unity 2022.3.29 after multiple attempts when the network was unavailable. Now the hanged connection properly times out and repeats attempts. * Add LowLevelClientConnectionTests + fix `Create_channel_with_custom_data` failing due to a field not being declared in the model and therefore being automatically added into additional properties
1 parent 19d8cdc commit 099b023

File tree

11 files changed

+240
-20
lines changed

11 files changed

+240
-20
lines changed

Assets/Plugins/StreamChat/Core/LowLevelClient/ReconnectScheduler.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using StreamChat.Core.LowLevelClient.Models;
3+
using StreamChat.Libs.Logs;
34
using StreamChat.Libs.NetworkMonitors;
45
using StreamChat.Libs.Time;
56

@@ -39,12 +40,13 @@ private set
3940
}
4041

4142
public ReconnectScheduler(ITimeService timeService, IStreamChatLowLevelClient lowLevelClient,
42-
INetworkMonitor networkMonitor)
43+
INetworkMonitor networkMonitor, ILogs logs)
4344
{
4445
_client = lowLevelClient ?? throw new ArgumentNullException(nameof(lowLevelClient));
4546
_timeService = timeService ?? throw new ArgumentNullException(nameof(timeService));
4647
_networkMonitor = networkMonitor ?? throw new ArgumentNullException(nameof(networkMonitor));
47-
48+
_logs = logs ?? throw new ArgumentNullException(nameof(logs));
49+
4850
_networkMonitor.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
4951

5052
_client.Connected += OnConnected;
@@ -106,6 +108,7 @@ public void Stop()
106108
private readonly IStreamChatLowLevelClient _client;
107109
private readonly ITimeService _timeService;
108110
private readonly INetworkMonitor _networkMonitor;
111+
private readonly ILogs _logs;
109112

110113
private int _reconnectAttempts;
111114
private bool _isStopped;
@@ -178,6 +181,9 @@ private void OnConnectionStateChanged(ConnectionState previous, ConnectionState
178181

179182
private void OnNetworkAvailabilityChanged(bool isNetworkAvailable)
180183
{
184+
#if STREAM_DEBUG_ENABLED
185+
_logs.Warning($"Network availability changed to: {isNetworkAvailable}");
186+
#endif
181187
if (!isNetworkAvailable)
182188
{
183189
return;

Assets/Plugins/StreamChat/Core/LowLevelClient/Requests/ChannelMemberRequest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ ChannelMemberRequestInternalDTO ISavableTo<ChannelMemberRequestInternalDTO>.Save
8181
InviteRejectedAt = InviteRejectedAt,
8282
Invited = Invited,
8383
IsModerator = IsModerator,
84+
#pragma warning disable CS0618
8485
Role = Role,
86+
#pragma warning restore CS0618
8587
ShadowBanned = ShadowBanned,
8688
UpdatedAt = UpdatedAt,
8789
User = User.TrySaveToDto(),

Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ private set
179179

180180
var previous = _connectionState;
181181
_connectionState = value;
182+
183+
#if STREAM_DEBUG_ENABLED
184+
_logs.Warning($"Connection state changed from: {previous} to: {value}");
185+
#endif
186+
182187
ConnectionStateChanged?.Invoke(previous, _connectionState);
183188

184189
if (value == ConnectionState.Disconnected)
@@ -302,7 +307,7 @@ public StreamChatLowLevelClient(AuthCredentials authCredentials, IWebsocketClien
302307
UserApi = new UserApi(InternalUserApi);
303308
DeviceApi = new DeviceApi(InternalDeviceApi);
304309

305-
_reconnectScheduler = new ReconnectScheduler(_timeService, this, _networkMonitor);
310+
_reconnectScheduler = new ReconnectScheduler(_timeService, this, _networkMonitor, _logs);
306311
_reconnectScheduler.ReconnectionScheduled += OnReconnectionScheduled;
307312

308313
RegisterEventHandlers();
@@ -483,6 +488,7 @@ internal async Task<OwnUserInternalDTO> ConnectUserAsync(string apiKey, string u
483488

484489
var connectionUri = _requestUriFactory.CreateConnectionUri();
485490

491+
//StreamTodo: pass the cancellation token here cancellationToken
486492
await _websocketClient.ConnectAsync(connectionUri);
487493

488494
var ownUserDto = await _connectUserTaskSource.Task;

Assets/Plugins/StreamChat/Libs/Websockets/IWebsocketClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public interface IWebsocketClient : IDisposable
1515

1616
bool TryDequeueMessage(out string message);
1717

18-
Task ConnectAsync(Uri serverUri);
18+
Task ConnectAsync(Uri serverUri, int timeout = 3);
1919

2020
void Update();
2121

Assets/Plugins/StreamChat/Libs/Websockets/WebsocketClient.cs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using StreamChat.Libs.Logs;
11+
#if STREAM_DEBUG_ENABLED
12+
using System.Diagnostics;
13+
#endif
1114

1215
namespace StreamChat.Libs.Websockets
1316
{
@@ -35,7 +38,7 @@ public WebsocketClient(ILogs logs, Encoding encoding = default, bool isDebugMode
3538

3639
public bool TryDequeueMessage(out string message) => _receiveQueue.TryDequeue(out message);
3740

38-
public async Task ConnectAsync(Uri serverUri)
41+
public async Task ConnectAsync(Uri serverUri, int timeout = 3)
3942
{
4043
if (IsConnected || IsConnecting)
4144
{
@@ -52,11 +55,48 @@ await TryDisposeResourcesAsync(WebSocketCloseStatus.NormalClosure,
5255
_connectionCts = new CancellationTokenSource();
5356

5457
_internalClient = new ClientWebSocket();
55-
await _internalClient.ConnectAsync(_uri, _connectionCts.Token).ConfigureAwait(false);
58+
59+
#if STREAM_DEBUG_ENABLED
60+
var ws = new Stopwatch();
61+
ws.Start();
62+
_logs.Warning($"Internal WS ConnectAsync CALL");
63+
#endif
64+
65+
var connectTask = _internalClient.ConnectAsync(_uri, _connectionCts.Token);
66+
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeout));
67+
68+
// We handle timeout this way because ConnectAsync was hanging after multiple attempts on Unity 2022.3.29 & Android 14 and cancellation via passed token didn't work
69+
var finishedTask = await Task.WhenAny(connectTask, timeoutTask);
70+
71+
if (finishedTask == timeoutTask)
72+
{
73+
#if STREAM_DEBUG_ENABLED
74+
_logs.Warning("Internal WS Connection attempt timed out.");
75+
#endif
76+
throw new TimeoutException($"Connection attempt timed out after {timeout} seconds.");
77+
}
78+
79+
if (_connectionCts == null || _connectionCts.Token.IsCancellationRequested)
80+
{
81+
#if STREAM_DEBUG_ENABLED
82+
_logs.Warning("Internal WS Connection attempt cancelled.");
83+
#endif
84+
throw new OperationCanceledException();
85+
}
86+
87+
await connectTask;
88+
89+
#if STREAM_DEBUG_ENABLED
90+
ws.Stop();
91+
_logs.Warning($"Internal WS ConnectAsync COMPLETED in {ws.ElapsedMilliseconds} ms.");
92+
#endif
93+
5694
}
5795
catch (OperationCanceledException e)
5896
{
5997
LogExceptionIfDebugMode(e);
98+
OnConnectionFailed();
99+
return;
60100
}
61101
catch (WebSocketException e)
62102
{
@@ -93,6 +133,15 @@ public void Send(string message)
93133

94134
public void Update()
95135
{
136+
#if STREAM_DEBUG_ENABLED
137+
138+
if(_internalClient != null && _internalClient.State != _lastState)
139+
{
140+
_logs.Warning($"Internal WS state -> changed from {_lastState} to " + _internalClient.State);
141+
_lastState = _internalClient.State;
142+
}
143+
#endif
144+
96145
var disconnect = false;
97146
while (_threadWebsocketExceptionsLog.TryDequeue(out var webSocketException))
98147
{
@@ -124,9 +173,13 @@ public async Task DisconnectAsync(WebSocketCloseStatus closeStatus, string close
124173

125174
public void Dispose()
126175
{
127-
LogInfoIfDebugMode("Dispose");
128-
DisconnectAsync(WebSocketCloseStatus.NormalClosure, "WebSocket client is disposed")
129-
.ContinueWith(_ => LogExceptionIfDebugMode(_.Exception), TaskContinuationOptions.OnlyOnFaulted);
176+
LogInfoIfDebugMode("Dispose " + Thread.CurrentThread.ManagedThreadId);
177+
178+
if(_internalClient != null && !_clientClosedStates.Contains(_internalClient.State))
179+
{
180+
DisconnectAsync(WebSocketCloseStatus.NormalClosure, "WebSocket client is disposed")
181+
.ContinueWith(t => LogExceptionIfDebugMode(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
182+
}
130183
}
131184

132185
private const int UpdatesPerSecond = 20;
@@ -163,6 +216,8 @@ public void Dispose()
163216
private ClientWebSocket _internalClient;
164217
private CancellationTokenSource _connectionCts;
165218

219+
private WebSocketState _lastState;
220+
166221
private async void SendMessagesCallback(object state)
167222
{
168223
if (!IsConnected || _connectionCts == null || _connectionCts.IsCancellationRequested)
@@ -250,6 +305,14 @@ private async Task TryDisposeResourcesAsync(WebSocketCloseStatus closeStatus, st
250305
{
251306
_backgroundReceiveTimer?.Dispose();
252307
_backgroundReceiveTimer = null;
308+
}
309+
catch (Exception e)
310+
{
311+
LogExceptionIfDebugMode(e);
312+
}
313+
314+
try
315+
{
253316
_backgroundSendTimer?.Dispose();
254317
_backgroundSendTimer = null;
255318
}
@@ -281,6 +344,9 @@ private async Task TryDisposeResourcesAsync(WebSocketCloseStatus closeStatus, st
281344
{
282345
if (!_clientClosedStates.Contains(_internalClient.State))
283346
{
347+
#if STREAM_DEBUG_ENABLED
348+
_logs.Warning("Internal WS - Close in state: " + _internalClient.State);
349+
#endif
284350
await _internalClient.CloseOutputAsync(closeStatus, closeMessage, CancellationToken.None);
285351
}
286352
}
@@ -290,6 +356,9 @@ private async Task TryDisposeResourcesAsync(WebSocketCloseStatus closeStatus, st
290356
}
291357
finally
292358
{
359+
#if STREAM_DEBUG_ENABLED
360+
_logs.Warning("Internal WS - Dispose in state: " + _internalClient.State);
361+
#endif
293362
_internalClient.Dispose();
294363
_internalClient = null;
295364
}
@@ -300,7 +369,7 @@ private async Task TryDisposeResourcesAsync(WebSocketCloseStatus closeStatus, st
300369
// Called from a background thread
301370
private void OnReceivedCloseMessage()
302371
=> DisconnectAsync(WebSocketCloseStatus.InternalServerError, "Server closed the connection")
303-
.ContinueWith(_ => LogThreadExceptionIfDebugMode(_.Exception), TaskContinuationOptions.OnlyOnFaulted);
372+
.ContinueWith(t => LogThreadExceptionIfDebugMode(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
304373

305374
private async Task<string> TryReceiveSingleMessageAsync()
306375
{
@@ -344,7 +413,7 @@ private async Task<string> TryReceiveSingleMessageAsync()
344413
throw new Exception("Unhandled WebSocket message type: " + WebSocketMessageType.Binary);
345414
}
346415

347-
return "";
416+
return string.Empty;
348417
}
349418
}
350419

Assets/Plugins/StreamChat/SampleProject/Scripts/StreamChatClientBehaviour.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ protected void Awake()
6666
}
6767
});
6868

69-
_missingCredentials = true;
70-
7169
#if UNITY_EDITOR
7270

7371
StartCoroutine(BlinkProjectAsset(_authCredentialsAsset, popup));
@@ -81,7 +79,6 @@ protected void Awake()
8179
}
8280

8381
private IStreamChatClient _client;
84-
private bool _missingCredentials;
8582

8683
[SerializeField]
8784
private RootView _rootView;

Assets/Plugins/StreamChat/Tests/LowLevelClient/Integration/ChannelApiIntegrationTests.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using StreamChat.Core.Exceptions;
99
using StreamChat.Core.LowLevelClient.Models;
1010
using StreamChat.Core.LowLevelClient.Requests;
11+
using UnityEngine;
1112
using UnityEngine.TestTools;
1213

1314
namespace StreamChat.Tests.LowLevelClient.Integration
@@ -59,8 +60,14 @@ public IEnumerator Create_channel_with_custom_data()
5960
ChannelState channelState = null;
6061
yield return CreateTempUniqueChannel("messaging", requestBody, state => channelState = state);
6162

62-
Assert.AreEqual(3, channelState.Channel.AdditionalProperties.Count);
63-
Assert.AreEqual(3, channelState.Channel.AdditionalProperties.Count);
63+
Assert.IsTrue(channelState.Channel.AdditionalProperties.ContainsKey("MyNumber"));
64+
Assert.AreEqual(3, channelState.Channel.AdditionalProperties["MyNumber"]);
65+
Assert.IsTrue(channelState.Channel.AdditionalProperties.ContainsKey("MyString"));
66+
Assert.AreEqual("Hey Joe!", channelState.Channel.AdditionalProperties["MyString"]);
67+
Assert.IsTrue(channelState.Channel.AdditionalProperties.ContainsKey("MyIntArray"));
68+
69+
//StreamTodo: fix returned array here to not be JArray https://stream-io.atlassian.net/browse/PBE-4851
70+
//Assert.AreEqual(new int[] { 5, 8, 9 }, (int[])channelState.Channel.AdditionalProperties["MyIntArray"]);
6471
}
6572

6673
[UnityTest]

0 commit comments

Comments
 (0)