diff --git a/src/Silverback.Core/Justifications.cs b/src/Silverback.Core/Justifications.cs index 84c485960..b9f4ab7a8 100644 --- a/src/Silverback.Core/Justifications.cs +++ b/src/Silverback.Core/Justifications.cs @@ -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"; } } diff --git a/src/Silverback.Integration.HealthChecks/Messaging/HealthChecks/OutboxQueueHealthCheck.cs b/src/Silverback.Integration.HealthChecks/Messaging/HealthChecks/OutboxQueueHealthCheck.cs index bd5339047..81d904cce 100644 --- a/src/Silverback.Integration.HealthChecks/Messaging/HealthChecks/OutboxQueueHealthCheck.cs +++ b/src/Silverback.Integration.HealthChecks/Messaging/HealthChecks/OutboxQueueHealthCheck.cs @@ -42,20 +42,28 @@ public OutboxQueueHealthCheck(IOutboundQueueHealthCheckService service) public static int? MaxQueueLength { get; set; } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = Justifications.CatchAllExceptions)] public async Task 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); + } } } } diff --git a/tests/Silverback.Integration.HealthChecks.Tests/Messaging/HealthChecks/OutboxQueueHealthCheckTests.cs b/tests/Silverback.Integration.HealthChecks.Tests/Messaging/HealthChecks/OutboxQueueHealthCheckTests.cs new file mode 100644 index 000000000..efdcd05ef --- /dev/null +++ b/tests/Silverback.Integration.HealthChecks.Tests/Messaging/HealthChecks/OutboxQueueHealthCheckTests.cs @@ -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(); + outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any()).ThrowsAsync(new TimeoutException()); + + var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService); + + var serviceProvider = ServiceProviderHelper.GetServiceProvider( + services => services + .AddSilverback() + .WithConnectionToMessageBroker(options => options + .AddKafka() + .AddOutbox()) + .Services + .AddSingleton(outboundQueueHealthCheckService) + .AddSingleton(Substitute.For()) + .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(); + TimeoutException exceptionToBeThrown = new TimeoutException(); + outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any()).ThrowsAsync(exceptionToBeThrown); + + var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService); + + var serviceProvider = ServiceProviderHelper.GetServiceProvider( + services => services + .AddSilverback() + .WithConnectionToMessageBroker(options => options + .AddKafka() + .AddOutbox()) + .Services + .AddSingleton(outboundQueueHealthCheckService) + .AddSingleton(Substitute.For()) + .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(); + outboundQueueHealthCheckService.CheckIsHealthyAsync(Arg.Any()).Returns(healthy); + + var outboundQueueHealthCheck = new OutboxQueueHealthCheck(outboundQueueHealthCheckService); + + var serviceProvider = ServiceProviderHelper.GetServiceProvider( + services => services + .AddSilverback() + .WithConnectionToMessageBroker(options => options + .AddKafka() + .AddOutbox()) + .Services + .AddSingleton(outboundQueueHealthCheckService) + .AddSingleton(Substitute.For()) + .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>(); + + var context = new HealthCheckContext + { + Registration = healthCheckOptions.Value.Registrations.First() + }; + + return (context.Registration.Factory.Invoke(serviceProvider), context); + } + } +} diff --git a/tests/Silverback.Integration.HealthChecks.Tests/Silverback.Integration.HealthChecks.Tests.csproj b/tests/Silverback.Integration.HealthChecks.Tests/Silverback.Integration.HealthChecks.Tests.csproj index fee155e8c..31bc6de0b 100644 --- a/tests/Silverback.Integration.HealthChecks.Tests/Silverback.Integration.HealthChecks.Tests.csproj +++ b/tests/Silverback.Integration.HealthChecks.Tests/Silverback.Integration.HealthChecks.Tests.csproj @@ -27,6 +27,7 @@ +