From b3e8417a092c0ca240458dfd05b181f4c55f4fd8 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 13 Dec 2024 12:36:40 -0600 Subject: [PATCH] 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. --- .../RabbitMQHealthCheckBuilderExtensions.cs | 207 ++++++++++++ .../HealthChecks.Rabbitmq.v6.csproj | 4 - src/HealthChecks.Rabbitmq.v6/README.md | 91 ++++++ .../RabbitMQHealthCheckOptions.cs | 0 .../RabbitMQHealthCheckBuilderExtensions.cs | 158 ++------- .../HealthChecks.Rabbitmq.csproj | 1 - src/HealthChecks.Rabbitmq/README.md | 62 +--- .../RabbitMQHealthCheck.cs | 95 +----- .../DependencyInjection/RegistrationTests.cs | 38 +-- .../Functional/RabbitHealthCheckTests.cs | 157 +++------ .../HealthChecks.Rabbitmq.approved.txt | 19 +- .../DependencyInjection/RegistrationTests.cs | 104 ++++++ .../Functional/RabbitHealthCheckTests.cs | 301 ++++++++++++++++++ .../HealthChecks.RabbitMQ.v6.Tests.csproj | 4 - 14 files changed, 805 insertions(+), 436 deletions(-) create mode 100644 src/HealthChecks.Rabbitmq.v6/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs create mode 100644 src/HealthChecks.Rabbitmq.v6/README.md rename src/{HealthChecks.Rabbitmq => HealthChecks.Rabbitmq.v6}/RabbitMQHealthCheckOptions.cs (100%) create mode 100644 test/HealthChecks.RabbitMQ.v6.Tests/DependencyInjection/RegistrationTests.cs create mode 100644 test/HealthChecks.RabbitMQ.v6.Tests/Functional/RabbitHealthCheckTests.cs diff --git a/src/HealthChecks.Rabbitmq.v6/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs b/src/HealthChecks.Rabbitmq.v6/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs new file mode 100644 index 0000000000..3b4b1ad61b --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v6/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs @@ -0,0 +1,207 @@ +using HealthChecks.RabbitMQ; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using RabbitMQ.Client; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure . +/// +public static class RabbitMQHealthCheckBuilderExtensions +{ + private const string NAME = "rabbitmq"; + + /// + /// Add a health check for RabbitMQ services using connection string (amqp uri). + /// + /// The . + /// The RabbitMQ connection string to be used. + /// The RabbitMQ ssl options. Optional. If null, the ssl option will counted as disabled and not used. + /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRabbitMQ( + this IHealthChecksBuilder builder, + string rabbitConnectionString, + SslOption? sslOption = default, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.AddRabbitMQ(new Uri(rabbitConnectionString), sslOption, name, failureStatus, tags, timeout); + } + + /// + /// Add a health check for RabbitMQ services using connection string (amqp uri). + /// + /// The . + /// The RabbitMQ connection string to be used. + /// The RabbitMQ ssl options. Optional. If null, the ssl option will counted as disabled and not used. + /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRabbitMQ( + this IHealthChecksBuilder builder, + Uri rabbitConnectionString, + SslOption? sslOption = default, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? 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)); + } + + /// + /// Add a health check for RabbitMQ services using from service provider + /// or from service provider if none is found. At least one must be configured. + /// + /// + /// This method shouldn't be called more than once. + /// Each subsequent call will create a new connection, which overrides the previous ones. + /// + /// The . + /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRabbitMQ( + this IHealthChecksBuilder builder, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + builder.Services.AddSingleton(sp => + { + var connection = sp.GetService(); + var connectionFactory = sp.GetService(); + + 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(), + failureStatus, + tags, + timeout)); + } + + /// + /// Add a health check for RabbitMQ services. + /// + /// + /// will be called each time the healthcheck route is requested. However + /// the created will be reused. + /// + /// The . + /// The action to configure the RabbitMQ setup. + /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRabbitMQ( + this IHealthChecksBuilder builder, + Action? setup, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? 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)); + } + + /// + /// Add a health check for RabbitMQ services. + /// + /// + /// will be called the first time the healthcheck route is requested. However + /// the created will be reused. + /// + /// The . + /// The action to configure the RabbitMQ setup with . + /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRabbitMQ( + this IHealthChecksBuilder builder, + Action? setup, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? 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)); + } +} diff --git a/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj b/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj index de95041214..84982f03a2 100644 --- a/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj +++ b/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj @@ -14,10 +14,6 @@ - - - - diff --git a/src/HealthChecks.Rabbitmq.v6/README.md b/src/HealthChecks.Rabbitmq.v6/README.md new file mode 100644 index 0000000000..d95a46adbd --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v6/README.md @@ -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(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(sp => + new ConnectionFactory + { + Uri = new Uri("amqps://user:pass@host/vhost"), + AutomaticRecoveryEnabled = true + }) + .AddHealthChecks() + .AddRabbitMQ(); +} +``` diff --git a/src/HealthChecks.Rabbitmq/RabbitMQHealthCheckOptions.cs b/src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheckOptions.cs similarity index 100% rename from src/HealthChecks.Rabbitmq/RabbitMQHealthCheckOptions.cs rename to src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheckOptions.cs diff --git a/src/HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs b/src/HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs index 2ad58dec9c..9082222c2b 100644 --- a/src/HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs +++ b/src/HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs @@ -11,129 +11,14 @@ public static class RabbitMQHealthCheckBuilderExtensions { private const string NAME = "rabbitmq"; - /// - /// Add a health check for RabbitMQ services using connection string (amqp uri). - /// - /// The . - /// The RabbitMQ connection string to be used. - /// The RabbitMQ ssl options. Optional. If null, the ssl option will counted as disabled and not used. - /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. - /// - /// The that should be reported when the health check fails. Optional. If null then - /// the default status of will be reported. - /// - /// A list of tags that can be used to filter sets of health checks. Optional. - /// An optional representing the timeout of the check. - /// The specified . - public static IHealthChecksBuilder AddRabbitMQ( - this IHealthChecksBuilder builder, - string rabbitConnectionString, - SslOption? sslOption = default, - string? name = default, - HealthStatus? failureStatus = default, - IEnumerable? tags = default, - TimeSpan? timeout = default) - { - return builder.AddRabbitMQ(new Uri(rabbitConnectionString), sslOption, name, failureStatus, tags, timeout); - } - - /// - /// Add a health check for RabbitMQ services using connection string (amqp uri). - /// - /// The . - /// The RabbitMQ connection string to be used. - /// The RabbitMQ ssl options. Optional. If null, the ssl option will counted as disabled and not used. - /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. - /// - /// The that should be reported when the health check fails. Optional. If null then - /// the default status of will be reported. - /// - /// A list of tags that can be used to filter sets of health checks. Optional. - /// An optional representing the timeout of the check. - /// The specified . - public static IHealthChecksBuilder AddRabbitMQ( - this IHealthChecksBuilder builder, - Uri rabbitConnectionString, - SslOption? sslOption = default, - string? name = default, - HealthStatus? failureStatus = default, - IEnumerable? 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)); - } - - /// - /// Add a health check for RabbitMQ services using from service provider - /// or from service provider if none is found. At least one must be configured. - /// - /// - /// This method shouldn't be called more than once. - /// Each subsequent call will create a new connection, which overrides the previous ones. - /// - /// The . - /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. - /// - /// The that should be reported when the health check fails. Optional. If null then - /// the default status of will be reported. - /// - /// A list of tags that can be used to filter sets of health checks. Optional. - /// An optional representing the timeout of the check. - /// The specified . - public static IHealthChecksBuilder AddRabbitMQ( - this IHealthChecksBuilder builder, - string? name = default, - HealthStatus? failureStatus = default, - IEnumerable? tags = default, - TimeSpan? timeout = default) - { - builder.Services.AddSingleton(sp => - { - var connection = sp.GetService(); - var connectionFactory = sp.GetService(); - - 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(), - failureStatus, - tags, - timeout)); - } - /// /// Add a health check for RabbitMQ services. /// /// - /// will be called each time the healthcheck route is requested. However - /// the created will be reused. + /// will be called each time the healthcheck route is requested. /// /// The . - /// The action to configure the RabbitMQ setup. + /// The optional factory method to get the RabbitMQ connection from the . /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. /// /// The that should be reported when the health check fails. Optional. If null then @@ -144,32 +29,38 @@ public static IHealthChecksBuilder AddRabbitMQ( /// The specified . public static IHealthChecksBuilder AddRabbitMQ( this IHealthChecksBuilder builder, - Action? setup, + Func? factory = null, string? name = default, HealthStatus? failureStatus = default, IEnumerable? tags = default, TimeSpan? timeout = default) { - var options = new RabbitMQHealthCheckOptions(); - setup?.Invoke(options); - return builder.Add(new HealthCheckRegistration( name ?? NAME, - new RabbitMQHealthCheck(options), + sp => + { + return new RabbitMQHealthCheck(sp, sp => + { + // Pass the factory to RabbitMQHealthCheck to ensure the factory is called + // within CheckHealthAsync. HealthCheckRegistration.Factory should not throw + // exceptions, which can happen in the factory or getting the service. + var connection = factory?.Invoke(sp) ?? sp.GetRequiredService(); + return Task.FromResult(connection); + }); + }, failureStatus, tags, timeout)); } /// - /// Add a health check for RabbitMQ services. + /// Add a health check for RabbitMQ services allowing for an asynchronous connection factory. /// /// - /// will be called each time the healthcheck route is requested. However - /// the created will be reused. + /// will be called each time the healthcheck route is requested. /// /// The . - /// The action to configure the RabbitMQ setup with . + /// The asynchronous factory method to get the RabbitMQ connection from the . /// The health check name. Optional. If null the type name 'rabbitmq' will be used for the name. /// /// The that should be reported when the health check fails. Optional. If null then @@ -180,26 +71,15 @@ public static IHealthChecksBuilder AddRabbitMQ( /// The specified . public static IHealthChecksBuilder AddRabbitMQ( this IHealthChecksBuilder builder, - Action? setup, + Func> factory, string? name = default, HealthStatus? failureStatus = default, IEnumerable? 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); - }, + sp => new RabbitMQHealthCheck(sp, factory), failureStatus, tags, timeout)); diff --git a/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj index 2d2837aabb..cb31621a95 100644 --- a/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj +++ b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj @@ -10,7 +10,6 @@ - diff --git a/src/HealthChecks.Rabbitmq/README.md b/src/HealthChecks.Rabbitmq/README.md index d95a46adbd..29ca0e2e5d 100644 --- a/src/HealthChecks.Rabbitmq/README.md +++ b/src/HealthChecks.Rabbitmq/README.md @@ -11,30 +11,11 @@ With all of the following examples, you can additionally add the following param - `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. +Ideally, this should be configured as a singleton. The health check should use the same IConnection instance that is used in the application. ```csharp public void ConfigureServices(IServiceCollection services) @@ -45,47 +26,38 @@ public void ConfigureServices(IServiceCollection services) var factory = new ConnectionFactory { Uri = new Uri("amqps://user:pass@host/vhost"), - AutomaticRecoveryEnabled = true }; - return factory.CreateConnection(); + return factory.CreateConnectionAsync().GetAwaiter().GetResult(); }) .AddHealthChecks() .AddRabbitMQ(); } ``` -Alternatively, you can specify the connection to use with a factory function given the `IServiceProvider`. +### Caching IConnection outside of Dependency Injection + +Alternatively, you can create the connection outside of the dependency injection container and use it in the health check. ```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(); - }); + .AddRabbitMQ(sp => connectionTask.Value); } -``` -Or you register IConnectionFactory and then the healthcheck will create a single connection for that one. +private static readonly Lazy> connectionTask = new Lazy>(CreateConnection); -```csharp -public void ConfigureServices(IServiceCollection services) +private static async Task CreateConnection() { - services - .AddSingleton(sp => - new ConnectionFactory - { - Uri = new Uri("amqps://user:pass@host/vhost"), - AutomaticRecoveryEnabled = true - }) - .AddHealthChecks() - .AddRabbitMQ(); + var factory = new ConnectionFactory + { + Uri = new Uri("amqps://user:pass@host/vhost"), + }; + return await factory.CreateConnectionAsync(); } ``` + +### Breaking changes + +`RabbitMQHealthCheck` was letting the users specify how `IConnection` should be created (from raw connection string or from `Uri` or from `IConnectionFactory`), at a cost of maintaining an internal, static client instances cache. Now the type does not create client instances nor maintain an internal cache and **it's the caller responsibility to provide the instance of `IConnection`** (please see [#2048](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2148) for more details). Since RabbitMQ [recommends](https://www.rabbitmq.com/client-libraries/dotnet-api-guide#connection-and-channel-lifespan) reusing client instances since they can be expensive to create, it's recommended to register a singleton factory method for `IConnection`. So the client is created only when needed and once per whole application lifetime. diff --git a/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs b/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs index 4de8e996f8..348a693160 100644 --- a/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs +++ b/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Microsoft.Extensions.Diagnostics.HealthChecks; using RabbitMQ.Client; @@ -9,20 +8,19 @@ namespace HealthChecks.RabbitMQ; /// public class RabbitMQHealthCheck : IHealthCheck { - private static readonly ConcurrentDictionary> _connections = new(); + private readonly IConnection? _connection; + private readonly IServiceProvider? _serviceProvider; + private readonly Func>? _factory; - private IConnection? _connection; - private readonly RabbitMQHealthCheckOptions _options; - - public RabbitMQHealthCheck(RabbitMQHealthCheckOptions options) + public RabbitMQHealthCheck(IConnection connection) { - _options = Guard.ThrowIfNull(options); - _connection = options.Connection; + _connection = Guard.ThrowIfNull(connection); + } - if (_connection is null && _options.ConnectionFactory is null && _options.ConnectionUri is null) - { - throw new ArgumentException("A connection, connection factory, or connection string must be set!", nameof(options)); - } + public RabbitMQHealthCheck(IServiceProvider serviceProvider, Func> factory) + { + _serviceProvider = Guard.ThrowIfNull(serviceProvider); + _factory = Guard.ThrowIfNull(factory); } /// @@ -30,7 +28,8 @@ public async Task CheckHealthAsync(HealthCheckContext context { try { - var connection = await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + var connection = _connection ?? await _factory!(_serviceProvider!).ConfigureAwait(false); + await using var model = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false); return HealthCheckResult.Healthy(); @@ -40,74 +39,4 @@ public async Task CheckHealthAsync(HealthCheckContext context return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); } } - - private async Task EnsureConnectionAsync(CancellationToken cancellationToken) => - _connection ??= await _connections.GetOrAddAsync(_options, async options => - { - var factory = options.ConnectionFactory; - - if (factory is null) - { - Guard.ThrowIfNull(options.ConnectionUri); - factory = new ConnectionFactory - { - Uri = options.ConnectionUri, - AutomaticRecoveryEnabled = true - }; - - if (options.RequestedConnectionTimeout is not null) - { - ((ConnectionFactory)factory).RequestedConnectionTimeout = options.RequestedConnectionTimeout.Value; - } - - if (options.Ssl is not null) - { - ((ConnectionFactory)factory).Ssl = options.Ssl; - } - } - - return await factory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); -} - -internal static class ConcurrentDictionaryExtensions -{ - /// - /// Provides an alternative to specifically for asynchronous values. The factory method will only run once. - /// - public static async Task GetOrAddAsync( - this ConcurrentDictionary> dictionary, - TKey key, - Func> valueFactory) where TKey : notnull - { - while (true) - { - if (dictionary.TryGetValue(key, out var task)) - { - return await task.ConfigureAwait(false); - } - - // This is the task that we'll return to all waiters. We'll complete it when the factory is complete - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - if (dictionary.TryAdd(key, tcs.Task)) - { - try - { - var value = await valueFactory(key).ConfigureAwait(false); - tcs.TrySetResult(value); - return await tcs.Task.ConfigureAwait(false); - } - catch (Exception ex) - { - // Make sure all waiters see the exception - tcs.SetException(ex); - - // We remove the entry if the factory failed so it's not a permanent failure - // and future gets can retry (this could be a pluggable policy) - dictionary.TryRemove(key, out _); - throw; - } - } - } - } } diff --git a/test/HealthChecks.RabbitMQ.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.RabbitMQ.Tests/DependencyInjection/RegistrationTests.cs index bec2f371b2..435919b6af 100644 --- a/test/HealthChecks.RabbitMQ.Tests/DependencyInjection/RegistrationTests.cs +++ b/test/HealthChecks.RabbitMQ.Tests/DependencyInjection/RegistrationTests.cs @@ -1,3 +1,5 @@ +using RabbitMQ.Client; + namespace HealthChecks.RabbitMQ.Tests.DependencyInjection; public class rabbitmq_registration_should @@ -10,7 +12,7 @@ public void add_health_check_when_properly_configured() { var services = new ServiceCollection(); services.AddHealthChecks() - .AddRabbitMQ(rabbitConnectionString: FAKE_CONNECTION_STRING); + .AddRabbitMQ(); using var serviceProvider = services.BuildServiceProvider(); var options = serviceProvider.GetRequiredService>(); @@ -29,7 +31,7 @@ public void add_named_health_check_when_properly_configured() var customCheckName = "my-" + DEFAULT_CHECK_NAME; services.AddHealthChecks() - .AddRabbitMQ(FAKE_CONNECTION_STRING, name: customCheckName); + .AddRabbitMQ(name: customCheckName); using var serviceProvider = services.BuildServiceProvider(); var options = serviceProvider.GetRequiredService>(); @@ -52,39 +54,11 @@ public void add_named_health_check_with_connection_string_factory_by_iServicePro }); services.AddHealthChecks() - .AddRabbitMQ((sp, options) => + .AddRabbitMQ(sp => { var connectionString = sp.GetRequiredService().ConnectionString; - options.ConnectionUri = new Uri(connectionString); - }, name: customCheckName); - - using var serviceProvider = services.BuildServiceProvider(); - var options = serviceProvider.GetRequiredService>(); - - var registration = options.Value.Registrations.First(); - var check = registration.Factory(serviceProvider); - - registration.Name.ShouldBe(customCheckName); - check.ShouldBeOfType(); - } - - [Fact] - public void add_named_health_check_with_uri_string_factory_by_iServiceProvider_registered() - { - var services = new ServiceCollection(); - var customCheckName = "my-" + DEFAULT_CHECK_NAME; - services.AddSingleton(new RabbitMqSetting - { - ConnectionString = FAKE_CONNECTION_STRING - }); - - services.AddHealthChecks() - .AddRabbitMQ((sp, options) => - { - var connectionString = new Uri(sp.GetRequiredService().ConnectionString); - - options.ConnectionUri = connectionString; + return new ConnectionFactory() { Uri = new Uri(connectionString) }.CreateConnectionAsync(); }, name: customCheckName); using var serviceProvider = services.BuildServiceProvider(); diff --git a/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs b/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs index 7a8aafb209..b0886b62e7 100644 --- a/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs +++ b/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs @@ -14,33 +14,9 @@ public async Task be_healthy_if_rabbitmq_is_available() .ConfigureServices(services => { services.AddHealthChecks() - .AddRabbitMQ(rabbitConnectionString: connectionString, tags: ["rabbitmq"]); - }) - .Configure(app => - { - app.UseHealthChecks("/health", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("rabbitmq") - }); - }); - - using var server = new TestServer(webHostBuilder); - - using var response = await server.CreateRequest("/health").GetAsync(); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); - } - - [Fact] - public async Task be_healthy_if_rabbitmq_is_available_using_ssloption() - { - var connectionString = "amqp://localhost:5672"; - - var webHostBuilder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddHealthChecks() - .AddRabbitMQ(rabbitConnectionString: connectionString, sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); + .AddRabbitMQ( + _ => new ConnectionFactory() { Uri = new Uri(connectionString) }.CreateConnectionAsync(), + tags: ["rabbitmq"]); }) .Configure(app => { @@ -60,11 +36,15 @@ public async Task be_healthy_if_rabbitmq_is_available_using_ssloption() [Fact] public async Task be_unhealthy_if_rabbitmq_is_not_available() { + var connectionString = "amqp://localhost:6672"; + var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => { services.AddHealthChecks() - .AddRabbitMQ("amqp://localhost:6672", sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); + .AddRabbitMQ( + _ => new ConnectionFactory() { Uri = new Uri(connectionString) }.CreateConnectionAsync(), + tags: ["rabbitmq"]); }) .Configure(app => { @@ -82,7 +62,7 @@ public async Task be_unhealthy_if_rabbitmq_is_not_available() } [Fact] - public async Task be_healthy_if_rabbitmq_is_available_using_iconnectionfactory() + public async Task be_healthy_if_rabbitmq_is_available_using_iconnection() { var connectionString = "amqp://localhost:5672"; @@ -93,12 +73,18 @@ public async Task be_healthy_if_rabbitmq_is_available_using_iconnectionfactory() Ssl = new SslOption(serverName: "localhost", enabled: false) }; + var connection = await factory.CreateConnectionAsync(); + var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => { services .AddHealthChecks() - .AddRabbitMQ(options => options.ConnectionFactory = factory, tags: ["rabbitmq"]); + .Add(new HealthCheckRegistration( + "rabbitmq", + _ => new RabbitMQHealthCheck(connection), + failureStatus: null, + tags: ["rabbitmq"])); }) .Configure(app => { @@ -116,7 +102,7 @@ public async Task be_healthy_if_rabbitmq_is_available_using_iconnectionfactory() } [Fact] - public async Task be_healthy_if_rabbitmq_is_available_using_iconnection() + public async Task be_healthy_if_rabbitmq_is_available_using_iconnection_in_serviceprovider() { var connectionString = "amqp://localhost:5672"; @@ -127,11 +113,7 @@ public async Task be_healthy_if_rabbitmq_is_available_using_iconnection() Ssl = new SslOption(serverName: "localhost", enabled: false) }; -#if RABBITMQ_V6 - var connection = factory.CreateConnection(); -#else var connection = await factory.CreateConnectionAsync(); -#endif var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => @@ -156,32 +138,6 @@ public async Task be_healthy_if_rabbitmq_is_available_using_iconnection() response.StatusCode.ShouldBe(HttpStatusCode.OK); } - [Fact] - public async Task be_healthy_if_rabbitmq_is_available_and_specify_default_ssloption() - { - var connectionString = "amqp://localhost:5672"; - - var webHostBuilder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddHealthChecks() - .AddRabbitMQ(connectionString, sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); - }) - .Configure(app => - { - app.UseHealthChecks("/health", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("rabbitmq") - }); - }); - - using var server = new TestServer(webHostBuilder); - - using var response = await server.CreateRequest("/health").GetAsync(); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); - } - [Fact] public async Task be_not_crash_on_startup_when_rabbitmq_is_down_at_startup() { @@ -199,7 +155,9 @@ public async Task be_not_crash_on_startup_when_rabbitmq_is_down_at_startup() }; }) .AddHealthChecks() - .AddRabbitMQ(tags: ["rabbitmq"]); + .AddRabbitMQ( + sp => sp.GetRequiredService().CreateConnectionAsync(), + tags: ["rabbitmq"]); }) .Configure(app => { @@ -215,34 +173,6 @@ public async Task be_not_crash_on_startup_when_rabbitmq_is_down_at_startup() response1.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); } - [Fact] - public async Task be_healthy_if_rabbitmq_is_available_using_iServiceProvider() - { - var connectionString = "amqp://localhost:5672"; - - var webHostBuilder = new WebHostBuilder() - .ConfigureServices(services => - { - services - .AddHealthChecks() - .AddRabbitMQ(options => options.ConnectionUri = new Uri(connectionString), tags: ["rabbitmq"]); - - }) - .Configure(app => - { - app.UseHealthChecks("/health", new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("rabbitmq") - }); - }); - - using var server = new TestServer(webHostBuilder); - - using var response = await server.CreateRequest("/health").GetAsync(); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); - } - [Fact] public async Task two_rabbitmq_health_check() { @@ -252,9 +182,12 @@ public async Task two_rabbitmq_health_check() var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => { + services.AddKeyedSingleton("1", (sp, _) => new ConnectionFactory() { Uri = new Uri(connectionString1) }.CreateConnectionAsync().GetAwaiter().GetResult()); + services.AddKeyedSingleton("2", (sp, _) => new ConnectionFactory() { Uri = new Uri(connectionString2) }.CreateConnectionAsync().GetAwaiter().GetResult()); + services.AddHealthChecks() - .AddRabbitMQ(rabbitConnectionString: connectionString1, name: "rabbitmq1") - .AddRabbitMQ(rabbitConnectionString: connectionString2, name: "rabbitmq2"); + .AddRabbitMQ(sp => sp.GetRequiredKeyedService("1"), name: "rabbitmq1") + .AddRabbitMQ(sp => sp.GetRequiredKeyedService("2"), name: "rabbitmq2"); }) .Configure(app => { @@ -277,29 +210,27 @@ public async Task two_rabbitmq_health_check() response2.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); } - // https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/714 [Fact] - public async Task should_respect_timeout() + public async Task no_connection_registered() { - var services = new ServiceCollection(); - - services - .AddLogging() - .AddHealthChecks() - .AddRabbitMQ(opt => + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddHealthChecks() + .AddRabbitMQ(tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions { - opt.RequestedConnectionTimeout = TimeSpan.FromSeconds(1); - opt.ConnectionUri = new Uri($"amqps://user:pwd@invalid-host:5672"); - }, - timeout: TimeSpan.FromSeconds(10)); - - using var provider = services.BuildServiceProvider(); - var healthCheckService = provider.GetRequiredService(); - var start = DateTime.Now; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var report = await healthCheckService.CheckHealthAsync(cts.Token); - report.Status.ShouldBe(HealthStatus.Unhealthy); - var end = DateTime.Now; - (end - start).ShouldBeLessThan(TimeSpan.FromSeconds(10)); + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response1 = await server.CreateRequest("/health").GetAsync(); + response1.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); } } diff --git a/test/HealthChecks.RabbitMQ.Tests/HealthChecks.Rabbitmq.approved.txt b/test/HealthChecks.RabbitMQ.Tests/HealthChecks.Rabbitmq.approved.txt index e4e522964f..c0f129fb20 100644 --- a/test/HealthChecks.RabbitMQ.Tests/HealthChecks.Rabbitmq.approved.txt +++ b/test/HealthChecks.RabbitMQ.Tests/HealthChecks.Rabbitmq.approved.txt @@ -2,27 +2,16 @@ namespace HealthChecks.RabbitMQ { public class RabbitMQHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { - public RabbitMQHealthCheck(HealthChecks.RabbitMQ.RabbitMQHealthCheckOptions options) { } + public RabbitMQHealthCheck(RabbitMQ.Client.IConnection connection) { } + public RabbitMQHealthCheck(System.IServiceProvider serviceProvider, System.Func> factory) { } public System.Threading.Tasks.Task CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { } } - public class RabbitMQHealthCheckOptions - { - public RabbitMQHealthCheckOptions() { } - public RabbitMQ.Client.IConnection? Connection { get; set; } - public RabbitMQ.Client.IConnectionFactory? ConnectionFactory { get; set; } - public System.Uri? ConnectionUri { get; set; } - public System.TimeSpan? RequestedConnectionTimeout { get; set; } - public RabbitMQ.Client.SslOption? Ssl { get; set; } - } } namespace Microsoft.Extensions.DependencyInjection { public static class RabbitMQHealthCheckBuilderExtensions { - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string rabbitConnectionString, RabbitMQ.Client.SslOption? sslOption = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Uri rabbitConnectionString, RabbitMQ.Client.SslOption? sslOption = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func> factory, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func? factory = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable? tags = null, System.TimeSpan? timeout = default) { } } } \ No newline at end of file diff --git a/test/HealthChecks.RabbitMQ.v6.Tests/DependencyInjection/RegistrationTests.cs b/test/HealthChecks.RabbitMQ.v6.Tests/DependencyInjection/RegistrationTests.cs new file mode 100644 index 0000000000..6ceaa458f6 --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v6.Tests/DependencyInjection/RegistrationTests.cs @@ -0,0 +1,104 @@ +namespace HealthChecks.RabbitMQ.Tests.DependencyInjection; + +public class rabbitmq_registration_should +{ + private const string FAKE_CONNECTION_STRING = "amqp://server"; + private const string DEFAULT_CHECK_NAME = "rabbitmq"; + + [Fact] + public void add_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + services.AddHealthChecks() + .AddRabbitMQ(rabbitConnectionString: FAKE_CONNECTION_STRING); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe(DEFAULT_CHECK_NAME); + check.ShouldBeOfType(); + } + + [Fact] + public void add_named_health_check_when_properly_configured() + { + var services = new ServiceCollection(); + var customCheckName = "my-" + DEFAULT_CHECK_NAME; + + services.AddHealthChecks() + .AddRabbitMQ(FAKE_CONNECTION_STRING, name: customCheckName); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe(customCheckName); + check.ShouldBeOfType(); + } + + [Fact] + public void add_named_health_check_with_connection_string_factory_by_iServiceProvider_registered() + { + var services = new ServiceCollection(); + var customCheckName = "my-" + DEFAULT_CHECK_NAME; + services.AddSingleton(new RabbitMqSetting + { + ConnectionString = FAKE_CONNECTION_STRING + }); + + services.AddHealthChecks() + .AddRabbitMQ((sp, options) => + { + var connectionString = sp.GetRequiredService().ConnectionString; + + options.ConnectionUri = new Uri(connectionString); + }, name: customCheckName); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe(customCheckName); + check.ShouldBeOfType(); + } + + [Fact] + public void add_named_health_check_with_uri_string_factory_by_iServiceProvider_registered() + { + var services = new ServiceCollection(); + var customCheckName = "my-" + DEFAULT_CHECK_NAME; + services.AddSingleton(new RabbitMqSetting + { + ConnectionString = FAKE_CONNECTION_STRING + }); + + services.AddHealthChecks() + .AddRabbitMQ((sp, options) => + { + var connectionString = new Uri(sp.GetRequiredService().ConnectionString); + + options.ConnectionUri = connectionString; + }, name: customCheckName); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var registration = options.Value.Registrations.First(); + var check = registration.Factory(serviceProvider); + + registration.Name.ShouldBe(customCheckName); + check.ShouldBeOfType(); + } +} + +public class RabbitMqSetting +{ + public string ConnectionString { get; set; } = null!; +} diff --git a/test/HealthChecks.RabbitMQ.v6.Tests/Functional/RabbitHealthCheckTests.cs b/test/HealthChecks.RabbitMQ.v6.Tests/Functional/RabbitHealthCheckTests.cs new file mode 100644 index 0000000000..fc36601b36 --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v6.Tests/Functional/RabbitHealthCheckTests.cs @@ -0,0 +1,301 @@ +using System.Net; +using RabbitMQ.Client; + +namespace HealthChecks.RabbitMQ.Tests.Functional; + +public class rabbitmq_healthcheck_should +{ + [Fact] + public async Task be_healthy_if_rabbitmq_is_available() + { + var connectionString = "amqp://localhost:5672"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddRabbitMQ(rabbitConnectionString: connectionString, tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_healthy_if_rabbitmq_is_available_using_ssloption() + { + var connectionString = "amqp://localhost:5672"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddRabbitMQ(rabbitConnectionString: connectionString, sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_unhealthy_if_rabbitmq_is_not_available() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddRabbitMQ("amqp://localhost:6672", sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_healthy_if_rabbitmq_is_available_using_iconnectionfactory() + { + var connectionString = "amqp://localhost:5672"; + + var factory = new ConnectionFactory() + { + Uri = new Uri(connectionString), + AutomaticRecoveryEnabled = true, + Ssl = new SslOption(serverName: "localhost", enabled: false) + }; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddHealthChecks() + .AddRabbitMQ(options => options.ConnectionFactory = factory, tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_healthy_if_rabbitmq_is_available_using_iconnection() + { + var connectionString = "amqp://localhost:5672"; + + var factory = new ConnectionFactory() + { + Uri = new Uri(connectionString), + AutomaticRecoveryEnabled = true, + Ssl = new SslOption(serverName: "localhost", enabled: false) + }; + + var connection = factory.CreateConnection(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(connection) + .AddHealthChecks() + .AddRabbitMQ(tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_healthy_if_rabbitmq_is_available_and_specify_default_ssloption() + { + var connectionString = "amqp://localhost:5672"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddRabbitMQ(connectionString, sslOption: new SslOption(serverName: "localhost", enabled: false), tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_not_crash_on_startup_when_rabbitmq_is_down_at_startup() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(sp => + { + return new ConnectionFactory() + { + Uri = new Uri("amqp://localhost:3333"), + AutomaticRecoveryEnabled = true, + Ssl = new SslOption(serverName: "localhost", enabled: false) + }; + }) + .AddHealthChecks() + .AddRabbitMQ(tags: ["rabbitmq"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response1 = await server.CreateRequest("/health").GetAsync(); + response1.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_healthy_if_rabbitmq_is_available_using_iServiceProvider() + { + var connectionString = "amqp://localhost:5672"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services + .AddHealthChecks() + .AddRabbitMQ(options => options.ConnectionUri = new Uri(connectionString), tags: ["rabbitmq"]); + + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("rabbitmq") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task two_rabbitmq_health_check() + { + const string connectionString1 = "amqp://localhost:5672"; + const string connectionString2 = "amqp://localhost:6672/"; + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddRabbitMQ(rabbitConnectionString: connectionString1, name: "rabbitmq1") + .AddRabbitMQ(rabbitConnectionString: connectionString2, name: "rabbitmq2"); + }) + .Configure(app => + { + app.UseHealthChecks("/health1", new HealthCheckOptions + { + Predicate = r => r.Name.Equals("rabbitmq1") + }); + app.UseHealthChecks("/health2", new HealthCheckOptions + { + Predicate = r => r.Name.Equals("rabbitmq2") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response1 = await server.CreateRequest("/health1").GetAsync(); + using var response2 = await server.CreateRequest("/health2").GetAsync(); + + response1.StatusCode.ShouldBe(HttpStatusCode.OK); + response2.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + // https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/714 + [Fact] + public async Task should_respect_timeout() + { + var services = new ServiceCollection(); + + services + .AddLogging() + .AddHealthChecks() + .AddRabbitMQ(opt => + { + opt.RequestedConnectionTimeout = TimeSpan.FromSeconds(1); + opt.ConnectionUri = new Uri($"amqps://user:pwd@invalid-host:5672"); + }, + timeout: TimeSpan.FromSeconds(10)); + + using var provider = services.BuildServiceProvider(); + var healthCheckService = provider.GetRequiredService(); + var start = DateTime.Now; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var report = await healthCheckService.CheckHealthAsync(cts.Token); + report.Status.ShouldBe(HealthStatus.Unhealthy); + var end = DateTime.Now; + (end - start).ShouldBeLessThan(TimeSpan.FromSeconds(10)); + } +} diff --git a/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj b/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj index e0ec3cf27d..d37d8c713e 100644 --- a/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj +++ b/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj @@ -2,14 +2,10 @@ HealthChecks.RabbitMQ.Tests - $(DefineConstants);RABBITMQ_V6 - - -