diff --git a/.github/workflows/healthchecks_rabbitmq_ci.yml b/.github/workflows/healthchecks_rabbitmq_ci.yml index bae8535c93..0f6507a94e 100644 --- a/.github/workflows/healthchecks_rabbitmq_ci.yml +++ b/.github/workflows/healthchecks_rabbitmq_ci.yml @@ -20,7 +20,9 @@ on: branches: [ master ] paths: - src/HealthChecks.Rabbitmq/** + - src/HealthChecks.Rabbitmq.v6/** - test/HealthChecks.RabbitMQ.Tests/** + - test/HealthChecks.RabbitMQ.v6.Tests/** - test/_SHARED/** - .github/workflows/healthchecks_rabbitmq_ci.yml - Directory.Build.props @@ -46,15 +48,21 @@ jobs: - name: Restore run: | dotnet restore ./src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj && - dotnet restore ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj + dotnet restore ./src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj && + dotnet restore ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj && + dotnet restore ./test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj - name: Check formatting run: | dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && - dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) + dotnet format --no-restore --verify-no-changes --severity warn ./src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && + dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) && + dotnet format --no-restore --verify-no-changes --severity warn ./test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj || (echo "Run 'dotnet format' to fix issues" && exit 1) - name: Build run: | dotnet build --no-restore ./src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj && - dotnet build --no-restore ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj + dotnet build --no-restore ./src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj && + dotnet build --no-restore ./test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj && + dotnet build --no-restore ./test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj - name: Test run: > dotnet test @@ -65,6 +73,16 @@ jobs: --results-directory .coverage -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + - name: Test v6 + run: > + dotnet test + ./test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj + --no-restore + --no-build + --collect "XPlat Code Coverage" + --results-directory .coverage + -- + DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - name: Upload Coverage uses: codecov/codecov-action@v5 with: diff --git a/.github/workflows/healthchecks_rabbitmq_v6_cd.yml b/.github/workflows/healthchecks_rabbitmq_v6_cd.yml new file mode 100644 index 0000000000..7e44e8c68e --- /dev/null +++ b/.github/workflows/healthchecks_rabbitmq_v6_cd.yml @@ -0,0 +1,16 @@ +name: HealthChecks RabbitMQ v6 CD + +on: + push: + tags: + - release-rabbitmq-* + - release-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + PROJECT_PATH: ./src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.Rabbitmq.v6 diff --git a/.github/workflows/healthchecks_rabbitmq_v6_cd_preview.yml b/.github/workflows/healthchecks_rabbitmq_v6_cd_preview.yml new file mode 100644 index 0000000000..1afa3429f3 --- /dev/null +++ b/.github/workflows/healthchecks_rabbitmq_v6_cd_preview.yml @@ -0,0 +1,17 @@ +name: HealthChecks RabbitMQ v6 Preview CD + +on: + push: + tags: + - preview-rabbitmq-* + - preview-all-* + +jobs: + build: + uses: ./.github/workflows/reusable_cd_preview_workflow.yml + secrets: inherit + with: + BUILD_CONFIG: Release + VERSION_SUFFIX_PREFIX: rc1 + PROJECT_PATH: ./src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj + PACKAGE_NAME: AspNetCore.HealthChecks.Rabbitmq.v6 diff --git a/AspNetCore.Diagnostics.HealthChecks.sln b/AspNetCore.Diagnostics.HealthChecks.sln index cc3eac0450..5cfda92fe4 100644 --- a/AspNetCore.Diagnostics.HealthChecks.sln +++ b/AspNetCore.Diagnostics.HealthChecks.sln @@ -311,6 +311,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.v6", "src\HealthChecks.Rabbitmq.v6\HealthChecks.Rabbitmq.v6.csproj", "{C76D7349-A3D2-7277-93C6-EE92E8E447A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HealthChecks.RabbitMQ.v6.Tests", "test\HealthChecks.RabbitMQ.v6.Tests\HealthChecks.RabbitMQ.v6.Tests.csproj", "{2787F63E-ABEA-9461-CDF3-97FE7C5C3DCC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -869,6 +873,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 @@ -1012,6 +1024,8 @@ Global {7ECFCB71-4627-4671-9222-2C91EB8FB882} = {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/Directory.Packages.props b/Directory.Packages.props index cd3bd30299..1cdc346bdd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -82,7 +82,7 @@ - + diff --git a/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj b/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj new file mode 100644 index 0000000000..de95041214 --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj @@ -0,0 +1,23 @@ + + + + $(DefaultLibraryTargetFrameworks) + $(PackageTags);RabbitMQ + HealthChecks.RabbitMQ is the health check package for RabbitMQ.Client (version 6). + $(HealthCheckRabbitMQ) + HealthChecks.Rabbitmq + HealthChecks.RabbitMQ + README.md + + + + + + + + + + + + + diff --git a/src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheck.cs b/src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheck.cs new file mode 100644 index 0000000000..0eca28ad8f --- /dev/null +++ b/src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheck.cs @@ -0,0 +1,74 @@ +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 Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + // TODO: cancellationToken unused, see https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/714 + try + { + using var model = EnsureConnection().CreateModel(); + return HealthCheckResultTask.Healthy; + } + catch (Exception ex) + { + return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, exception: ex)); + } + } + + private IConnection EnsureConnection() + { + _connection ??= _connections.GetOrAdd(_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 factory.CreateConnection(); + }); + + return _connection; + } +} diff --git a/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj index add39f3891..2d2837aabb 100644 --- a/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj +++ b/src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj @@ -3,7 +3,7 @@ $(DefaultLibraryTargetFrameworks) $(PackageTags);RabbitMQ - HealthChecks.RabbitMQ is the health check package for RabbitMQ. + HealthChecks.RabbitMQ is the health check package for RabbitMQ.Client. $(HealthCheckRabbitMQ) HealthChecks.RabbitMQ diff --git a/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs b/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs index 4c164667a2..4de8e996f8 100644 --- a/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs +++ b/src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs @@ -9,7 +9,7 @@ namespace HealthChecks.RabbitMQ; /// public class RabbitMQHealthCheck : IHealthCheck { - private static readonly ConcurrentDictionary _connections = new(); + private static readonly ConcurrentDictionary> _connections = new(); private IConnection? _connection; private readonly RabbitMQHealthCheckOptions _options; @@ -26,49 +26,88 @@ public RabbitMQHealthCheck(RabbitMQHealthCheckOptions options) } /// - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - // TODO: cancellationToken unused, see https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/714 try { - using var model = EnsureConnection().CreateModel(); - return HealthCheckResultTask.Healthy; + 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 Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, exception: ex)); + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); } } - private IConnection EnsureConnection() - { - _connection ??= _connections.GetOrAdd(_options, _ => - { - var factory = _options.ConnectionFactory; + private async Task EnsureConnectionAsync(CancellationToken cancellationToken) => + _connection ??= await _connections.GetOrAddAsync(_options, async options => + { + var factory = options.ConnectionFactory; - if (factory is null) + if (factory is null) + { + Guard.ThrowIfNull(options.ConnectionUri); + factory = new ConnectionFactory { - Guard.ThrowIfNull(_options.ConnectionUri); - factory = new ConnectionFactory - { - Uri = _options.ConnectionUri, - AutomaticRecoveryEnabled = true - }; + Uri = options.ConnectionUri, + AutomaticRecoveryEnabled = true + }; - if (_options.RequestedConnectionTimeout is not null) - { - ((ConnectionFactory)factory).RequestedConnectionTimeout = _options.RequestedConnectionTimeout.Value; - } + if (options.RequestedConnectionTimeout is not null) + { + ((ConnectionFactory)factory).RequestedConnectionTimeout = options.RequestedConnectionTimeout.Value; + } - if (_options.Ssl is not null) - { - ((ConnectionFactory)factory).Ssl = _options.Ssl; - } + if (options.Ssl is not null) + { + ((ConnectionFactory)factory).Ssl = options.Ssl; } + } - return factory.CreateConnection(); - }); + return await factory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); +} - return _connection; +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/Functional/RabbitHealthCheckTests.cs b/test/HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs index 8ddbdbd8e2..7a8aafb209 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..90dd74e20a 100644 --- a/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj +++ b/test/HealthChecks.RabbitMQ.Tests/HealthChecks.RabbitMQ.Tests.csproj @@ -1,4 +1,4 @@ - + diff --git a/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj b/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj new file mode 100644 index 0000000000..e0ec3cf27d --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.RabbitMQ.v6.Tests.csproj @@ -0,0 +1,15 @@ + + + + HealthChecks.RabbitMQ.Tests + $(DefineConstants);RABBITMQ_V6 + + + + + + + + + + diff --git a/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.Rabbitmq.approved.txt b/test/HealthChecks.RabbitMQ.v6.Tests/HealthChecks.Rabbitmq.approved.txt new file mode 100644 index 0000000000..e4e522964f --- /dev/null +++ b/test/HealthChecks.RabbitMQ.v6.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