Skip to content

Commit 2c92f22

Browse files
authored
Feature/implement reactive network monitor to trigger reconnection (#122)
* Add network monitor that uses Unity's network reachability to monitor network state and notify whenever it changes * refactored rescheduling logic to a separate class * Add unit tests for ReconnectScheduler + refactor client ctor test
1 parent f2a33d4 commit 2c92f22

16 files changed

Lines changed: 552 additions & 164 deletions

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
namespace StreamChat.Core.LowLevelClient
99
{
10+
/// <summary>
11+
/// Handler delegate for a connection state change
12+
/// </summary>
13+
public delegate void ConnectionStateChangeHandler(ConnectionState previous, ConnectionState current);
14+
1015
/// <summary>
1116
/// Stream Chat Client
1217
/// </summary>
@@ -16,6 +21,11 @@ public interface IStreamChatLowLevelClient : IAuthProvider, IConnectionProvider,
1621
/// Client established WebSockets connection and is ready to send and receive data
1722
/// </summary>
1823
event ConnectionHandler Connected;
24+
25+
/// <summary>
26+
/// Client is attempting to reconnect after lost connection
27+
/// </summary>
28+
event Action Reconnecting;
1929

2030
/// <summary>
2131
/// Client lost connection with the server. if ReconnectStrategy is Exponential or Constant it will attempt to reconnect.
@@ -26,7 +36,7 @@ public interface IStreamChatLowLevelClient : IAuthProvider, IConnectionProvider,
2636
/// <summary>
2737
/// Raised when connection state changes. Returns previous and the current connection state respectively
2838
/// </summary>
29-
event Action<ConnectionState, ConnectionState> ConnectionStateChanged;
39+
event ConnectionStateChangeHandler ConnectionStateChanged;
3040

3141
ConnectionState ConnectionState { get; }
3242
ReconnectStrategy ReconnectStrategy { get; }
@@ -75,6 +85,6 @@ void SetReconnectStrategySettings(ReconnectStrategy reconnectStrategy, float? ex
7585

7686
void ConnectUser(AuthCredentials userAuthCredentials);
7787

78-
Task DisconnectAsync(bool permanently = false);
88+
Task DisconnectAsync(bool permanent = false);
7989
}
8090
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using System;
2+
using StreamChat.Core.LowLevelClient.Models;
3+
using StreamChat.Libs.NetworkMonitors;
4+
using StreamChat.Libs.Time;
5+
6+
namespace StreamChat.Core.LowLevelClient
7+
{
8+
/// <summary>
9+
/// Schedules next reconnection time based on the past attempts and network availability
10+
/// </summary>
11+
internal class ReconnectScheduler : IDisposable
12+
{
13+
public event Action ReconnectionScheduled;
14+
public ReconnectStrategy ReconnectStrategy { get; private set; } = ReconnectStrategy.Exponential;
15+
public float ReconnectConstantInterval { get; private set; } = 1;
16+
public float ReconnectExponentialMinInterval { get; private set; } = 0.01f;
17+
public float ReconnectExponentialMaxInterval { get; private set; } = 64;
18+
public int ReconnectMaxInstantTrials { get; private set; } = 5; //StreamTodo: allow to control this by user
19+
20+
public double? NextReconnectTime
21+
{
22+
get => _nextReconnectTime;
23+
private set
24+
{
25+
var isEqual = _nextReconnectTime.HasValue && value.HasValue &&
26+
Math.Abs(_nextReconnectTime.Value - value.Value) < float.Epsilon;
27+
if (isEqual)
28+
{
29+
return;
30+
}
31+
32+
_nextReconnectTime = value;
33+
34+
if (_nextReconnectTime.HasValue)
35+
{
36+
ReconnectionScheduled?.Invoke();
37+
}
38+
}
39+
}
40+
41+
public ReconnectScheduler(ITimeService timeService, IStreamChatLowLevelClient lowLevelClient,
42+
INetworkMonitor networkMonitor)
43+
{
44+
_client = lowLevelClient ?? throw new ArgumentNullException(nameof(lowLevelClient));
45+
_timeService = timeService ?? throw new ArgumentNullException(nameof(timeService));
46+
_networkMonitor = networkMonitor ?? throw new ArgumentNullException(nameof(networkMonitor));
47+
48+
_networkMonitor.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
49+
50+
_client.Connected += OnConnected;
51+
_client.Reconnecting += OnReconnecting;
52+
_client.ConnectionStateChanged += OnConnectionStateChanged;
53+
}
54+
55+
public void Dispose()
56+
{
57+
if (_client != null)
58+
{
59+
_client.Connected -= OnConnected;
60+
_client.Reconnecting -= OnReconnecting;
61+
_client.ConnectionStateChanged -= OnConnectionStateChanged;
62+
}
63+
}
64+
65+
public void SetReconnectStrategySettings(ReconnectStrategy reconnectStrategy, float? exponentialMinInterval,
66+
float? exponentialMaxInterval, float? constantInterval)
67+
{
68+
ReconnectStrategy = reconnectStrategy;
69+
70+
void ThrowIfLessOrEqualToZero(float value, string name)
71+
{
72+
if (value <= 0)
73+
{
74+
throw new ArgumentException($"{name} needs to be greater than zero, given: " + value);
75+
}
76+
}
77+
78+
if (exponentialMinInterval.HasValue)
79+
{
80+
ThrowIfLessOrEqualToZero(exponentialMinInterval.Value, nameof(exponentialMinInterval));
81+
ReconnectExponentialMinInterval = exponentialMinInterval.Value;
82+
}
83+
84+
if (exponentialMaxInterval.HasValue)
85+
{
86+
ThrowIfLessOrEqualToZero(exponentialMaxInterval.Value, nameof(exponentialMaxInterval));
87+
ReconnectExponentialMaxInterval = exponentialMaxInterval.Value;
88+
}
89+
90+
if (constantInterval.HasValue)
91+
{
92+
ThrowIfLessOrEqualToZero(constantInterval.Value, nameof(constantInterval));
93+
ReconnectConstantInterval = constantInterval.Value;
94+
}
95+
}
96+
97+
public void Stop()
98+
{
99+
NextReconnectTime = float.MaxValue;
100+
_isStopped = true;
101+
}
102+
103+
//StreamTodo: connection info could be split to separate interface
104+
private readonly IStreamChatLowLevelClient _client;
105+
private readonly ITimeService _timeService;
106+
private readonly INetworkMonitor _networkMonitor;
107+
108+
private int _reconnectAttempts;
109+
private bool _isStopped;
110+
private double? _nextReconnectTime;
111+
112+
private void TryScheduleNextReconnectTime()
113+
{
114+
if (NextReconnectTime.HasValue && NextReconnectTime.Value > _timeService.Time)
115+
{
116+
return;
117+
}
118+
119+
if (_isStopped || ReconnectStrategy == ReconnectStrategy.Never)
120+
{
121+
return;
122+
}
123+
124+
double? GetNextReconnectTime()
125+
{
126+
if (_reconnectAttempts <= ReconnectMaxInstantTrials)
127+
{
128+
return _timeService.Time;
129+
}
130+
131+
switch (ReconnectStrategy)
132+
{
133+
case ReconnectStrategy.Exponential:
134+
135+
var baseInterval = Math.Pow(2, _reconnectAttempts);
136+
var interval = Math.Min(Math.Max(ReconnectExponentialMinInterval, baseInterval),
137+
ReconnectExponentialMaxInterval);
138+
return _timeService.Time + interval;
139+
case ReconnectStrategy.Constant:
140+
return _timeService.Time + ReconnectConstantInterval;
141+
case ReconnectStrategy.Never:
142+
return null;
143+
default:
144+
throw new ArgumentOutOfRangeException(
145+
$"Unhandled {nameof(ReconnectStrategy)}: {ReconnectStrategy}");
146+
}
147+
}
148+
149+
NextReconnectTime = GetNextReconnectTime();
150+
}
151+
152+
private void OnConnectionStateChanged(ConnectionState previous, ConnectionState current)
153+
{
154+
switch (current)
155+
{
156+
case ConnectionState.Disconnected:
157+
158+
TryScheduleNextReconnectTime();
159+
160+
break;
161+
case ConnectionState.Connecting:
162+
163+
NextReconnectTime = default;
164+
165+
break;
166+
case ConnectionState.WaitToReconnect:
167+
break;
168+
case ConnectionState.Connected:
169+
break;
170+
case ConnectionState.Closing:
171+
break;
172+
default:
173+
throw new ArgumentOutOfRangeException(nameof(current), current, null);
174+
}
175+
}
176+
177+
private void OnNetworkAvailabilityChanged(bool isNetworkAvailable)
178+
{
179+
if (!isNetworkAvailable)
180+
{
181+
return;
182+
}
183+
184+
if (_client.ConnectionState == ConnectionState.Connected ||
185+
_client.ConnectionState == ConnectionState.Connecting)
186+
{
187+
return;
188+
}
189+
190+
if (_isStopped)
191+
{
192+
return;
193+
}
194+
195+
NextReconnectTime = _timeService.Time;
196+
}
197+
198+
private void OnReconnecting()
199+
{
200+
_reconnectAttempts++;
201+
}
202+
203+
private void OnConnected(OwnUser localUser)
204+
{
205+
_reconnectAttempts = 0;
206+
NextReconnectTime = default;
207+
}
208+
}
209+
}

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

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)