Skip to content

Commit b3e8417

Browse files
authored
Remove private static cache from RabbitMQHealthCheck (#2343)
* Remove private static cache from RabbitMQHealthCheck RabbitMQHealthCheck is maintaining it's private static cache of instances. It's wrong, as it can cause a memory leak (it won't ever be freed). Moreover, it's can create instances of IConnection. This is wrong, as it can lead into a situation when we have multiple instances of IConnection that are connected to the same server: one used by the app and another created and used by the health check. And we should have only one. We should do the same as in #2040, #2116 and #2096 I left RabbitMQ.v6 as-is to limit the breaking change. Only when an existing user moves from RabbitMQ.Client v6 to v7 will they need to update their health check (along with other breaking changes in RabbitMQ.Client). * Add test when no IConnection is registered in DI.
1 parent c291a6a commit b3e8417

File tree

14 files changed

+805
-436
lines changed

14 files changed

+805
-436
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using HealthChecks.RabbitMQ;
2+
using Microsoft.Extensions.Diagnostics.HealthChecks;
3+
using RabbitMQ.Client;
4+
5+
namespace Microsoft.Extensions.DependencyInjection;
6+
7+
/// <summary>
8+
/// Extension methods to configure <see cref="RabbitMQHealthCheck"/>.
9+
/// </summary>
10+
public static class RabbitMQHealthCheckBuilderExtensions
11+
{
12+
private const string NAME = "rabbitmq";
13+
14+
/// <summary>
15+
/// Add a health check for RabbitMQ services using connection string (amqp uri).
16+
/// </summary>
17+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
18+
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
19+
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
20+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
21+
/// <param name="failureStatus">
22+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
23+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
24+
/// </param>
25+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
26+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
27+
/// <returns>The specified <paramref name="builder"/>.</returns>
28+
public static IHealthChecksBuilder AddRabbitMQ(
29+
this IHealthChecksBuilder builder,
30+
string rabbitConnectionString,
31+
SslOption? sslOption = default,
32+
string? name = default,
33+
HealthStatus? failureStatus = default,
34+
IEnumerable<string>? tags = default,
35+
TimeSpan? timeout = default)
36+
{
37+
return builder.AddRabbitMQ(new Uri(rabbitConnectionString), sslOption, name, failureStatus, tags, timeout);
38+
}
39+
40+
/// <summary>
41+
/// Add a health check for RabbitMQ services using connection string (amqp uri).
42+
/// </summary>
43+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
44+
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
45+
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
46+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
47+
/// <param name="failureStatus">
48+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
49+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
50+
/// </param>
51+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
52+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
53+
/// <returns>The specified <paramref name="builder"/>.</returns>
54+
public static IHealthChecksBuilder AddRabbitMQ(
55+
this IHealthChecksBuilder builder,
56+
Uri rabbitConnectionString,
57+
SslOption? sslOption = default,
58+
string? name = default,
59+
HealthStatus? failureStatus = default,
60+
IEnumerable<string>? tags = default,
61+
TimeSpan? timeout = default)
62+
{
63+
var options = new RabbitMQHealthCheckOptions
64+
{
65+
ConnectionUri = rabbitConnectionString,
66+
Ssl = sslOption
67+
};
68+
69+
return builder.Add(new HealthCheckRegistration(
70+
name ?? NAME,
71+
new RabbitMQHealthCheck(options),
72+
failureStatus,
73+
tags,
74+
timeout));
75+
}
76+
77+
/// <summary>
78+
/// Add a health check for RabbitMQ services using <see cref="IConnection"/> from service provider
79+
/// or <see cref="IConnectionFactory"/> from service provider if none is found. At least one must be configured.
80+
/// </summary>
81+
/// <remarks>
82+
/// This method shouldn't be called more than once.
83+
/// Each subsequent call will create a new connection, which overrides the previous ones.
84+
/// </remarks>
85+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
86+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
87+
/// <param name="failureStatus">
88+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
89+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
90+
/// </param>
91+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
92+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
93+
/// <returns>The specified <paramref name="builder"/>.</returns>
94+
public static IHealthChecksBuilder AddRabbitMQ(
95+
this IHealthChecksBuilder builder,
96+
string? name = default,
97+
HealthStatus? failureStatus = default,
98+
IEnumerable<string>? tags = default,
99+
TimeSpan? timeout = default)
100+
{
101+
builder.Services.AddSingleton(sp =>
102+
{
103+
var connection = sp.GetService<IConnection>();
104+
var connectionFactory = sp.GetService<IConnectionFactory>();
105+
106+
if (connection != null)
107+
{
108+
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { Connection = connection });
109+
}
110+
else if (connectionFactory != null)
111+
{
112+
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { ConnectionFactory = connectionFactory });
113+
}
114+
else
115+
{
116+
throw new ArgumentException($"Either an IConnection or IConnectionFactory must be registered with the service provider");
117+
}
118+
});
119+
120+
return builder.Add(new HealthCheckRegistration(
121+
name ?? NAME,
122+
sp => sp.GetRequiredService<RabbitMQHealthCheck>(),
123+
failureStatus,
124+
tags,
125+
timeout));
126+
}
127+
128+
/// <summary>
129+
/// Add a health check for RabbitMQ services.
130+
/// </summary>
131+
/// <remarks>
132+
/// <paramref name="setup"/> will be called each time the healthcheck route is requested. However
133+
/// the created <see cref="IConnection"/> will be reused.
134+
/// </remarks>
135+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
136+
/// <param name="setup">The action to configure the RabbitMQ setup.</param>
137+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
138+
/// <param name="failureStatus">
139+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
140+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
141+
/// </param>
142+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
143+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
144+
/// <returns>The specified <paramref name="builder"/>.</returns>
145+
public static IHealthChecksBuilder AddRabbitMQ(
146+
this IHealthChecksBuilder builder,
147+
Action<RabbitMQHealthCheckOptions>? setup,
148+
string? name = default,
149+
HealthStatus? failureStatus = default,
150+
IEnumerable<string>? tags = default,
151+
TimeSpan? timeout = default)
152+
{
153+
var options = new RabbitMQHealthCheckOptions();
154+
setup?.Invoke(options);
155+
156+
return builder.Add(new HealthCheckRegistration(
157+
name ?? NAME,
158+
new RabbitMQHealthCheck(options),
159+
failureStatus,
160+
tags,
161+
timeout));
162+
}
163+
164+
/// <summary>
165+
/// Add a health check for RabbitMQ services.
166+
/// </summary>
167+
/// <remarks>
168+
/// <paramref name="setup"/> will be called the first time the healthcheck route is requested. However
169+
/// the created <see cref="IConnection"/> will be reused.
170+
/// </remarks>
171+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
172+
/// <param name="setup">The action to configure the RabbitMQ setup with <see cref="IServiceProvider"/>.</param>
173+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
174+
/// <param name="failureStatus">
175+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
176+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
177+
/// </param>
178+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
179+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
180+
/// <returns>The specified <paramref name="builder"/>.</returns>
181+
public static IHealthChecksBuilder AddRabbitMQ(
182+
this IHealthChecksBuilder builder,
183+
Action<IServiceProvider, RabbitMQHealthCheckOptions>? setup,
184+
string? name = default,
185+
HealthStatus? failureStatus = default,
186+
IEnumerable<string>? tags = default,
187+
TimeSpan? timeout = default)
188+
{
189+
var options = new RabbitMQHealthCheckOptions();
190+
191+
return builder.Add(new HealthCheckRegistration(
192+
name ?? NAME,
193+
sp =>
194+
{
195+
if (!options.AlreadyConfiguredByHealthCheckRegistrationCall)
196+
{
197+
setup?.Invoke(sp, options);
198+
options.AlreadyConfiguredByHealthCheckRegistrationCall = true;
199+
}
200+
201+
return new RabbitMQHealthCheck(options);
202+
},
203+
failureStatus,
204+
tags,
205+
timeout));
206+
}
207+
}

src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
<PackageReference Include="RabbitMQ.Client" VersionOverride="[6.8.1,7.0.0)" />
1515

1616
<Compile Include="../HealthCheckResultTask.cs" />
17-
<Compile Include="../HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" Link="DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" />
18-
<Compile Include="../HealthChecks.Rabbitmq/RabbitMQHealthCheckOptions.cs" />
19-
20-
<None Include="../HealthChecks.Rabbitmq/README.md" Pack="true" PackagePath="\" />
2117
</ItemGroup>
2218

2319
</Project>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# RabbitMQ Health Check
2+
3+
This health check verifies the ability to communicate with a RabbitMQ server
4+
5+
## Example Usage
6+
7+
With all of the following examples, you can additionally add the following parameters:
8+
9+
- `name`: The health check name. Default if not specified is `rabbitmq`.
10+
- `failureStatus`: The `HealthStatus` that should be reported when the health check fails. Default is `HealthStatus.Unhealthy`.
11+
- `tags`: A list of tags that can be used to filter sets of health checks.
12+
- `timeout`: A `System.TimeSpan` representing the timeout of the check.
13+
14+
### Basic
15+
16+
This will create a new `IConnection` and reuse on every request to get the health check result. Use
17+
the extension method where you provide the `Uri` to connect with. You can optionally set the `SslOption` if needed.
18+
IConnection created with this option use UseBackgroundThreadsForIO by default in order to gracefully shutdown on non reference IConnection by ServiceCollection.
19+
20+
```csharp
21+
public void ConfigureServices(IServiceCollection services)
22+
{
23+
services
24+
.AddHealthChecks()
25+
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host1/vhost")
26+
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host2/vhost");
27+
}
28+
```
29+
30+
### Dependency Injected `IConnection`
31+
32+
As per [RabbitMQ docs](https://www.rabbitmq.com/connections.html) and its suggestions on
33+
[high connectivity churn](https://www.rabbitmq.com/networking.html#dealing-with-high-connection-churn), connections are meant to be long lived.
34+
Ideally, this should be configured as a singleton.
35+
36+
If you are sharing a single connection for every time a health check is requested,
37+
you must ensure automatic recovery is enable so that the connection can be re-established if lost.
38+
39+
```csharp
40+
public void ConfigureServices(IServiceCollection services)
41+
{
42+
services
43+
.AddSingleton<IConnection>(sp =>
44+
{
45+
var factory = new ConnectionFactory
46+
{
47+
Uri = new Uri("amqps://user:pass@host/vhost"),
48+
AutomaticRecoveryEnabled = true
49+
};
50+
return factory.CreateConnection();
51+
})
52+
.AddHealthChecks()
53+
.AddRabbitMQ();
54+
}
55+
```
56+
57+
Alternatively, you can specify the connection to use with a factory function given the `IServiceProvider`.
58+
59+
```csharp
60+
public void ConfigureServices(IServiceCollection services)
61+
{
62+
services
63+
.AddHealthChecks()
64+
.AddRabbitMQ(sp =>
65+
{
66+
var factory = new ConnectionFactory
67+
{
68+
Uri = new Uri("amqps://user:pass@host/vhost"),
69+
AutomaticRecoveryEnabled = true
70+
};
71+
return factory.CreateConnection();
72+
});
73+
}
74+
```
75+
76+
Or you register IConnectionFactory and then the healthcheck will create a single connection for that one.
77+
78+
```csharp
79+
public void ConfigureServices(IServiceCollection services)
80+
{
81+
services
82+
.AddSingleton<IConnectionFactory>(sp =>
83+
new ConnectionFactory
84+
{
85+
Uri = new Uri("amqps://user:pass@host/vhost"),
86+
AutomaticRecoveryEnabled = true
87+
})
88+
.AddHealthChecks()
89+
.AddRabbitMQ();
90+
}
91+
```

0 commit comments

Comments
 (0)