Skip to content

Commit

Permalink
fix: Handle exceptions in OutboxQueueHealthCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Griffiths authored and BEagle1984 committed Dec 22, 2023
1 parent 9b1d5d5 commit 4871326
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/Silverback.Core/Justifications.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ internal static class Justifications

public const string ExecutesSyncOrAsync =
"The method executes either synchronously or asynchronously";

public const string CatchAllExceptions =
"All exception types require catching";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,28 @@ public OutboxQueueHealthCheck(IOutboundQueueHealthCheckService service)
public static int? MaxQueueLength { get; set; }

/// <inheritdoc cref="IHealthCheck.CheckHealthAsync" />
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = Justifications.CatchAllExceptions)]
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
Check.NotNull(context, nameof(context));

if (await _service.CheckIsHealthyAsync(MaxMessageAge).ConfigureAwait(false))
return new HealthCheckResult(HealthStatus.Healthy);
try
{
if (await _service.CheckIsHealthyAsync(MaxMessageAge).ConfigureAwait(false))
return new HealthCheckResult(HealthStatus.Healthy);

string errorMessage = "The outbound queue exceeded the configured limits " +
$"(max message age: {MaxMessageAge.ToString()}, " +
$"max queue length: {MaxQueueLength?.ToString(CultureInfo.InvariantCulture) ?? "-"}).";
string errorMessage = "The outbound queue exceeded the configured limits " +
$"(max message age: {MaxMessageAge.ToString()}, " +
$"max queue length: {MaxQueueLength?.ToString(CultureInfo.InvariantCulture) ?? "-"}).";

return new HealthCheckResult(context.Registration.FailureStatus, errorMessage);
return new HealthCheckResult(context.Registration.FailureStatus, errorMessage);
}
catch (Exception ex)
{
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2020 Sergio Aquilini
// This code is licensed under MIT license (see LICENSE file for details)

using System;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Silverback.Database;
using Silverback.Messaging.HealthChecks;
using Silverback.Messaging.Outbound.TransactionalOutbox.Repositories;
using Xunit;

namespace Silverback.Tests.Integration.HealthChecks.Messaging.HealthChecks
{
public class OutboxQueueHealthCheckTests
{
[Fact]
public async Task CheckHealthAsync_ExceptionThrown_UnhealthyReturned()
{
var outboundQueueHealthCheckService = Substitute.For<IOutboundQueueHealthCheckService>();
outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any<TimeSpan>()).ThrowsAsync(new TimeoutException());

var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService);

var serviceProvider = ServiceProviderHelper.GetServiceProvider(
services => services
.AddSilverback()
.WithConnectionToMessageBroker(options => options
.AddKafka()
.AddOutbox<DbOutboxWriter, DbOutboxReader>())
.Services
.AddSingleton(outboundQueueHealthCheckService)
.AddSingleton(Substitute.For<IDbContext>())
.AddHealthChecks()
.Add(new HealthCheckRegistration("OutboundQueue", outboundQueueHealthCheck, default, default)));

var (healthCheck, context) = GetHealthCheck(serviceProvider);

var result = await healthCheck.CheckHealthAsync(context);

result.Status.Should().Be(HealthStatus.Unhealthy);
}

[Fact]
public async Task CheckHealthAsync_ExceptionThrown_ExceptionReturned()
{
var outboundQueueHealthCheckService = Substitute.For<IOutboundQueueHealthCheckService>();
TimeoutException exceptionToBeThrown = new TimeoutException();
outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any<TimeSpan>()).ThrowsAsync(exceptionToBeThrown);

var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService);

var serviceProvider = ServiceProviderHelper.GetServiceProvider(
services => services
.AddSilverback()
.WithConnectionToMessageBroker(options => options
.AddKafka()
.AddOutbox<DbOutboxWriter, DbOutboxReader>())
.Services
.AddSingleton(outboundQueueHealthCheckService)
.AddSingleton(Substitute.For<IDbContext>())
.AddHealthChecks()
.Add(new HealthCheckRegistration("OutboundQueue", outboundQueueHealthCheck, default, default)));

var (healthCheck, context) = GetHealthCheck(serviceProvider);

var result = await healthCheck.CheckHealthAsync(context);

result.Exception.Should().Be(exceptionToBeThrown);
}

[Theory]
[InlineData(true, HealthStatus.Healthy)]
[InlineData(false, HealthStatus.Unhealthy)]
public async Task CheckHealthAsync_HealthyCheck_ReturnsExpectedStatus(bool healthy, HealthStatus expectedStatus)
{
var outboundQueueHealthCheckService = Substitute.For<IOutboundQueueHealthCheckService>();
outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any<TimeSpan>()).Returns(healthy);

var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService);

var serviceProvider = ServiceProviderHelper.GetServiceProvider(
services => services
.AddSilverback()
.WithConnectionToMessageBroker(options => options
.AddKafka()
.AddOutbox<DbOutboxWriter, DbOutboxReader>())
.Services
.AddSingleton(outboundQueueHealthCheckService)
.AddSingleton(Substitute.For<IDbContext>())
.AddHealthChecks()
.Add(new HealthCheckRegistration("OutboundQueue", outboundQueueHealthCheck, default, default)));

var (healthCheck, context) = GetHealthCheck(serviceProvider);

var result = await healthCheck.CheckHealthAsync(context);

result.Status.Should().Be(expectedStatus);
}

private static (IHealthCheck HealthCheck, HealthCheckContext Context) GetHealthCheck(IServiceProvider serviceProvider)
{
var healthCheckOptions = serviceProvider
.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var context = new HealthCheckContext
{
Registration = healthCheckOptions.Value.Registrations.First()
};

return (context.Registration.Factory.Invoke(serviceProvider), context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Silverback.Core.EFCore30\Silverback.Core.EFCore30.csproj" />
<ProjectReference Include="..\..\src\Silverback.Integration.HealthChecks\Silverback.Integration.HealthChecks.csproj" />
<ProjectReference Include="..\..\src\Silverback.Integration.Kafka\Silverback.Integration.Kafka.csproj" />
<ProjectReference Include="..\Silverback.Tests.Common.Integration\Silverback.Tests.Common.Integration.csproj" />
<ProjectReference Include="..\Silverback.Tests.Common\Silverback.Tests.Common.csproj" />
</ItemGroup>
Expand Down

0 comments on commit 4871326

Please sign in to comment.