Skip to content

Commit

Permalink
Add support for RabbitMQ.Client version 7.
Browse files Browse the repository at this point in the history
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, 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.
2. We add a new, forked component named HealthChecks.Rabbitmq.v7 which will have a dependency on 7.0.0 and contains updates so the health checks will work with v7. People who explicitly want to use version 7 can opt into using this package.
3. When the next major version of HealthChecks ships, we can "swap" the dependencies around.
The HealthChecks.Rabbitmq package will be updated to depend on version 7 of RabbitMQ.Client.
If RabbitMQ.Client v6 is still in support, we can create HeatlhChecks.Rabbitmq.v6 which has the dependency limit [6.8.1, 7.0.0) and works with the version 6 of RabbitMQ.Client.
HealthChecks.Rabbitmq.v7 will be dead-ended.
  • Loading branch information
eerhardt committed Nov 25, 2024
1 parent 508123e commit 6ee691f
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 1 deletion.
14 changes: 14 additions & 0 deletions AspNetCore.Diagnostics.HealthChecks.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
24 changes: 24 additions & 0 deletions src/HealthChecks.Rabbitmq.v7/HealthChecks.Rabbitmq.v7.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<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 7+).</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" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.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>
113 changes: 113 additions & 0 deletions src/HealthChecks.Rabbitmq.v7/RabbitMQHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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, Task<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 async Task<HealthCheckResult> 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<IConnection> 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
{
/// <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;
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/HealthChecks.Rabbitmq/HealthChecks.Rabbitmq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
<PackageReference Include="RabbitMQ.Client" Version="[6.8.1,7.0.0)" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
<Compile Include="../HealthCheckResultTask.cs" />
</ItemGroup>
Expand Down
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,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<DefineConstants>$(DefineConstants);RABBITMQ_V6</DefineConstants>
</PropertyGroup>

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

<PropertyGroup>
<AssemblyName>HealthChecks.RabbitMQ.Tests</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\HealthChecks.Rabbitmq.v7\HealthChecks.Rabbitmq.v7.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" />

<None Include="../HealthChecks.RabbitMQ.Tests/HealthChecks.Rabbitmq.approved.txt" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> 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<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action<HealthChecks.RabbitMQ.RabbitMQHealthCheckOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddRabbitMQ(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Action<System.IServiceProvider, HealthChecks.RabbitMQ.RabbitMQHealthCheckOptions>? setup, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? 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<string>? 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<string>? tags = null, System.TimeSpan? timeout = default) { }
}
}

0 comments on commit 6ee691f

Please sign in to comment.