Skip to content

Commit 11ac90e

Browse files
Add redis startup error handling (#41141)
1 parent 8b5d067 commit 11ac90e

File tree

3 files changed

+70
-3
lines changed

3 files changed

+70
-3
lines changed

src/SignalR/server/StackExchangeRedis/src/Internal/RedisLog.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public static void ConnectingToEndpoints(ILogger logger, EndPointCollection endp
5858
[LoggerMessage(13, LogLevel.Error, "Error forwarding client result with ID '{InvocationID}' to server.", EventName = "ErrorForwardingResult")]
5959
public static partial void ErrorForwardingResult(ILogger logger, string invocationId, Exception ex);
6060

61+
[LoggerMessage(14, LogLevel.Error, "Error connecting to Redis.", EventName = "ErrorConnecting")]
62+
public static partial void ErrorConnecting(ILogger logger, Exception ex);
63+
6164
// This isn't DefineMessage-based because it's just the simple TextWriter logging from ConnectionMultiplexer
6265
public static void ConnectionMultiplexerMessage(ILogger logger, string? message)
6366
{

src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposab
3535
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1);
3636
private readonly IHubProtocolResolver _hubProtocolResolver;
3737
private readonly ClientResultsManager _clientResultsManager = new();
38+
private bool _redisConnectErrorLogged;
3839

3940
private readonly AckHandler _ackHandler;
4041
private int _internalAckId;
@@ -90,6 +91,7 @@ public RedisHubLifetimeManager(ILogger<RedisHubLifetimeManager<THub>> logger,
9091
public override async Task OnConnectedAsync(HubConnectionContext connection)
9192
{
9293
await EnsureRedisServerConnection();
94+
9395
var feature = new RedisFeature();
9496
connection.Features.Set<IRedisFeature>(feature);
9597

@@ -112,11 +114,17 @@ public override Task OnDisconnectedAsync(HubConnectionContext connection)
112114
{
113115
_connections.Remove(connection);
114116

115-
var tasks = new List<Task>();
117+
// If the bus is null then the Redis connection failed to be established and none of the other connection setup ran
118+
if (_bus is null)
119+
{
120+
return Task.CompletedTask;
121+
}
116122

117123
var connectionChannel = _channels.Connection(connection.ConnectionId);
124+
var tasks = new List<Task>();
125+
118126
RedisLog.Unsubscribe(_logger, connectionChannel);
119-
tasks.Add(_bus!.UnsubscribeAsync(connectionChannel));
127+
tasks.Add(_bus.UnsubscribeAsync(connectionChannel));
120128

121129
var feature = connection.Features.GetRequiredFeature<IRedisFeature>();
122130
var groupNames = feature.Groups;
@@ -704,7 +712,21 @@ private async Task EnsureRedisServerConnection()
704712
if (_redisServerConnection == null)
705713
{
706714
var writer = new LoggerTextWriter(_logger);
707-
_redisServerConnection = await _options.ConnectAsync(writer);
715+
try
716+
{
717+
_redisServerConnection = await _options.ConnectAsync(writer);
718+
}
719+
catch (Exception ex)
720+
{
721+
// If the connection hasn't been established yet we shouldn't keep logging the same error over and over
722+
// for every new client connection.
723+
if (!_redisConnectErrorLogged)
724+
{
725+
RedisLog.ErrorConnecting(_logger, ex);
726+
_redisConnectErrorLogged = true;
727+
}
728+
throw;
729+
}
708730
_bus = _redisServerConnection.GetSubscriber();
709731

710732
_redisServerConnection.ConnectionRestored += (_, e) =>

src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
using Microsoft.AspNetCore.SignalR.Specification.Tests;
88
using Microsoft.AspNetCore.SignalR.Tests;
99
using Microsoft.AspNetCore.Testing;
10+
using Microsoft.Extensions.Logging;
1011
using Microsoft.Extensions.Logging.Abstractions;
12+
using Microsoft.Extensions.Logging.Testing;
1113
using Microsoft.Extensions.Options;
14+
using Moq;
1215
using Newtonsoft.Json.Linq;
1316
using Newtonsoft.Json.Serialization;
1417
using Xunit;
@@ -83,6 +86,45 @@ public async Task CamelCasedJsonIsPreservedAcrossRedisBoundary()
8386
}
8487
}
8588

89+
[Fact]
90+
public async Task ErrorFromConnectionFactoryLogsAndAllowsDisconnect()
91+
{
92+
var server = new TestRedisServer();
93+
94+
var testSink = new TestSink();
95+
var logger = new TestLogger("", testSink, true);
96+
var mockLoggerFactory = new Mock<ILoggerFactory>();
97+
mockLoggerFactory
98+
.Setup(m => m.CreateLogger(It.IsAny<string>()))
99+
.Returns((string categoryName) => (ILogger)logger);
100+
var loggerT = mockLoggerFactory.Object.CreateLogger<RedisHubLifetimeManager<Hub>>();
101+
102+
var manager = new RedisHubLifetimeManager<Hub>(
103+
loggerT,
104+
Options.Create(new RedisOptions()
105+
{
106+
ConnectionFactory = _ => throw new Exception("throw from connect")
107+
}),
108+
new DefaultHubProtocolResolver(new IHubProtocol[]
109+
{
110+
}, NullLogger<DefaultHubProtocolResolver>.Instance));
111+
112+
using (var client = new TestClient())
113+
{
114+
var connection = HubConnectionContextUtils.Create(client.Connection);
115+
116+
var ex = await Assert.ThrowsAsync<Exception>(() => manager.OnConnectedAsync(connection)).DefaultTimeout();
117+
Assert.Equal("throw from connect", ex.Message);
118+
119+
await manager.OnDisconnectedAsync(connection).DefaultTimeout();
120+
}
121+
122+
var logs = testSink.Writes.ToArray();
123+
Assert.Single(logs);
124+
Assert.Equal("Error connecting to Redis.", logs[0].Message);
125+
Assert.Equal("throw from connect", logs[0].Exception.Message);
126+
}
127+
86128
public override TestRedisServer CreateBackplane()
87129
{
88130
return new TestRedisServer();

0 commit comments

Comments
 (0)