diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index 56d827accc..8075587267 100644 --- a/AspNetCore.Diagnostics.HealthChecks.sln +++ b/AspNetCore.Diagnostics.HealthChecks.sln @@ -310,6 +310,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Milvus", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecks.Milvus.Tests", "test\HealthChecks.Milvus.Tests\HealthChecks.Milvus.Tests.csproj", "{D49CF52C-9D21-4D98-8A15-A2B259E9C003}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.Rabbitmq.v7", "src\HealthChecks.Rabbitmq.v7\HealthChecks.Rabbitmq.v7.csproj", "{C76D7349-A3D2-7277-93C6-EE92E8E447A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.RabbitMQ.v7.Tests", "test\HealthChecks.RabbitMQ.v7.Tests\HealthChecks.RabbitMQ.v7.Tests.csproj", "{2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -868,6 +872,14 @@ Global {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Debug|Any CPU.Build.0 = Debug|Any CPU {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Release|Any CPU.ActiveCfg = Release|Any CPU {D49CF52C-9D21-4D98-8A15-A2B259E9C003}.Release|Any CPU.Build.0 = Release|Any CPU + {C76D7349-A3D2-7277-93C6-EE92E8E447A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C76D7349-A3D2-7277-93C6-EE92E8E447A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C76D7349-A3D2-7277-93C6-EE92E8E447A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C76D7349-A3D2-7277-93C6-EE92E8E447A5}.Release|Any CPU.Build.0 = Release|Any CPU + {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1011,6 +1023,8 @@ Global {3B812989-2C4E-4FCE-B3A0-EF9C00A9B3A5} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} {17913EAF-3B12-495B-80EA-9EB975FBE6BA} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} {D49CF52C-9D21-4D98-8A15-A2B259E9C003} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} + {C76D7349-A3D2-7277-93C6-EE92E8E447A5} = {2A3FD988-2BB8-43CF-B3A2-B70E648259D4} + {2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC} = {FF4414C2-8863-4ADA-8A1D-4B9F25C361FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B8C62A1-11B6-469F-874C-A02443256568} diff --git a/src/HealthChecks.Rabbitmq.v7/HealthChecks.Rabbitmq.v7.csproj b/src/HealthChecks.Rabbitmq.v7/HealthChecks.Rabbitmq.v7.csproj new file mode 100644 index 0000000000..86bff9b5ab --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v7/HealthChecks.Rabbitmq.v7.csproj @@ -0,0 +1,24 @@ + + + + $(DefaultLibraryTargetFrameworks) + $(PackageTags);RabbitMQ + HealthChecks.RabbitMQ is the health check package for RabbitMQ.Client (version 7+). + $(HealthCheckRabbitMQ) + HealthChecks.Rabbitmq + HealthChecks.RabbitMQ + README.md + + + + + + + + + + + + + + diff --git a/src/HealthChecks.Rabbitmq.v7/RabbitMQHealthCheck.cs b/src/HealthChecks.Rabbitmq.v7/RabbitMQHealthCheck.cs new file mode 100644 index 0000000000..4de8e996f8 --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v7/RabbitMQHealthCheck.cs @@ -0,0 +1,113 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using RabbitMQ.Client; + +namespace HealthChecks.RabbitMQ; + +/// +/// A health check for RabbitMQ services. +/// +public class RabbitMQHealthCheck : IHealthCheck +{ + private static readonly ConcurrentDictionary> _connections = new(); + + private IConnection? _connection; + private readonly RabbitMQHealthCheckOptions _options; + + public RabbitMQHealthCheck(RabbitMQHealthCheckOptions options) + { + _options = Guard.ThrowIfNull(options); + _connection = options.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 async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var connection = await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + await using var model = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + 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/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj index b81c68ab77..8f8195426d 100644 --- a/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj +++ b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs b/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs index 07c09d7713..d1791972d2 100644 --- a/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs +++ b/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs @@ -127,7 +127,11 @@ 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 => diff --git a/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj b/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj index c2ed3dfd92..48b7d153dc 100644 --- a/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj +++ b/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj @@ -1,5 +1,9 @@ + + $(DefineConstants);RABBITMQ_V6 + + diff --git a/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.RabbitMQ.v7.Tests.csproj b/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.RabbitMQ.v7.Tests.csproj new file mode 100644 index 0000000000..71aeca6d6c --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.RabbitMQ.v7.Tests.csproj @@ -0,0 +1,16 @@ + + + + HealthChecks.RabbitMQ.Tests + + + + + + + + + + + + diff --git a/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.Rabbitmq.approved.txt b/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.Rabbitmq.approved.txt new file mode 100644 index 0000000000..e4e522964f --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v7.Tests/HealthChecks.Rabbitmq.approved.txt @@ -0,0 +1,28 @@ +namespace HealthChecks.RabbitMQ +{ + public class RabbitMQHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck + { + public RabbitMQHealthCheck(HealthChecks.RabbitMQ.RabbitMQHealthCheckOptions options) { } + 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) { } + } +} \ No newline at end of file