Skip to content

Commit fd0b49d

Browse files
committed
https://trello.com/c/fTYkIH1O Attempt to make the signalr connection logic more robust
1 parent 2b4a158 commit fd0b49d

File tree

2 files changed

+63
-53
lines changed

2 files changed

+63
-53
lines changed

ProReception.DistributionServerInfrastructure/HostedServices/SignalRHostedService.cs

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Configuration;
55
using Flurl;
66
using Flurl.Http;
7+
using JetBrains.Annotations;
78
using Microsoft.AspNetCore.SignalR.Client;
89
using Microsoft.Extensions.Hosting;
910
using Microsoft.Extensions.Logging;
@@ -12,8 +13,8 @@
1213
using Polly.Retry;
1314
using ProReceptionApi;
1415
using Settings;
15-
using Settings.Models.Public;
1616

17+
[PublicAPI]
1718
[UnsupportedOSPlatform("browser")] // Proxy support in SignalR is not supported in browser
1819
public abstract class SignalRHostedService<T>(
1920
IOptions<ProReceptionApiConfiguration> proReceptionApiConfigurationOptions,
@@ -23,16 +24,16 @@ public abstract class SignalRHostedService<T>(
2324
ISettingsManagerBase settingsManagerBase)
2425
: IHostedService
2526
{
26-
private readonly CancellationTokenSource _stoppingCts = new();
27+
private readonly CancellationTokenSource stoppingCts = new();
2728

28-
private Task? _startUpTask;
29-
private HubConnection? _hubConnection;
29+
private Task? startUpTask;
30+
private HubConnection? hubConnection;
3031

3132
protected abstract string HubPath { get; }
3233

3334
public Task StartAsync(CancellationToken cancellationToken)
3435
{
35-
_startUpTask = ExecuteStartUp(_stoppingCts.Token);
36+
startUpTask = ExecuteStartUp(stoppingCts.Token);
3637

3738
return Task.CompletedTask;
3839
}
@@ -42,24 +43,24 @@ public async Task StopAsync(CancellationToken cancellationToken)
4243
logger.LogInformation($"Stopping {typeof(T).Name}...");
4344

4445
// Stop called without start
45-
if (_startUpTask == null)
46+
if (startUpTask == null)
4647
{
4748
return;
4849
}
4950

5051
try
5152
{
5253
// Signal cancellation to the executing method
53-
await _stoppingCts.CancelAsync();
54+
await stoppingCts.CancelAsync();
5455
}
5556
finally
5657
{
5758
// Wait until the task completes or the stop token triggers
58-
await Task.WhenAny(_startUpTask, Task.Delay(Timeout.Infinite, cancellationToken));
59+
await Task.WhenAny(startUpTask, Task.Delay(Timeout.Infinite, cancellationToken));
5960

60-
if (_hubConnection != null)
61+
if (hubConnection != null)
6162
{
62-
await _hubConnection.DisposeAsync();
63+
await hubConnection.DisposeAsync();
6364
}
6465
}
6566
}
@@ -86,7 +87,7 @@ private async Task ExecuteStartUp(CancellationToken cancellationToken)
8687
exceptionMessage += $" Response body: {await flurlException.GetResponseStringAsync()}";
8788
}
8889

89-
logger.LogWarning("Attempt {AttemptNumber} failed: {ExceptionMessage}. Waiting {RetryDelay} before next try.", args.AttemptNumber, exceptionMessage, args.RetryDelay);
90+
logger.LogWarning("Attempt {AttemptNumber} failed: {ExceptionMessage}. Waiting {RetryDelay} before next try", args.AttemptNumber, exceptionMessage, args.RetryDelay);
9091
}
9192
})
9293
.Build()
@@ -95,67 +96,76 @@ private async Task ExecuteStartUp(CancellationToken cancellationToken)
9596

9697
private async Task LoginAndCreateSignalRConnection(CancellationToken cancellationToken)
9798
{
98-
TokensRecord? proReceptionTokens;
99-
do
100-
{
101-
proReceptionTokens = await GetProReceptionTokens(cancellationToken);
102-
} while (!cancellationToken.IsCancellationRequested && string.IsNullOrWhiteSpace(proReceptionTokens?.AccessToken));
103-
10499
if (!cancellationToken.IsCancellationRequested)
105100
{
106-
if (string.IsNullOrWhiteSpace(proReceptionTokens?.AccessToken))
107-
{
108-
throw new ApplicationException("The ProReception access token is null or empty (this should never happen)");
109-
}
110-
111101
logger.LogInformation("Establishing SignalR connection...");
112102

113-
_hubConnection = new HubConnectionBuilder()
103+
hubConnection = new HubConnectionBuilder()
114104
.WithUrl(proReceptionApiConfigurationOptions.Value.BaseUrl.AppendPathSegment(HubPath), options =>
115105
{
116-
options.Headers.Add("Authorization", $"Bearer {proReceptionTokens.AccessToken}");
106+
// Provide dynamic access token for every (re)connect to avoid stale tokens
107+
options.AccessTokenProvider = async () =>
108+
{
109+
var current = settingsManagerBase.GetTokens();
110+
if (current is null)
111+
{
112+
// No tokens available (e.g., user logged out) -> return null to let reconnect keep retrying
113+
logger.LogInformation("SignalR AccessTokenProvider: no tokens available");
114+
return null;
115+
}
116+
117+
// Refresh token if expiring soon
118+
if (current.ExpiresAtUtc.AddMinutes(-10) < DateTime.UtcNow)
119+
{
120+
try
121+
{
122+
current = await proReceptionApiClient.RefreshAndSaveTokens(current);
123+
logger.LogInformation("SignalR AccessTokenProvider: token refreshed");
124+
}
125+
catch (Exception ex)
126+
{
127+
logger.LogWarning(ex, "SignalR AccessTokenProvider: failed to refresh token");
128+
// Return existing token (may fail with 401 and trigger reconnect), or null if missing
129+
if (string.IsNullOrWhiteSpace(current.AccessToken))
130+
{
131+
return null;
132+
}
133+
}
134+
}
135+
136+
return current.AccessToken;
137+
};
138+
117139
options.Headers.Add("X-DistributionServerAppId", settingsManagerBase.GetDistributionServerAppId().ToString());
118140
options.Proxy = proxyConfigurationOptions.Value.GetWebProxy();
119141
})
120142
.WithAutomaticReconnect()
121143
.Build();
122144

123-
ConfigureListeners(_hubConnection);
145+
ConfigureListeners(hubConnection);
124146

125-
_hubConnection.Closed += async _ =>
147+
hubConnection.Reconnecting += error =>
126148
{
127-
logger.LogInformation("SignalR connection lost, will retry...");
128-
await _hubConnection.StopAsync(cancellationToken);
129-
await ExecuteStartUp(cancellationToken);
149+
logger.LogInformation(error, "SignalR connection reconnecting...");
150+
return Task.CompletedTask;
130151
};
131152

132-
await _hubConnection.StartAsync(cancellationToken);
133-
134-
logger.LogInformation("SignalR connection successfully established");
135-
}
136-
}
137-
138-
private async Task<TokensRecord?> GetProReceptionTokens(CancellationToken cancellationToken)
139-
{
140-
while(!cancellationToken.IsCancellationRequested)
141-
{
142-
var proReceptionTokens = settingsManagerBase.GetTokens();
143-
144-
if (!string.IsNullOrWhiteSpace(proReceptionTokens?.AccessToken))
153+
hubConnection.Reconnected += connectionId =>
145154
{
146-
if (proReceptionTokens.ExpiresAtUtc.AddMinutes(-10) < DateTime.UtcNow)
147-
{
148-
return await proReceptionApiClient.RefreshAndSaveTokens(proReceptionTokens);
149-
}
155+
logger.LogInformation("SignalR connection reconnected. ConnectionId={ConnectionId}", connectionId);
156+
return Task.CompletedTask;
157+
};
150158

151-
return proReceptionTokens;
152-
}
159+
hubConnection.Closed += error =>
160+
{
161+
// Do not call StopAsync or recursively restart; rely on automatic reconnect and outer retry/startup logic
162+
logger.LogInformation(error, "SignalR connection closed. Waiting for background retry logic...");
163+
return Task.CompletedTask;
164+
};
153165

154-
logger.LogInformation("Not logged into ProReception, sleeping...");
166+
await hubConnection.StartAsync(cancellationToken);
155167

156-
await Task.Delay(1000, cancellationToken);
168+
logger.LogInformation("SignalR connection successfully established");
157169
}
158-
159-
return null;
160170
}
161171
}

version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>2.1.0</Version>
3+
<Version>2.2.0</Version>
44
</PropertyGroup>
55
</Project>

0 commit comments

Comments
 (0)