Skip to content

Commit

Permalink
Messaging service extension for health check service (#63)
Browse files Browse the repository at this point in the history
* Add health check for RabbitMQ

Signed-off-by: Victor Chang <[email protected]>
  • Loading branch information
mocsharp authored Aug 31, 2022
1 parent 64222e2 commit a9ec685
Show file tree
Hide file tree
Showing 28 changed files with 498 additions and 185 deletions.
3 changes: 2 additions & 1 deletion .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ header:
- '**/*.ruleset'
- 'src/.sonarlint/**'
- 'src/coverlet.runsettings'
- 'src/Monai.Deploy.Messaging.sln'

comment: on-failure

Expand All @@ -28,4 +29,4 @@ header:
Config:
extensions:
- ".conf"
comment_style_id: Hashtag
comment_style_id: Hashtag
16 changes: 15 additions & 1 deletion doc/dependency_decisions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Messaging/Configuration/ConfigurationException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Messaging/Events/ExportCompleteEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Messaging/Events/ExportRequestEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion src/Messaging/Events/ExportStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/


namespace Monai.Deploy.Messaging.Events
{
public enum ExportStatus
Expand Down
1 change: 0 additions & 1 deletion src/Messaging/Events/FailureReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/


namespace Monai.Deploy.Messaging.Events
{
public enum FailureReason
Expand Down
2 changes: 1 addition & 1 deletion src/Messaging/Events/WorkflowRequestEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions src/Messaging/HealthCheckRegistrationBase.cs
Original file line number Diff line number Diff line change
@@ -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<string>? tags = null,
TimeSpan? timeout = null);
}
}
137 changes: 105 additions & 32 deletions src/Messaging/IServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,29 @@
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;

namespace Monai.Deploy.Messaging
{
public static class IServiceCollectionExtensions
{
private static IFileSystem? s_fileSystem;

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
/// </summary>
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
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<string>? tags = null,
TimeSpan? timeout = null)
=> AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem(), registerHealthCheck, failureStatus, tags, timeout);

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
Expand All @@ -45,8 +50,15 @@ public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(th
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
=> Add<IMessageBrokerSubscriberService, SubscriberServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(
this IServiceCollection services,
string fullyQualifiedTypeName,
IFileSystem fileSystem,
bool registerHealthCheck = true,
HealthStatus? failureStatus = null,
IEnumerable<string>? tags = null,
TimeSpan? timeout = null)
=> Add<IMessageBrokerSubscriberService, SubscriberServiceRegistrationBase, SubscriberServiceHealthCheckRegistrationBase>(services, fullyQualifiedTypeName, fileSystem, registerHealthCheck, failureStatus, tags, timeout);

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
Expand All @@ -55,8 +67,14 @@ public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(th
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
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<string>? tags = null,
TimeSpan? timeout = null)
=> AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem(), registerHealthCheck, failureStatus, tags, timeout);

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
Expand All @@ -66,42 +84,96 @@ public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(thi
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
=> Add<IMessageBrokerPublisherService, PublisherServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);

private static IServiceCollection Add<T, U>(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<string>? tags = null,
TimeSpan? timeout = null)
=> Add<IMessageBrokerPublisherService, PublisherServiceRegistrationBase, PublisherServiceHealthCheckRegistrationBase>(services, fullyQualifiedTypeName, fileSystem, registerHealthCheck, failureStatus, tags, timeout);

private static IServiceCollection Add<T, U, V>(
this IServiceCollection services,
string fullyQualifiedTypeName,
IFileSystem fileSystem,
bool registerHealthCheck = true,
HealthStatus? failureStatus = null,
IEnumerable<string>? 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<T>(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<T>(fullyQualifiedTypeName, serviceAssembly))
RegisterServices<U>(services, fullyQualifiedTypeName, serviceAssembly);

if (registerHealthCheck)
{
throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(T).Name} interface.");
RegisterHealtChecks<V>(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<V>(
IServiceCollection services,
string fullyQualifiedTypeName,
Assembly serviceAssembly,
HealthStatus? failureStatus,
IEnumerable<string>? 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<U>(
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<T>(string fullyQualifiedTypeName, Assembly storageServiceAssembly)
internal static bool IsSupportedType<T>(string fullyQualifiedTypeName, Assembly storageServiceAssembly)
{
Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName));
Guard.Against.Null(storageServiceAssembly, nameof(storageServiceAssembly));
Expand All @@ -112,7 +184,7 @@ private static bool IsSupportedType<T>(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]))
Expand All @@ -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);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Messaging/Monai.Deploy.Messaging.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
Expand Down
1 change: 0 additions & 1 deletion src/Messaging/SR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/


namespace Monai.Deploy.Messaging
{
internal static class SR
Expand Down
Loading

0 comments on commit a9ec685

Please sign in to comment.