From a9ec685c7766735dc339727f036e9a66e8e074fe Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Wed, 31 Aug 2022 01:22:44 -0700 Subject: [PATCH] Messaging service extension for health check service (#63) * Add health check for RabbitMQ Signed-off-by: Victor Chang --- .licenserc.yaml | 3 +- doc/dependency_decisions.yml | 16 +- .../Configuration/ConfigurationException.cs | 2 +- src/Messaging/Events/ExportCompleteEvent.cs | 2 +- src/Messaging/Events/ExportRequestEvent.cs | 2 +- src/Messaging/Events/ExportStatus.cs | 1 - src/Messaging/Events/FailureReason.cs | 1 - src/Messaging/Events/WorkflowRequestEvent.cs | 2 +- src/Messaging/HealthCheckRegistrationBase.cs | 38 +++++ src/Messaging/IServiceCollectionExtension.cs | 137 ++++++++++++++---- src/Messaging/Monai.Deploy.Messaging.csproj | 1 + src/Messaging/SR.cs | 1 - src/Messaging/ServiceRegistrationBase.cs | 32 ---- .../IServiceCollectionExtensionsTests.cs | 63 ++++---- .../Tests/ServiceRegistrationBaseTests.cs | 43 ------ .../Tests/TaskCancellationEventTests.cs | 1 - src/Plugins/RabbitMQ/ConfigurationKeys.cs | 5 +- src/Plugins/RabbitMQ/InternalVisible.cs | 1 + src/Plugins/RabbitMQ/Logger.cs | 3 + .../PublisherServicHealthCheckBuilder.cs | 50 +++++++ .../RabbitMQ/PublisherServiceRegistration.cs | 4 - src/Plugins/RabbitMQ/RabbitMQHealthCheck.cs | 66 +++++++++ .../RabbitMqMessagePublisherService.cs | 14 +- .../RabbitMqMessageSubscriberService.cs | 33 ++--- .../SubscriberServicHealthCheckBuilder.cs | 50 +++++++ .../RabbitMQ/SubscriberServiceRegistration.cs | 4 - .../RabbitMQ/Tests/RabbitMQHealthCheckTest.cs | 66 +++++++++ .../RabbitMQ/Tests/ServiceRegistrationTest.cs | 42 +++++- 28 files changed, 498 insertions(+), 185 deletions(-) create mode 100644 src/Messaging/HealthCheckRegistrationBase.cs delete mode 100644 src/Messaging/Tests/ServiceRegistrationBaseTests.cs create mode 100644 src/Plugins/RabbitMQ/PublisherServicHealthCheckBuilder.cs create mode 100644 src/Plugins/RabbitMQ/RabbitMQHealthCheck.cs create mode 100644 src/Plugins/RabbitMQ/SubscriberServicHealthCheckBuilder.cs create mode 100644 src/Plugins/RabbitMQ/Tests/RabbitMQHealthCheckTest.cs diff --git a/.licenserc.yaml b/.licenserc.yaml index 44846d9..78a6349 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -13,6 +13,7 @@ header: - '**/*.ruleset' - 'src/.sonarlint/**' - 'src/coverlet.runsettings' + - 'src/Monai.Deploy.Messaging.sln' comment: on-failure @@ -28,4 +29,4 @@ header: Config: extensions: - ".conf" - comment_style_id: Hashtag \ No newline at end of file + comment_style_id: Hashtag diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index 65db3fa..64d9a5c 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -27,6 +27,20 @@ :versions: - 17.3.0 :when: 2022-08-16 21:39:37.382080790 Z +- - :approve + - Microsoft.Extensions.Diagnostics.HealthChecks + - :who: mocsharp + :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) + :versions: + - 6.0.8 + :when: 2022-08-29 18:11:22.090772006 Z +- - :approve + - Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions + - :who: mocsharp + :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) + :versions: + - 6.0.8 + :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.Configuration - :who: mocsharp @@ -144,7 +158,7 @@ - :who: mocsharp :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) :versions: - - 6.0.0 + - 6.0.1 :when: 2022-08-16 21:39:44.471693654 Z - - :approve - Microsoft.Extensions.Logging.Configuration diff --git a/src/Messaging/Configuration/ConfigurationException.cs b/src/Messaging/Configuration/ConfigurationException.cs index 959eb15..461d19e 100644 --- a/src/Messaging/Configuration/ConfigurationException.cs +++ b/src/Messaging/Configuration/ConfigurationException.cs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System.Runtime.Serialization; namespace Monai.Deploy.Messaging.Configuration diff --git a/src/Messaging/Events/ExportCompleteEvent.cs b/src/Messaging/Events/ExportCompleteEvent.cs index 0f43033..4577493 100644 --- a/src/Messaging/Events/ExportCompleteEvent.cs +++ b/src/Messaging/Events/ExportCompleteEvent.cs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System.ComponentModel.DataAnnotations; using Ardalis.GuardClauses; using Newtonsoft.Json; diff --git a/src/Messaging/Events/ExportRequestEvent.cs b/src/Messaging/Events/ExportRequestEvent.cs index 92552c0..71388f3 100644 --- a/src/Messaging/Events/ExportRequestEvent.cs +++ b/src/Messaging/Events/ExportRequestEvent.cs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System.ComponentModel.DataAnnotations; using Newtonsoft.Json; diff --git a/src/Messaging/Events/ExportStatus.cs b/src/Messaging/Events/ExportStatus.cs index 88b05a1..bebf792 100644 --- a/src/Messaging/Events/ExportStatus.cs +++ b/src/Messaging/Events/ExportStatus.cs @@ -14,7 +14,6 @@ * limitations under the License. */ - namespace Monai.Deploy.Messaging.Events { public enum ExportStatus diff --git a/src/Messaging/Events/FailureReason.cs b/src/Messaging/Events/FailureReason.cs index 5e4c054..fba7a5d 100644 --- a/src/Messaging/Events/FailureReason.cs +++ b/src/Messaging/Events/FailureReason.cs @@ -14,7 +14,6 @@ * limitations under the License. */ - namespace Monai.Deploy.Messaging.Events { public enum FailureReason diff --git a/src/Messaging/Events/WorkflowRequestEvent.cs b/src/Messaging/Events/WorkflowRequestEvent.cs index 924760b..b437233 100644 --- a/src/Messaging/Events/WorkflowRequestEvent.cs +++ b/src/Messaging/Events/WorkflowRequestEvent.cs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System.ComponentModel.DataAnnotations; using Monai.Deploy.Messaging.Common; using Newtonsoft.Json; diff --git a/src/Messaging/HealthCheckRegistrationBase.cs b/src/Messaging/HealthCheckRegistrationBase.cs new file mode 100644 index 0000000..7f47176 --- /dev/null +++ b/src/Messaging/HealthCheckRegistrationBase.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Monai.Deploy.Messaging +{ + public abstract class SubscriberServiceHealthCheckRegistrationBase : HealthCheckRegistrationBase + { + } + + public abstract class PublisherServiceHealthCheckRegistrationBase : HealthCheckRegistrationBase + { + } + + public abstract class HealthCheckRegistrationBase + { + public abstract IHealthChecksBuilder Configure( + IHealthChecksBuilder builder, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null); + } +} diff --git a/src/Messaging/IServiceCollectionExtension.cs b/src/Messaging/IServiceCollectionExtension.cs index 9923d4f..fd7516f 100644 --- a/src/Messaging/IServiceCollectionExtension.cs +++ b/src/Messaging/IServiceCollectionExtension.cs @@ -18,6 +18,7 @@ using System.Reflection; using Ardalis.GuardClauses; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Monai.Deploy.Messaging.API; using Monai.Deploy.Messaging.Configuration; @@ -25,8 +26,6 @@ namespace Monai.Deploy.Messaging { public static class IServiceCollectionExtensions { - private static IFileSystem? s_fileSystem; - /// /// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service. /// @@ -34,8 +33,14 @@ public static class IServiceCollectionExtensions /// Fully qualified type name of the service to use. /// Instance of . /// - public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName) - => AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem()); + public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService( + this IServiceCollection services, + string fullyQualifiedTypeName, + bool registerHealthCheck = true, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + => AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem(), registerHealthCheck, failureStatus, tags, timeout); /// /// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service. @@ -45,8 +50,15 @@ public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(th /// Instance of . /// Instance of . /// - public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) - => Add(services, fullyQualifiedTypeName, fileSystem); + public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService( + this IServiceCollection services, + string fullyQualifiedTypeName, + IFileSystem fileSystem, + bool registerHealthCheck = true, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + => Add(services, fullyQualifiedTypeName, fileSystem, registerHealthCheck, failureStatus, tags, timeout); /// /// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service. @@ -55,8 +67,14 @@ public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(th /// Fully qualified type name of the service to use. /// Instance of . /// - public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName) - => AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem()); + public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService( + this IServiceCollection services, + string fullyQualifiedTypeName, + bool registerHealthCheck = true, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + => AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem(), registerHealthCheck, failureStatus, tags, timeout); /// /// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service. @@ -66,42 +84,96 @@ public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(thi /// Instance of . /// Instance of . /// - public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) - => Add(services, fullyQualifiedTypeName, fileSystem); - - private static IServiceCollection Add(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) where U : ServiceRegistrationBase + public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService( + this IServiceCollection services, + string fullyQualifiedTypeName, + IFileSystem fileSystem, + bool registerHealthCheck = true, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + => Add(services, fullyQualifiedTypeName, fileSystem, registerHealthCheck, failureStatus, tags, timeout); + + private static IServiceCollection Add( + this IServiceCollection services, + string fullyQualifiedTypeName, + IFileSystem fileSystem, + bool registerHealthCheck = true, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + where U : ServiceRegistrationBase + where V : HealthCheckRegistrationBase { Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName)); Guard.Against.Null(fileSystem, nameof(fileSystem)); - s_fileSystem = fileSystem; + ResolveEventHandler resolveEventHandler = (sender, args) => + { + return CurrentDomain_AssemblyResolve(args, fileSystem); + }; - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + AppDomain.CurrentDomain.AssemblyResolve += resolveEventHandler; try { - var serviceAssembly = LoadAssemblyFromDisk(GetAssemblyName(fullyQualifiedTypeName)); - var serviceRegistrationType = serviceAssembly.GetTypes().FirstOrDefault(p => p.BaseType == typeof(U)); + var serviceAssembly = LoadAssemblyFromDisk(GetAssemblyName(fullyQualifiedTypeName), fileSystem); - if (serviceRegistrationType is null || Activator.CreateInstance(serviceRegistrationType, fullyQualifiedTypeName) is not U serviceRegistrar) + if (!IsSupportedType(fullyQualifiedTypeName, serviceAssembly)) { - throw new ConfigurationException($"Service registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'."); + throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(T).Name} interface."); } - if (!IsSupportedType(fullyQualifiedTypeName, serviceAssembly)) + RegisterServices(services, fullyQualifiedTypeName, serviceAssembly); + + if (registerHealthCheck) { - throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(T).Name} interface."); + RegisterHealtChecks(services, fullyQualifiedTypeName, serviceAssembly, failureStatus, tags, timeout); } - return serviceRegistrar.Configure(services); + return services; } finally { - AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; + AppDomain.CurrentDomain.AssemblyResolve -= resolveEventHandler; + } + } + + private static void RegisterHealtChecks( + IServiceCollection services, + string fullyQualifiedTypeName, + Assembly serviceAssembly, + HealthStatus? failureStatus, + IEnumerable? tags, + TimeSpan? timeout) where V : HealthCheckRegistrationBase + { + var healthCheckBaseType = serviceAssembly.GetTypes().FirstOrDefault(p => p.BaseType == typeof(V)); + + if (healthCheckBaseType is null || Activator.CreateInstance(healthCheckBaseType) is not V healthCheckBuilderBase) + { + throw new ConfigurationException($"Health check registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'."); + } + + var healthCheckBuilder = services.AddHealthChecks(); + healthCheckBuilderBase.Configure(healthCheckBuilder, failureStatus, tags, timeout); + } + + private static void RegisterServices( + IServiceCollection services, + string fullyQualifiedTypeName, + Assembly serviceAssembly) where U : ServiceRegistrationBase + { + var serviceRegistrationType = serviceAssembly.GetTypes().FirstOrDefault(p => p.BaseType == typeof(U)); + + if (serviceRegistrationType is null || Activator.CreateInstance(serviceRegistrationType) is not U serviceRegistrar) + { + throw new ConfigurationException($"Service registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'."); } + + serviceRegistrar.Configure(services); } - private static bool IsSupportedType(string fullyQualifiedTypeName, Assembly storageServiceAssembly) + internal static bool IsSupportedType(string fullyQualifiedTypeName, Assembly storageServiceAssembly) { Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName)); Guard.Against.Null(storageServiceAssembly, nameof(storageServiceAssembly)); @@ -112,7 +184,7 @@ private static bool IsSupportedType(string fullyQualifiedTypeName, Assembly s storageServiceType.GetInterfaces().Contains(typeof(T)); } - private static string GetAssemblyName(string fullyQualifiedTypeName) + internal static string GetAssemblyName(string fullyQualifiedTypeName) { var assemblyNameParts = fullyQualifiedTypeName.Split(',', StringSplitOptions.None); if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1])) @@ -126,31 +198,32 @@ private static string GetAssemblyName(string fullyQualifiedTypeName) return assemblyNameParts[1].Trim(); } - private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + internal static Assembly CurrentDomain_AssemblyResolve(ResolveEventArgs args, IFileSystem fileSystem) { Guard.Against.Null(args, nameof(args)); + Guard.Against.Null(fileSystem, nameof(fileSystem)); var requestedAssemblyName = new AssemblyName(args.Name); - return LoadAssemblyFromDisk(requestedAssemblyName.Name); + return LoadAssemblyFromDisk(requestedAssemblyName.Name!, fileSystem); } - private static Assembly LoadAssemblyFromDisk(string assemblyName) + internal static Assembly LoadAssemblyFromDisk(string assemblyName, IFileSystem fileSystem) { Guard.Against.NullOrWhiteSpace(assemblyName, nameof(assemblyName)); - Guard.Against.Null(s_fileSystem, nameof(s_fileSystem)); + Guard.Against.Null(fileSystem, nameof(fileSystem)); - if (!s_fileSystem.Directory.Exists(SR.PlugInDirectoryPath)) + if (!fileSystem.Directory.Exists(SR.PlugInDirectoryPath)) { throw new ConfigurationException($"Plug-in directory '{SR.PlugInDirectoryPath}' cannot be found."); } - var assemblyFilePath = s_fileSystem.Path.Combine(SR.PlugInDirectoryPath, $"{assemblyName}.dll"); - if (!s_fileSystem.File.Exists(assemblyFilePath)) + var assemblyFilePath = fileSystem.Path.Combine(SR.PlugInDirectoryPath, $"{assemblyName}.dll"); + if (!fileSystem.File.Exists(assemblyFilePath)) { throw new ConfigurationException($"The configured plug-in '{assemblyFilePath}' cannot be found."); } - var asesmblyeData = s_fileSystem.File.ReadAllBytes(assemblyFilePath); + var asesmblyeData = fileSystem.File.ReadAllBytes(assemblyFilePath); return Assembly.Load(asesmblyeData); } } diff --git a/src/Messaging/Monai.Deploy.Messaging.csproj b/src/Messaging/Monai.Deploy.Messaging.csproj index ed089fd..2e55d8a 100644 --- a/src/Messaging/Monai.Deploy.Messaging.csproj +++ b/src/Messaging/Monai.Deploy.Messaging.csproj @@ -76,6 +76,7 @@ + diff --git a/src/Messaging/SR.cs b/src/Messaging/SR.cs index 84fa01f..9e6bc3d 100644 --- a/src/Messaging/SR.cs +++ b/src/Messaging/SR.cs @@ -14,7 +14,6 @@ * limitations under the License. */ - namespace Monai.Deploy.Messaging { internal static class SR diff --git a/src/Messaging/ServiceRegistrationBase.cs b/src/Messaging/ServiceRegistrationBase.cs index 5a01ee5..798b17c 100644 --- a/src/Messaging/ServiceRegistrationBase.cs +++ b/src/Messaging/ServiceRegistrationBase.cs @@ -14,52 +14,20 @@ * limitations under the License. */ -using Ardalis.GuardClauses; using Microsoft.Extensions.DependencyInjection; -using Monai.Deploy.Messaging.Configuration; namespace Monai.Deploy.Messaging { public abstract class SubscriberServiceRegistrationBase : ServiceRegistrationBase { - protected SubscriberServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } } public abstract class PublisherServiceRegistrationBase : ServiceRegistrationBase { - protected PublisherServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } } public abstract class ServiceRegistrationBase { - protected string FullyQualifiedAssemblyName { get; } - protected string AssemblyFilename { get; } - - protected ServiceRegistrationBase(string fullyQualifiedAssemblyName) - { - Guard.Against.NullOrWhiteSpace(fullyQualifiedAssemblyName, nameof(fullyQualifiedAssemblyName)); - FullyQualifiedAssemblyName = fullyQualifiedAssemblyName; - AssemblyFilename = ParseAssemblyName(); - } - - private string ParseAssemblyName() - { - var assemblyNameParts = FullyQualifiedAssemblyName.Split(',', StringSplitOptions.None); - if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1])) - { - throw new ConfigurationException($"Storage service '{FullyQualifiedAssemblyName}' is invalid. Please provide a fully qualified name.") - { - HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name" - }; - } - - return assemblyNameParts[1].Trim(); - } - public abstract IServiceCollection Configure(IServiceCollection services); } } diff --git a/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs b/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs index 421e8b0..6701127 100644 --- a/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs +++ b/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs @@ -15,11 +15,13 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Monai.Deploy.Messaging.API; using Monai.Deploy.Messaging.Common; using Monai.Deploy.Messaging.Configuration; @@ -76,26 +78,6 @@ public void AddMonaiDeployMessageBrokerServices_ThrowsIfPlugInDllIsMissing() Assert.Equal($"The configured plug-in '{SR.PlugInDirectoryPath}{Path.DirectorySeparatorChar}{badType.Assembly.ManifestModule.Name}' cannot be found.", exception.Message); } - [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if service registrar cannot be found in the assembly")] - public void AddMonaiDeployMessageBrokerServices_ThrowsIfServiceRegistrarCannotBeFoundInTheAssembly() - { - var badType = typeof(Assert); - var typeName = badType.AssemblyQualifiedName; - var assemblyData = GetAssemblyeBytes(badType.Assembly); - var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); - var fileSystem = new MockFileSystem(); - fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); - fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); - var serviceCollection = new Mock(); - var exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); - Assert.NotNull(exception); - Assert.Equal($"Service registrar cannot be found for the configured plug-in '{typeName}'.", exception.Message); - - exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); - Assert.NotNull(exception); - Assert.Equal($"Service registrar cannot be found for the configured plug-in '{typeName}'.", exception.Message); - } - [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if service type is not supported")] public void AddMonaiDeployMessageBrokerServices_ThrowsIfServiceTypeIsNotSupported() { @@ -129,7 +111,7 @@ public void AddMonaiDeployMessageBrokerPublisherService_ConfiuresServicesAsExpec fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); var serviceCollection = new Mock(); serviceCollection.Setup(p => p.Clear()); - var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem, false)); Assert.Null(exception); serviceCollection.Verify(p => p.Clear(), Times.Once()); } @@ -146,7 +128,24 @@ public void AddMonaiDeployMessageBrokerSubscriberService_ConfiuresServicesAsExpe fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); var serviceCollection = new Mock(); serviceCollection.Setup(p => p.Clear()); - var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem, false)); + Assert.Null(exception); + serviceCollection.Verify(p => p.Clear(), Times.Once()); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerSubscriberService configures all services as expected")] + public void AddMonaiDeployMessageBrokerSubscriberService_ConfiuresServicesAndHealthCheckAsExpected() + { + var badType = typeof(GoodSubscriberService); + var typeName = badType.AssemblyQualifiedName; + var assemblyData = GetAssemblyeBytes(badType.Assembly); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Clear()); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem, true)); Assert.Null(exception); serviceCollection.Verify(p => p.Clear(), Times.Once()); } @@ -157,12 +156,24 @@ private static byte[] GetAssemblyeBytes(Assembly assembly) } } - internal class TestSubscriberServiceRegistrar : SubscriberServiceRegistrationBase + internal class TestSubscriberHealthCheckRegistrar : SubscriberServiceHealthCheckRegistrationBase { - public TestSubscriberServiceRegistrar(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + public override IHealthChecksBuilder Configure(IHealthChecksBuilder builder, HealthStatus? failureStatus = null, IEnumerable? tags = null, TimeSpan? timeout = null) { + return builder; } + } + internal class TestPublisherHealthCheckRegistrar : PublisherServiceHealthCheckRegistrationBase + { + public override IHealthChecksBuilder Configure(IHealthChecksBuilder builder, HealthStatus? failureStatus = null, IEnumerable? tags = null, TimeSpan? timeout = null) + { + return builder; + } + } + + internal class TestSubscriberServiceRegistrar : SubscriberServiceRegistrationBase + { public override IServiceCollection Configure(IServiceCollection services) { services.Clear(); @@ -172,10 +183,6 @@ public override IServiceCollection Configure(IServiceCollection services) internal class TestPublisherServiceRegistrar : PublisherServiceRegistrationBase { - public TestPublisherServiceRegistrar(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } - public override IServiceCollection Configure(IServiceCollection services) { services.Clear(); diff --git a/src/Messaging/Tests/ServiceRegistrationBaseTests.cs b/src/Messaging/Tests/ServiceRegistrationBaseTests.cs deleted file mode 100644 index 313c00b..0000000 --- a/src/Messaging/Tests/ServiceRegistrationBaseTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 MONAI Consortium - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using System; -using Microsoft.Extensions.DependencyInjection; -using Monai.Deploy.Messaging.Configuration; -using Xunit; - -namespace Monai.Deploy.Messaging.Tests -{ - internal class TestServiceRegistration : ServiceRegistrationBase - { - public TestServiceRegistration(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } - - public override IServiceCollection Configure(IServiceCollection services) => throw new NotImplementedException(); - } - - public class ServiceRegistrationBaseTests - { - [Theory(DisplayName = "ParseAssemblyName - throws if fully qualified assembly name is invalid")] - [InlineData("mytype")] - [InlineData("mytype,, myversion")] - public void ParseAssemblyName_ThrowIfFullyQualifiedAssemblyNameIsInvalid(string assemblyeName) - { - Assert.Throws(() => new TestPublisherServiceRegistrar(assemblyeName)); - } - } -} diff --git a/src/Messaging/Tests/TaskCancellationEventTests.cs b/src/Messaging/Tests/TaskCancellationEventTests.cs index 0d7fb67..6a86eb3 100644 --- a/src/Messaging/Tests/TaskCancellationEventTests.cs +++ b/src/Messaging/Tests/TaskCancellationEventTests.cs @@ -52,6 +52,5 @@ public void ValidationThrowsOnError() var exception = Record.Exception(() => runnerComplete.Validate()); Assert.Null(exception); } - } } diff --git a/src/Plugins/RabbitMQ/ConfigurationKeys.cs b/src/Plugins/RabbitMQ/ConfigurationKeys.cs index a161bb2..81f727d 100644 --- a/src/Plugins/RabbitMQ/ConfigurationKeys.cs +++ b/src/Plugins/RabbitMQ/ConfigurationKeys.cs @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + namespace Monai.Deploy.Messaging.RabbitMQ { internal static class ConfigurationKeys { + public static readonly string PublisherServiceName = "Rabbit MQ Publisher"; + public static readonly string SubscriberServiceName = "Rabbit MQ Subscriber"; + public static readonly string EndPoint = "endpoint"; public static readonly string Username = "username"; public static readonly string Password = "password"; diff --git a/src/Plugins/RabbitMQ/InternalVisible.cs b/src/Plugins/RabbitMQ/InternalVisible.cs index 0456e61..00c2435 100644 --- a/src/Plugins/RabbitMQ/InternalVisible.cs +++ b/src/Plugins/RabbitMQ/InternalVisible.cs @@ -17,3 +17,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Monai.Deploy.Messaging.RabbitMQ.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Plugins/RabbitMQ/Logger.cs b/src/Plugins/RabbitMQ/Logger.cs index 663cf05..e187145 100644 --- a/src/Plugins/RabbitMQ/Logger.cs +++ b/src/Plugins/RabbitMQ/Logger.cs @@ -58,5 +58,8 @@ public static partial class Logger [LoggerMessage(EventId = 10011, Level = LogLevel.Error, Message = "Exception thrown: Message ID={messageId}.")] public static partial void Exception(this ILogger logger, string messageId, Exception ex); + + [LoggerMessage(EventId = 10012, Level = LogLevel.Error, Message = "Health check failure.")] + public static partial void HealthCheckError(this ILogger logger, Exception ex); } } diff --git a/src/Plugins/RabbitMQ/PublisherServicHealthCheckBuilder.cs b/src/Plugins/RabbitMQ/PublisherServicHealthCheckBuilder.cs new file mode 100644 index 0000000..0e65c86 --- /dev/null +++ b/src/Plugins/RabbitMQ/PublisherServicHealthCheckBuilder.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.Messaging.Configuration; + +namespace Monai.Deploy.Messaging.RabbitMQ +{ + public class PublisherServicHealthCheckBuilder : PublisherServiceHealthCheckRegistrationBase + { + public override IHealthChecksBuilder Configure( + IHealthChecksBuilder builder, + HealthStatus? failureStatus = null, + IEnumerable? tags = null, + TimeSpan? timeout = null) + { + builder.Add(new HealthCheckRegistration( + ConfigurationKeys.PublisherServiceName, + serviceProvider => + { + var options = serviceProvider.GetRequiredService>(); + var logger = serviceProvider.GetRequiredService>(); + var connectionFactory = serviceProvider.GetRequiredService(); + return new RabbitMQHealthCheck(connectionFactory, options.Value.PublisherSettings, logger, RabbitMQMessagePublisherService.ValidateConfiguration); + }, + failureStatus, + tags, + timeout)); + return builder; + } + } +} diff --git a/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs b/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs index 83eac38..82eb40a 100644 --- a/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs +++ b/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs @@ -21,10 +21,6 @@ namespace Monai.Deploy.Messaging.RabbitMQ { public class PublisherServiceRegistration : PublisherServiceRegistrationBase { - public PublisherServiceRegistration(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } - public override IServiceCollection Configure(IServiceCollection services) { return services diff --git a/src/Plugins/RabbitMQ/RabbitMQHealthCheck.cs b/src/Plugins/RabbitMQ/RabbitMQHealthCheck.cs new file mode 100644 index 0000000..c1975da --- /dev/null +++ b/src/Plugins/RabbitMQ/RabbitMQHealthCheck.cs @@ -0,0 +1,66 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.Messaging.RabbitMQ +{ + internal class RabbitMQHealthCheck : IHealthCheck + { + private readonly IRabbitMQConnectionFactory _connectionFactory; + private readonly Dictionary _options; + private readonly ILogger _logger; + + public RabbitMQHealthCheck( + IRabbitMQConnectionFactory connectionFactory, + Dictionary options, + ILogger logger, + Action> validator) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + validator(options); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new()) + { + try + { + _ = _connectionFactory.CreateChannel( + _options[ConfigurationKeys.EndPoint], + _options[ConfigurationKeys.Username], + _options[ConfigurationKeys.Password], + _options[ConfigurationKeys.VirtualHost], + _options.ContainsKey(ConfigurationKeys.UseSSL) ? _options[ConfigurationKeys.UseSSL] : string.Empty, + _options.ContainsKey(ConfigurationKeys.Port) ? _options[ConfigurationKeys.Port] : string.Empty); + + return await Task.FromResult(HealthCheckResult.Healthy()).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.HealthCheckError(ex); + return await Task.FromResult(HealthCheckResult.Unhealthy(exception: ex)).ConfigureAwait(false); + } + } + } +} diff --git a/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs b/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs index e615ded..a0eb267 100644 --- a/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs +++ b/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System; +using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using Ardalis.GuardClauses; @@ -42,7 +43,7 @@ public class RabbitMQMessagePublisherService : IMessageBrokerPublisherService private readonly string _portNumber; private bool _disposedValue; - public string Name => "Rabbit MQ Publisher"; + public string Name => ConfigurationKeys.PublisherServiceName; public RabbitMQMessagePublisherService(IOptions options, ILogger logger, @@ -54,7 +55,7 @@ public RabbitMQMessagePublisherService(IOptions configuration) { Guard.Against.Null(configuration, nameof(configuration)); - Guard.Against.Null(configuration.PublisherSettings, nameof(configuration.PublisherSettings)); foreach (var key in ConfigurationKeys.PublisherRequiredKeys) { - if (!configuration.PublisherSettings.ContainsKey(key)) + if (!configuration.ContainsKey(key)) { - throw new ConfigurationException($"{Name} is missing configuration for {key}."); + throw new ConfigurationException($"{ConfigurationKeys.PublisherServiceName} is missing configuration for {key}."); } } } diff --git a/src/Plugins/RabbitMQ/RabbitMqMessageSubscriberService.cs b/src/Plugins/RabbitMQ/RabbitMqMessageSubscriberService.cs index 17d11cf..5bf2c06 100644 --- a/src/Plugins/RabbitMQ/RabbitMqMessageSubscriberService.cs +++ b/src/Plugins/RabbitMQ/RabbitMqMessageSubscriberService.cs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - + using System; using System.Collections.Generic; using System.Globalization; @@ -45,7 +45,7 @@ public class RabbitMQMessageSubscriberService : IMessageBrokerSubscriberService private readonly IModel _channel; private bool _disposedValue; - public string Name => "Rabbit MQ Subscriber"; + public string Name => ConfigurationKeys.SubscriberServiceName; public RabbitMQMessageSubscriberService(IOptions options, ILogger logger, @@ -56,15 +56,15 @@ public RabbitMQMessageSubscriberService(IOptions configuration) { Guard.Against.Null(configuration, nameof(configuration)); - Guard.Against.Null(configuration.SubscriberSettings, nameof(configuration.SubscriberSettings)); foreach (var key in ConfigurationKeys.SubscriberRequiredKeys) { - if (!configuration.SubscriberSettings.ContainsKey(key)) + if (!configuration.ContainsKey(key)) { - throw new ConfigurationException($"{Name} is missing configuration for {key}."); + throw new ConfigurationException($"{ConfigurationKeys.SubscriberServiceName} is missing configuration for {key}."); } } - if (!int.TryParse(configuration.SubscriberSettings[ConfigurationKeys.DeliveryLimit], out var deliveryLimit)) + if (!int.TryParse(configuration[ConfigurationKeys.DeliveryLimit], out var deliveryLimit)) { - throw new ConfigurationException($"{Name} has a non int value for {ConfigurationKeys.DeliveryLimit}"); + throw new ConfigurationException($"{ConfigurationKeys.SubscriberServiceName} has a non int value for {ConfigurationKeys.DeliveryLimit}"); } - if (!int.TryParse(configuration.SubscriberSettings[ConfigurationKeys.RequeueDelay], out var requeueDelay)) + if (!int.TryParse(configuration[ConfigurationKeys.RequeueDelay], out var requeueDelay)) { - throw new ConfigurationException($"{Name} has a non int value for {ConfigurationKeys.RequeueDelay}"); + throw new ConfigurationException($"{ConfigurationKeys.SubscriberServiceName} has a non int value for {ConfigurationKeys.RequeueDelay}"); } if (deliveryLimit < 0 || requeueDelay < 0) { - throw new ConfigurationException($"{Name} has int values of less than 1"); + throw new ConfigurationException($"{ConfigurationKeys.SubscriberServiceName} has int values of less than 1"); } } @@ -221,7 +220,7 @@ public void SubscribeAsync(string[] topics, string queue, Func? tags = null, + TimeSpan? timeout = null) + { + builder.Add(new HealthCheckRegistration( + ConfigurationKeys.SubscriberServiceName, + serviceProvider => + { + var options = serviceProvider.GetRequiredService>(); + var logger = serviceProvider.GetRequiredService>(); + var connectionFactory = serviceProvider.GetRequiredService(); + return new RabbitMQHealthCheck(connectionFactory, options.Value.SubscriberSettings, logger, RabbitMQMessageSubscriberService.ValidateConfiguration); + }, + failureStatus, + tags, + timeout)); + return builder; + } + } +} diff --git a/src/Plugins/RabbitMQ/SubscriberServiceRegistration.cs b/src/Plugins/RabbitMQ/SubscriberServiceRegistration.cs index b9b36f4..7a8a466 100644 --- a/src/Plugins/RabbitMQ/SubscriberServiceRegistration.cs +++ b/src/Plugins/RabbitMQ/SubscriberServiceRegistration.cs @@ -21,10 +21,6 @@ namespace Monai.Deploy.Messaging.RabbitMQ { public class SubscriberServiceRegistration : SubscriberServiceRegistrationBase { - public SubscriberServiceRegistration(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) - { - } - public override IServiceCollection Configure(IServiceCollection services) { services.AddSingleton(); diff --git a/src/Plugins/RabbitMQ/Tests/RabbitMQHealthCheckTest.cs b/src/Plugins/RabbitMQ/Tests/RabbitMQHealthCheckTest.cs new file mode 100644 index 0000000..6e69602 --- /dev/null +++ b/src/Plugins/RabbitMQ/Tests/RabbitMQHealthCheckTest.cs @@ -0,0 +1,66 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Monai.Deploy.Messaging.RabbitMQ.Tests +{ + public class RabbitMQHealthCheckTest + { + private readonly Mock _connectionFactory; + private readonly Mock> _logger; + private readonly Dictionary _options; + + public RabbitMQHealthCheckTest() + { + _connectionFactory = new Mock(); + _logger = new Mock>(); + _options = new Dictionary(); + _options.Add(ConfigurationKeys.EndPoint, ConfigurationKeys.EndPoint); + _options.Add(ConfigurationKeys.Username, ConfigurationKeys.Username); + _options.Add(ConfigurationKeys.Password, ConfigurationKeys.Password); + _options.Add(ConfigurationKeys.VirtualHost, ConfigurationKeys.VirtualHost); + } + + [Fact] + public async Task CheckHealthAsync_WhenFailedToListBucket_ReturnUnhealthy() + { + _connectionFactory.Setup(p => p.CreateChannel(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception("error")); + + var healthCheck = new RabbitMQHealthCheck(_connectionFactory.Object, _options, _logger.Object, (d) => { }); + var results = await healthCheck.CheckHealthAsync(new HealthCheckContext()).ConfigureAwait(false); + + Assert.Equal(HealthStatus.Unhealthy, results.Status); + Assert.NotNull(results.Exception); + Assert.Equal("error", results.Exception.Message); + } + + [Fact] + public async Task CheckHealthAsync_WhenListBucketSucceeds_ReturnHealthy() + { + _connectionFactory.Setup(p => p.CreateChannel(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + var healthCheck = new RabbitMQHealthCheck(_connectionFactory.Object, _options, _logger.Object, (d) => { }); + var results = await healthCheck.CheckHealthAsync(new HealthCheckContext()).ConfigureAwait(false); + + Assert.Equal(HealthStatus.Healthy, results.Status); + Assert.Null(results.Exception); + } + } +} diff --git a/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs b/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs index f77a2ef..06715b1 100644 --- a/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs +++ b/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs @@ -17,6 +17,7 @@ using System.IO.Abstractions.TestingHelpers; using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Monai.Deploy.Messaging.Common; using Monai.Deploy.Messaging.Events; using Moq; @@ -25,9 +26,9 @@ namespace Monai.Deploy.Messaging.RabbitMQ.Tests { #pragma warning disable CS8604 // Possible null reference argument. + public class ValidationTest { - [Fact(DisplayName = "Validates TaskUpdateEvent")] public void TaskUpdateEventTest() { @@ -42,35 +43,62 @@ public void TaskUpdateEventTest() public class PublisherServiceRegistrationTest : ServiceRegistrationTest { - [Fact(DisplayName = "Shall be able to Add MinIO as default storage service")] + [Fact] public void ShallAddRabbitMQAsDefaultMessagingService() { var serviceCollection = new Mock(); serviceCollection.Setup(p => p.Add(It.IsAny())); - var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(ServiceType.AssemblyQualifiedName, FileSystem); + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(ServiceType.AssemblyQualifiedName, FileSystem, false); Assert.Same(serviceCollection.Object, returnedServiceCollection); serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); } - } + [Fact] + public void ShallAddRabbitMQAsDefaultMessagingServicAndStorageHealthCheckse() + { + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(ServiceType.AssemblyQualifiedName, FileSystem, true); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.AtLeast(3)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(HealthCheckService))), Times.Once()); + } + } public class SubscriberServiceRegistrationTest : ServiceRegistrationTest { - [Fact(DisplayName = "Shall be able to Add MinIO as default storage service")] - public void ShallAddRabbitMQAsDefaultMessagingService() + [Fact] + public void AddMonaiDeployMessageBrokerSubscriberService_WhenCalled_RegisterServicesOnly() { var serviceCollection = new Mock(); serviceCollection.Setup(p => p.Add(It.IsAny())); - var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(ServiceType.AssemblyQualifiedName, FileSystem); + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(ServiceType.AssemblyQualifiedName, FileSystem, false); Assert.Same(serviceCollection.Object, returnedServiceCollection); serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); } + + [Fact] + public void AddMonaiDeployMessageBrokerSubscriberService_WhenCalled_RegisterServicesAndHealthChecks() + { + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(ServiceType.AssemblyQualifiedName, FileSystem, true); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.AtLeast(3)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(HealthCheckService))), Times.Once()); + } } public abstract class ServiceRegistrationTest