Skip to content

Commit

Permalink
Add support for RabbitMQ.Client version 7. (#2323)
Browse files Browse the repository at this point in the history
* Add support for RabbitMQ.Client version 7.

RabbitMQ.Client version 7 made major breaking changes - interface renames, all sync methods are removed and only async methods remain.

Handle this breaking change by splitting our package into 2, one for each major version.

1. For the current HealthChecks.Rabbitmq package, update to the new 7.0.0 version and update so the health checks will work with v7.
2. We add a new, forked component named HealthChecks.Rabbitmq.v6 which will have a dependency on 6.x . People who want to keep using the version 6 can opt into using this package. We put a NuGet version limit on our dependency: [6.8.1,7.0.0). This way people won't be able to update to the 7.0.0 version, which will break their app. They will need to migrate back to the base package to use version 7.
3. When RabbitMQ.Client v6 is no longer supported, we can dead-end the `HealthChecks.Rabbitmq.v6` package.

Fix #2319

- add workflows for the new v6 package

* Fix path
  • Loading branch information
eerhardt authored Dec 6, 2024
1 parent b21485e commit 6040468
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 35 deletions.
24 changes: 21 additions & 3 deletions .github/workflows/healthchecks_rabbitmq_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/healthchecks_rabbitmq_v6_cd.yml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions .github/workflows/healthchecks_rabbitmq_v6_cd_preview.yml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions AspNetCore.Diagnostics.HealthChecks.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
<PackageVersion Include="prometheus-net" Version="8.1.0" />
<PackageVersion Include="PublicApiGenerator" Version="11.0.0" />
<PackageVersion Include="Qdrant.Client" Version="1.12.0" />
<PackageVersion Include="RabbitMQ.Client" Version="6.8.1" />
<PackageVersion Include="RabbitMQ.Client" Version="7.0.0" />
<PackageVersion Include="RavenDB.Client" Version="6.0.1" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.7.0" />
Expand Down
23 changes: 23 additions & 0 deletions src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(DefaultLibraryTargetFrameworks)</TargetFrameworks>
<PackageTags>$(PackageTags);RabbitMQ</PackageTags>
<Description>HealthChecks.RabbitMQ is the health check package for RabbitMQ.Client (version 6).</Description>
<VersionPrefix>$(HealthCheckRabbitMQ)</VersionPrefix>
<AssemblyName>HealthChecks.Rabbitmq</AssemblyName>
<RootNamespace>HealthChecks.RabbitMQ</RootNamespace> <!--For backward naming compatibility-->
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<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>
74 changes: 74 additions & 0 deletions src/HealthChecks.Rabbitmq.v6/RabbitMQHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using RabbitMQ.Client;

namespace HealthChecks.RabbitMQ;

/// <summary>
/// A health check for RabbitMQ services.
/// </summary>
public class RabbitMQHealthCheck : IHealthCheck
{
private static readonly ConcurrentDictionary<RabbitMQHealthCheckOptions, IConnection> _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));
}
}

/// <inheritdoc />
public Task<HealthCheckResult> 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;
}
}
2 changes: 1 addition & 1 deletion src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(DefaultLibraryTargetFrameworks)</TargetFrameworks>
<PackageTags>$(PackageTags);RabbitMQ</PackageTags>
<Description>HealthChecks.RabbitMQ is the health check package for RabbitMQ.</Description>
<Description>HealthChecks.RabbitMQ is the health check package for RabbitMQ.Client.</Description>
<VersionPrefix>$(HealthCheckRabbitMQ)</VersionPrefix>
<RootNamespace>HealthChecks.RabbitMQ</RootNamespace> <!--For backward naming compatibility-->
</PropertyGroup>
Expand Down
97 changes: 68 additions & 29 deletions src/HealthChecks.Rabbitmq/RabbitMQHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace HealthChecks.RabbitMQ;
/// </summary>
public class RabbitMQHealthCheck : IHealthCheck
{
private static readonly ConcurrentDictionary<RabbitMQHealthCheckOptions, IConnection> _connections = new();
private static readonly ConcurrentDictionary<RabbitMQHealthCheckOptions, Task<IConnection>> _connections = new();

private IConnection? _connection;
private readonly RabbitMQHealthCheckOptions _options;
Expand All @@ -26,49 +26,88 @@ public RabbitMQHealthCheck(RabbitMQHealthCheckOptions options)
}

/// <inheritdoc />
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
public async Task<HealthCheckResult> 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<IConnection> 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
{
/// <summary>
/// Provides an alternative to <see cref="ConcurrentDictionary{TKey, TValue}.GetOrAdd(TKey, Func{TKey, TValue})"/> specifically for asynchronous values. The factory method will only run once.
/// </summary>
public static async Task<TValue> GetOrAddAsync<TKey, TValue>(
this ConcurrentDictionary<TKey, Task<TValue>> dictionary,
TKey key,
Func<TKey, Task<TValue>> 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<TValue>(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;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\..\src\HealthChecks.Rabbitmq\HealthChecks.Rabbitmq.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>HealthChecks.RabbitMQ.Tests</AssemblyName>
<DefineConstants>$(DefineConstants);RABBITMQ_V6</DefineConstants>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\HealthChecks.Rabbitmq.v6\HealthChecks.Rabbitmq.v6.csproj" />

<Compile Include="../HealthChecks.RabbitMQ.Tests/DependencyInjection/RegistrationTests.cs" Link="DependencyInjection/RegistrationTests.cs" />
<Compile Include="../HealthChecks.RabbitMQ.Tests/Functional/RabbitHealthCheckTests.cs" Link="Functional/RabbitHealthCheckTests.cs" />
</ItemGroup>

</Project>
Loading

0 comments on commit 6040468

Please sign in to comment.