Skip to content

Commit

Permalink
Remove private static cache from RabbitMQHealthCheck (#2343)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
eerhardt authored Dec 13, 2024
1 parent c291a6a commit b3e8417
Show file tree
Hide file tree
Showing 14 changed files with 805 additions and 436 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using HealthChecks.RabbitMQ;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using RabbitMQ.Client;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods to configure <see cref="RabbitMQHealthCheck"/>.
/// </summary>
public static class RabbitMQHealthCheckBuilderExtensions
{
private const string NAME = "rabbitmq";

/// <summary>
/// Add a health check for RabbitMQ services using connection string (amqp uri).
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
string rabbitConnectionString,
SslOption? sslOption = default,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return builder.AddRabbitMQ(new Uri(rabbitConnectionString), sslOption, name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a health check for RabbitMQ services using connection string (amqp uri).
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Uri rabbitConnectionString,
SslOption? sslOption = default,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions
{
ConnectionUri = rabbitConnectionString,
Ssl = sslOption
};

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
new RabbitMQHealthCheck(options),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services using <see cref="IConnection"/> from service provider
/// or <see cref="IConnectionFactory"/> from service provider if none is found. At least one must be configured.
/// </summary>
/// <remarks>
/// This method shouldn't be called more than once.
/// Each subsequent call will create a new connection, which overrides the previous ones.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
builder.Services.AddSingleton(sp =>
{
var connection = sp.GetService<IConnection>();
var connectionFactory = sp.GetService<IConnectionFactory>();

if (connection != null)
{
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { Connection = connection });
}
else if (connectionFactory != null)
{
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { ConnectionFactory = connectionFactory });
}
else
{
throw new ArgumentException($"Either an IConnection or IConnectionFactory must be registered with the service provider");
}
});

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => sp.GetRequiredService<RabbitMQHealthCheck>(),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services.
/// </summary>
/// <remarks>
/// <paramref name="setup"/> will be called each time the healthcheck route is requested. However
/// the created <see cref="IConnection"/> will be reused.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action to configure the RabbitMQ setup.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Action<RabbitMQHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions();
setup?.Invoke(options);

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
new RabbitMQHealthCheck(options),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services.
/// </summary>
/// <remarks>
/// <paramref name="setup"/> will be called the first time the healthcheck route is requested. However
/// the created <see cref="IConnection"/> will be reused.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action to configure the RabbitMQ setup with <see cref="IServiceProvider"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Action<IServiceProvider, RabbitMQHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions();

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp =>
{
if (!options.AlreadyConfiguredByHealthCheckRegistrationCall)
{
setup?.Invoke(sp, options);
options.AlreadyConfiguredByHealthCheckRegistrationCall = true;
}

return new RabbitMQHealthCheck(options);
},
failureStatus,
tags,
timeout));
}
}
4 changes: 0 additions & 4 deletions src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
<PackageReference Include="RabbitMQ.Client" VersionOverride="[6.8.1,7.0.0)" />

<Compile Include="../HealthCheckResultTask.cs" />
<Compile Include="../HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" Link="DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" />
<Compile Include="../HealthChecks.Rabbitmq/RabbitMQHealthCheckOptions.cs" />

<None Include="../HealthChecks.Rabbitmq/README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
91 changes: 91 additions & 0 deletions src/HealthChecks.Rabbitmq.v6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# RabbitMQ Health Check

This health check verifies the ability to communicate with a RabbitMQ server

## Example Usage

With all of the following examples, you can additionally add the following parameters:

- `name`: The health check name. Default if not specified is `rabbitmq`.
- `failureStatus`: The `HealthStatus` that should be reported when the health check fails. Default is `HealthStatus.Unhealthy`.
- `tags`: A list of tags that can be used to filter sets of health checks.
- `timeout`: A `System.TimeSpan` representing the timeout of the check.

### Basic

This will create a new `IConnection` and reuse on every request to get the health check result. Use
the extension method where you provide the `Uri` to connect with. You can optionally set the `SslOption` if needed.
IConnection created with this option use UseBackgroundThreadsForIO by default in order to gracefully shutdown on non reference IConnection by ServiceCollection.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHealthChecks()
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host1/vhost")
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host2/vhost");
}
```

### Dependency Injected `IConnection`

As per [RabbitMQ docs](https://www.rabbitmq.com/connections.html) and its suggestions on
[high connectivity churn](https://www.rabbitmq.com/networking.html#dealing-with-high-connection-churn), connections are meant to be long lived.
Ideally, this should be configured as a singleton.

If you are sharing a single connection for every time a health check is requested,
you must ensure automatic recovery is enable so that the connection can be re-established if lost.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
};
return factory.CreateConnection();
})
.AddHealthChecks()
.AddRabbitMQ();
}
```

Alternatively, you can specify the connection to use with a factory function given the `IServiceProvider`.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHealthChecks()
.AddRabbitMQ(sp =>
{
var factory = new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
};
return factory.CreateConnection();
});
}
```

Or you register IConnectionFactory and then the healthcheck will create a single connection for that one.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IConnectionFactory>(sp =>
new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
})
.AddHealthChecks()
.AddRabbitMQ();
}
```
Loading

0 comments on commit b3e8417

Please sign in to comment.