Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RabbitMQ.Client version 7. #2323

Merged
merged 5 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not related to this PR, but it shows that we really need to get rid of that static cache, as it brings so much complexity and increases a risk for various bugs (leaks etc).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - agreed. I will try to make that change next.

{
/// <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
Loading