Skip to content

Commit 32e63b2

Browse files
committed
Add async Redis connection multiplexer factory
1 parent a89a36f commit 32e63b2

6 files changed

Lines changed: 142 additions & 2 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ public void ConfigureServices(IServiceCollection services)
233233

234234
Each HealthCheck registration supports also name, tags, failure status and other optional parameters.
235235

236+
For Redis connections that require asynchronous setup, use the async multiplexer factory and return a shared
237+
`IConnectionMultiplexer` instance:
238+
239+
```csharp
240+
services.AddHealthChecks()
241+
.AddRedis(async (_, cancellationToken) =>
242+
{
243+
// Resolve credentials and reuse the multiplexer in application code.
244+
return await GetSharedRedisConnectionMultiplexerAsync(cancellationToken);
245+
});
246+
```
247+
236248
```csharp
237249
public void ConfigureServices(IServiceCollection services)
238250
{

docs/reference/getting-started.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ services
3636

3737
Each builder extension also supports operational metadata such as a custom name, tags, a failure status, and a timeout.
3838

39+
When Redis connection setup is asynchronous, use the async multiplexer factory. The factory should return a shared
40+
`IConnectionMultiplexer`; any asynchronous connection-string or credential resolution can happen inside the factory.
41+
42+
```csharp
43+
services
44+
.AddHealthChecks()
45+
.AddRedis(async (_, cancellationToken) =>
46+
{
47+
// Resolve credentials and reuse the multiplexer in application code.
48+
return await GetSharedRedisConnectionMultiplexerAsync(cancellationToken);
49+
});
50+
```
51+
3952
```csharp
4053
services
4154
.AddHealthChecks()

src/HealthChecks.Redis/DependencyInjection/RedisHealthCheckBuilderExtensions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,35 @@ public static IHealthChecksBuilder AddRedis(
120120
tags,
121121
timeout));
122122
}
123+
124+
/// <summary>
125+
/// Add a health check for Redis services using an asynchronous connection factory.
126+
/// </summary>
127+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
128+
/// <param name="connectionMultiplexerFactory">A factory to asynchronously build the Redis connection to use.</param>
129+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'redis' will be used for the name.</param>
130+
/// <param name="failureStatus">
131+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
132+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
133+
/// </param>
134+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
135+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
136+
/// <returns>The specified <paramref name="builder"/>.</returns>
137+
public static IHealthChecksBuilder AddRedis(
138+
this IHealthChecksBuilder builder,
139+
Func<IServiceProvider, CancellationToken, Task<IConnectionMultiplexer>> connectionMultiplexerFactory,
140+
string? name = default,
141+
HealthStatus? failureStatus = default,
142+
IEnumerable<string>? tags = default,
143+
TimeSpan? timeout = default)
144+
{
145+
Guard.ThrowIfNull(connectionMultiplexerFactory);
146+
147+
return builder.Add(new HealthCheckRegistration(
148+
name ?? NAME,
149+
sp => new RedisHealthCheck(cancellationToken => connectionMultiplexerFactory(sp, cancellationToken)),
150+
failureStatus,
151+
tags,
152+
timeout));
153+
}
123154
}

src/HealthChecks.Redis/RedisHealthCheck.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class RedisHealthCheck : IHealthCheck
1313
private readonly string? _redisConnectionString;
1414
private readonly IConnectionMultiplexer? _connectionMultiplexer;
1515
private readonly Func<IConnectionMultiplexer>? _connectionMultiplexerFactory;
16+
private readonly Func<CancellationToken, Task<IConnectionMultiplexer>>? _asyncConnectionMultiplexerFactory;
1617

1718
public RedisHealthCheck(string redisConnectionString)
1819
{
@@ -36,7 +37,12 @@ public RedisHealthCheck(IConnectionMultiplexer connectionMultiplexer)
3637
/// </remarks>
3738
internal RedisHealthCheck(Func<IConnectionMultiplexer> connectionMultiplexerFactory)
3839
{
39-
_connectionMultiplexerFactory = connectionMultiplexerFactory;
40+
_connectionMultiplexerFactory = Guard.ThrowIfNull(connectionMultiplexerFactory);
41+
}
42+
43+
internal RedisHealthCheck(Func<CancellationToken, Task<IConnectionMultiplexer>> connectionMultiplexerFactory)
44+
{
45+
_asyncConnectionMultiplexerFactory = Guard.ThrowIfNull(connectionMultiplexerFactory);
4046
}
4147

4248
/// <inheritdoc />
@@ -46,6 +52,19 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
4652
{
4753
IConnectionMultiplexer? connection = _connectionMultiplexer ?? _connectionMultiplexerFactory?.Invoke();
4854

55+
if (connection is null && _asyncConnectionMultiplexerFactory is not null)
56+
{
57+
try
58+
{
59+
connection = Guard.ThrowIfNull(
60+
await _asyncConnectionMultiplexerFactory(cancellationToken).ConfigureAwait(false));
61+
}
62+
catch (OperationCanceledException)
63+
{
64+
return new HealthCheckResult(context.Registration.FailureStatus, description: "Healthcheck timed out");
65+
}
66+
}
67+
4968
if (_redisConnectionString is not null && !_connections.TryGetValue(_redisConnectionString, out connection))
5069
{
5170
try

test/HealthChecks.Redis.Tests/DependencyInjection/RegistrationTests.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,68 @@ public void add_health_check_with_connection_multiplexer_when_properly_configure
109109
// the factory is called when it's used for the first time, as it can throw
110110
factoryCalled.ShouldBeFalse();
111111
}
112-
}
112+
113+
[Fact]
114+
public async Task add_health_check_with_async_connection_multiplexer_factory_when_properly_configured()
115+
{
116+
var connectionMultiplexer = Substitute.For<IConnectionMultiplexer>();
117+
connectionMultiplexer.GetEndPoints(configuredOnly: true).Returns([]);
118+
119+
var services = new ServiceCollection();
120+
var factoryCalled = false;
121+
CancellationToken capturedCancellationToken = default;
122+
123+
services.AddHealthChecks()
124+
.AddRedis(async (_, cancellationToken) =>
125+
{
126+
factoryCalled = true;
127+
capturedCancellationToken = cancellationToken;
128+
await Task.Yield();
129+
return connectionMultiplexer;
130+
});
131+
132+
using var serviceProvider = services.BuildServiceProvider();
133+
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
134+
135+
var registration = options.Value.Registrations.First();
136+
var check = registration.Factory(serviceProvider);
137+
138+
registration.Name.ShouldBe("redis");
139+
check.ShouldBeOfType<RedisHealthCheck>();
140+
factoryCalled.ShouldBeFalse();
141+
142+
using var cancellationTokenSource = new CancellationTokenSource();
143+
var result = await check.CheckHealthAsync(
144+
new HealthCheckContext { Registration = registration },
145+
cancellationTokenSource.Token);
146+
147+
result.Status.ShouldBe(HealthStatus.Healthy);
148+
factoryCalled.ShouldBeTrue();
149+
capturedCancellationToken.ShouldBe(cancellationTokenSource.Token);
150+
}
151+
152+
[Fact]
153+
public async Task return_timeout_when_async_connection_multiplexer_factory_is_cancelled()
154+
{
155+
using var cancellationTokenSource = new CancellationTokenSource();
156+
cancellationTokenSource.Cancel();
157+
158+
var services = new ServiceCollection();
159+
services.AddHealthChecks()
160+
.AddRedis((_, cancellationToken) =>
161+
Task.FromCanceled<IConnectionMultiplexer>(cancellationToken));
162+
163+
using var serviceProvider = services.BuildServiceProvider();
164+
var registration = serviceProvider
165+
.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
166+
.Value.Registrations.First();
167+
var check = registration.Factory(serviceProvider);
168+
169+
var result = await check.CheckHealthAsync(
170+
new HealthCheckContext { Registration = registration },
171+
cancellationTokenSource.Token);
172+
173+
result.Status.ShouldBe(HealthStatus.Unhealthy);
174+
result.Description.ShouldBe("Healthcheck timed out");
175+
}
176+
}

test/HealthChecks.Redis.Tests/HealthChecks.Redis.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection
1414
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRedis(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, StackExchange.Redis.IConnectionMultiplexer connectionMultiplexer, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
1515
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRedis(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func<System.IServiceProvider, StackExchange.Redis.IConnectionMultiplexer> connectionMultiplexerFactory, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
1616
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRedis(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func<System.IServiceProvider, string> connectionStringFactory, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
17+
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRedis(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func<System.IServiceProvider, System.Threading.CancellationToken, System.Threading.Tasks.Task<StackExchange.Redis.IConnectionMultiplexer>> connectionMultiplexerFactory, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
1718
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRedis(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string redisConnectionString, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
1819
}
1920
}

0 commit comments

Comments
 (0)