Merge pull request #500 from dotnet-maestro-bot/merge/release/2.2-to-master
[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
commit
0ce437875e
|
|
@ -27,6 +27,8 @@
|
||||||
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingConsolePackageVersion>
|
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingConsolePackageVersion>
|
||||||
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingPackageVersion>
|
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingTestingPackageVersion>
|
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingTestingPackageVersion>
|
||||||
|
<MicrosoftExtensionsHostingAbstractionsPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsHostingAbstractionsPackageVersion>
|
||||||
|
<MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsOptionsPackageVersion>
|
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsOptionsPackageVersion>
|
||||||
<MicrosoftExtensionsRazorViewsSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsRazorViewsSourcesPackageVersion>
|
<MicrosoftExtensionsRazorViewsSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsRazorViewsSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsStackTraceSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsStackTraceSourcesPackageVersion>
|
<MicrosoftExtensionsStackTraceSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsStackTraceSourcesPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<configuration>
|
|
||||||
<system.webServer>
|
|
||||||
<handlers>
|
|
||||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
|
|
||||||
</handlers>
|
|
||||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
|
|
||||||
</system.webServer>
|
|
||||||
</configuration>
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a publisher of <see cref="HealthReport"/> information.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The default health checks implementation provided an <c>IHostedService</c> implementation that can
|
||||||
|
/// be used to execute health checks at regular intervals and provide the resulting <see cref="HealthReport"/>
|
||||||
|
/// data to all registered <see cref="IHealthCheckPublisher"/> instances.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// To provide an <see cref="IHealthCheckPublisher"/> implementation, register an instance or type as a singleton
|
||||||
|
/// service in the dependency injection container.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="IHealthCheckPublisher"/> instances are provided with a <see cref="HealthReport"/> after executing
|
||||||
|
/// health checks in a background thread. The use of <see cref="IHealthCheckPublisher"/> depend on hosting in
|
||||||
|
/// an application using <c>IWebHost</c> or generic host (<c>IHost</c>). Execution of <see cref="IHealthCheckPublisher"/>
|
||||||
|
/// instance is not related to execution of health checks via a middleware.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public interface IHealthCheckPublisher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes the provided <paramref name="report"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="report">The <see cref="HealthReport"/>. The result of executing a set of health checks.</param>
|
||||||
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> which will complete when publishing is complete.</returns>
|
||||||
|
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -126,19 +126,19 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class EventIds
|
||||||
|
{
|
||||||
|
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
|
||||||
|
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
|
||||||
|
|
||||||
|
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
|
||||||
|
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
|
||||||
|
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
|
||||||
|
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
|
||||||
|
}
|
||||||
|
|
||||||
private static class Log
|
private static class Log
|
||||||
{
|
{
|
||||||
public static class EventIds
|
|
||||||
{
|
|
||||||
public static readonly EventId HealthCheckProcessingBegin = new EventId(100, "HealthCheckProcessingBegin");
|
|
||||||
public static readonly EventId HealthCheckProcessingEnd = new EventId(101, "HealthCheckProcessingEnd");
|
|
||||||
|
|
||||||
public static readonly EventId HealthCheckBegin = new EventId(102, "HealthCheckBegin");
|
|
||||||
public static readonly EventId HealthCheckEnd = new EventId(103, "HealthCheckEnd");
|
|
||||||
public static readonly EventId HealthCheckError = new EventId(104, "HealthCheckError");
|
|
||||||
public static readonly EventId HealthCheckData = new EventId(105, "HealthCheckData");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
|
private static readonly Action<ILogger, Exception> _healthCheckProcessingBegin = LoggerMessage.Define(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
EventIds.HealthCheckProcessingBegin,
|
EventIds.HealthCheckProcessingBegin,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +25,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
|
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
|
||||||
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
|
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<HealthCheckService, DefaultHealthCheckService>());
|
services.TryAddSingleton<HealthCheckService, DefaultHealthCheckService>();
|
||||||
|
services.TryAddSingleton<IHostedService, HealthCheckPublisherHostedService>();
|
||||||
return new HealthChecksBuilder(services);
|
return new HealthChecksBuilder(services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
|
{
|
||||||
|
internal sealed class HealthCheckPublisherHostedService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly HealthCheckService _healthCheckService;
|
||||||
|
private readonly IOptions<HealthCheckPublisherOptions> _options;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IHealthCheckPublisher[] _publishers;
|
||||||
|
|
||||||
|
private CancellationTokenSource _stopping;
|
||||||
|
private Timer _timer;
|
||||||
|
|
||||||
|
public HealthCheckPublisherHostedService(
|
||||||
|
HealthCheckService healthCheckService,
|
||||||
|
IOptions<HealthCheckPublisherOptions> options,
|
||||||
|
ILogger<HealthCheckPublisherHostedService> logger,
|
||||||
|
IEnumerable<IHealthCheckPublisher> publishers)
|
||||||
|
{
|
||||||
|
if (healthCheckService == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(healthCheckService));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publishers == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(publishers));
|
||||||
|
}
|
||||||
|
|
||||||
|
_healthCheckService = healthCheckService;
|
||||||
|
_options = options;
|
||||||
|
_logger = logger;
|
||||||
|
_publishers = publishers.ToArray();
|
||||||
|
|
||||||
|
_stopping = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsStopping => _stopping.IsCancellationRequested;
|
||||||
|
|
||||||
|
internal bool IsTimerRunning => _timer != null;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_publishers.Length == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
|
||||||
|
// fire before other code runs.
|
||||||
|
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stopping.Cancel();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore exceptions thrown as a result of a cancellation.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_publishers.Length == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_timer?.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
|
||||||
|
private async void Timer_Tick(object state)
|
||||||
|
{
|
||||||
|
await RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal for testing
|
||||||
|
internal async Task RunAsync()
|
||||||
|
{
|
||||||
|
var duration = ValueStopwatch.StartNew();
|
||||||
|
Logger.HealthCheckPublisherProcessingBegin(_logger);
|
||||||
|
|
||||||
|
CancellationTokenSource cancellation = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var timeout = _options.Value.Timeout;
|
||||||
|
|
||||||
|
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
|
||||||
|
cancellation.CancelAfter(timeout);
|
||||||
|
|
||||||
|
await RunAsyncCore(cancellation.Token);
|
||||||
|
|
||||||
|
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (IsStopping)
|
||||||
|
{
|
||||||
|
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||||
|
// a timeout and we want to log it.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This is an error, publishing failed.
|
||||||
|
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime(), ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cancellation.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunAsyncCore(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Forcibly yield - we want to unblock the timer thread.
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
// The health checks service does it's own logging, and doesn't throw exceptions.
|
||||||
|
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken);
|
||||||
|
|
||||||
|
var publishers = _publishers;
|
||||||
|
var tasks = new Task[publishers.Length];
|
||||||
|
for (var i = 0; i < publishers.Length; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = RunPublisherAsync(publishers[i], report, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPublisherAsync(IHealthCheckPublisher publisher, HealthReport report, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var duration = ValueStopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.HealthCheckPublisherBegin(_logger, publisher);
|
||||||
|
|
||||||
|
await publisher.PublishAsync(report, cancellationToken);
|
||||||
|
Logger.HealthCheckPublisherEnd(_logger, publisher, duration.GetElapsedTime());
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (IsStopping)
|
||||||
|
{
|
||||||
|
// This is a cancellation - if the app is shutting down we want to ignore it. Otherwise, it's
|
||||||
|
// a timeout and we want to log it.
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ocex)
|
||||||
|
{
|
||||||
|
Logger.HealthCheckPublisherTimeout(_logger, publisher, duration.GetElapsedTime());
|
||||||
|
throw ocex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.HealthCheckPublisherError(_logger, publisher, duration.GetElapsedTime(), ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class EventIds
|
||||||
|
{
|
||||||
|
public static readonly EventId HealthCheckPublisherProcessingBegin = new EventId(100, "HealthCheckPublisherProcessingBegin");
|
||||||
|
public static readonly EventId HealthCheckPublisherProcessingEnd = new EventId(101, "HealthCheckPublisherProcessingEnd");
|
||||||
|
public static readonly EventId HealthCheckPublisherProcessingError = new EventId(101, "HealthCheckPublisherProcessingError");
|
||||||
|
|
||||||
|
public static readonly EventId HealthCheckPublisherBegin = new EventId(102, "HealthCheckPublisherBegin");
|
||||||
|
public static readonly EventId HealthCheckPublisherEnd = new EventId(103, "HealthCheckPublisherEnd");
|
||||||
|
public static readonly EventId HealthCheckPublisherError = new EventId(104, "HealthCheckPublisherError");
|
||||||
|
public static readonly EventId HealthCheckPublisherTimeout = new EventId(104, "HealthCheckPublisherTimeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Logger
|
||||||
|
{
|
||||||
|
private static readonly Action<ILogger, Exception> _healthCheckPublisherProcessingBegin = LoggerMessage.Define(
|
||||||
|
LogLevel.Debug,
|
||||||
|
EventIds.HealthCheckPublisherProcessingBegin,
|
||||||
|
"Running health check publishers");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, double, Exception> _healthCheckPublisherProcessingEnd = LoggerMessage.Define<double>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
EventIds.HealthCheckPublisherProcessingEnd,
|
||||||
|
"Health check publisher processing completed after {ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, IHealthCheckPublisher, Exception> _healthCheckPublisherBegin = LoggerMessage.Define<IHealthCheckPublisher>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
EventIds.HealthCheckPublisherBegin,
|
||||||
|
"Running health check publisher '{HealthCheckPublisher}'");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherEnd = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
EventIds.HealthCheckPublisherEnd,
|
||||||
|
"Health check '{HealthCheckPublisher}' completed after {ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherError = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||||
|
LogLevel.Error,
|
||||||
|
EventIds.HealthCheckPublisherError,
|
||||||
|
"Health check {HealthCheckPublisher} threw an unhandled exception after {ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, IHealthCheckPublisher, double, Exception> _healthCheckPublisherTimeout = LoggerMessage.Define<IHealthCheckPublisher, double>(
|
||||||
|
LogLevel.Error,
|
||||||
|
EventIds.HealthCheckPublisherTimeout,
|
||||||
|
"Health check {HealthCheckPublisher} was canceled after {ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherProcessingBegin(ILogger logger)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherProcessingBegin(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherProcessingEnd(ILogger logger, TimeSpan duration, Exception exception = null)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherProcessingEnd(logger, duration.TotalMilliseconds, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherBegin(ILogger logger, IHealthCheckPublisher publisher)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherBegin(logger, publisher, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherEnd(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherEnd(logger, publisher, duration.TotalMilliseconds, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherError(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration, Exception exception)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherError(logger, publisher, duration.TotalMilliseconds, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HealthCheckPublisherTimeout(ILogger logger, IHealthCheckPublisher publisher, TimeSpan duration)
|
||||||
|
{
|
||||||
|
_healthCheckPublisherTimeout(logger, publisher, duration.TotalMilliseconds, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for the default service that executes <see cref="IHealthCheckPublisher"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HealthCheckPublisherOptions
|
||||||
|
{
|
||||||
|
private TimeSpan _delay;
|
||||||
|
private TimeSpan _period;
|
||||||
|
|
||||||
|
public HealthCheckPublisherOptions()
|
||||||
|
{
|
||||||
|
_delay = TimeSpan.FromSeconds(5);
|
||||||
|
_period = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the initial delay applied after the application starts before executing
|
||||||
|
/// <see cref="IHealthCheckPublisher"/> instances. The delay is applied once at startup, and does
|
||||||
|
/// not apply to subsequent iterations. The default value is 5 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Delay
|
||||||
|
{
|
||||||
|
get => _delay;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"The {nameof(Delay)} must not be infinite.", nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_delay = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the period of <see cref="IHealthCheckPublisher"/> execution. The default value is
|
||||||
|
/// 30 seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The <see cref="Period"/> cannot be set to a value lower than 1 second.
|
||||||
|
/// </remarks>
|
||||||
|
public TimeSpan Period
|
||||||
|
{
|
||||||
|
get => _period;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"The {nameof(Period)} must be greater than or equal to one second.", nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == System.Threading.Timeout.InfiniteTimeSpan)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"The {nameof(Period)} must not be infinite.", nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_delay = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a predicate that is used to filter the set of health checks executed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If <see cref="Predicate"/> is <c>null</c>, the health check publisher service will run all
|
||||||
|
/// registered health checks - this is the default behavior. To run a subset of health checks,
|
||||||
|
/// provide a function that filters the set of checks. The predicate will be evaluated each period.
|
||||||
|
/// </remarks>
|
||||||
|
public Func<HealthCheckRegistration, bool> Predicate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timeout for executing the health checks an all <see cref="IHealthCheckPublisher"/>
|
||||||
|
/// instances. Use <see cref="System.Threading.Timeout.InfiniteTimeSpan"/> to execute with no timeout.
|
||||||
|
/// The default value is 30 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,8 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
|
||||||
<PackageTags>diagnostics;healthchecks</PackageTags>
|
<PackageTags>diagnostics;healthchecks</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsHostingAbstractionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.NonCapturingTimer.Sources" Version="$(MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion)" PrivateAssets="All" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
@ -19,7 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.AddHealthChecks();
|
services.AddHealthChecks();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(services,
|
Assert.Collection(services.OrderBy(s => s.ServiceType.FullName),
|
||||||
actual =>
|
actual =>
|
||||||
{
|
{
|
||||||
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||||
|
|
@ -27,6 +29,14 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
|
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
|
||||||
Assert.Null(actual.ImplementationInstance);
|
Assert.Null(actual.ImplementationInstance);
|
||||||
Assert.Null(actual.ImplementationFactory);
|
Assert.Null(actual.ImplementationFactory);
|
||||||
|
},
|
||||||
|
actual =>
|
||||||
|
{
|
||||||
|
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||||
|
Assert.Equal(typeof(IHostedService), actual.ServiceType);
|
||||||
|
Assert.Equal(typeof(HealthCheckPublisherHostedService), actual.ImplementationType);
|
||||||
|
Assert.Null(actual.ImplementationInstance);
|
||||||
|
Assert.Null(actual.ImplementationFactory);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,528 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
|
{
|
||||||
|
public class HealthCheckPublisherHostedServiceTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WithoutPublishers_DoesNotStartTimer()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var publishers = new IHealthCheckPublisher[]
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WithPublishers_StartsTimer()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var publishers = new IHealthCheckPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Wait = unblock0.Task, },
|
||||||
|
new TestPublisher() { Wait = unblock1.Task, },
|
||||||
|
new TestPublisher() { Wait = unblock2.Task, },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, configure: (options) =>
|
||||||
|
{
|
||||||
|
options.Delay = TimeSpan.FromMilliseconds(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
unblock0.SetResult(null);
|
||||||
|
unblock1.SetResult(null);
|
||||||
|
unblock2.SetResult(null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StopAsync_CancelsExecution()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Wait = unblock.Task, }
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Start execution
|
||||||
|
var running = service.RunAsync();
|
||||||
|
|
||||||
|
// Wait for the publisher to see the cancellation token
|
||||||
|
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
Assert.Single(publishers[0].Entries);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.StopAsync(); // Trigger cancellation
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
|
||||||
|
unblock.SetResult(null);
|
||||||
|
|
||||||
|
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_WaitsForCompletion_Single()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sink = new TestSink();
|
||||||
|
|
||||||
|
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Wait = unblock.Task, },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, sink: sink);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var running = service.RunAsync();
|
||||||
|
|
||||||
|
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
unblock.SetResult(null);
|
||||||
|
|
||||||
|
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
|
||||||
|
for (var i = 0; i < publishers.Length; i++)
|
||||||
|
{
|
||||||
|
var report = Assert.Single(publishers[i].Entries).report;
|
||||||
|
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
sink.Writes,
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not testing logs here to avoid differences in logging order
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_WaitsForCompletion_Multiple()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var unblock0 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var unblock1 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var unblock2 = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Wait = unblock0.Task, },
|
||||||
|
new TestPublisher() { Wait = unblock1.Task, },
|
||||||
|
new TestPublisher() { Wait = unblock2.Task, },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var running = service.RunAsync();
|
||||||
|
|
||||||
|
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
await publishers[2].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
unblock0.SetResult(null);
|
||||||
|
unblock1.SetResult(null);
|
||||||
|
unblock2.SetResult(null);
|
||||||
|
|
||||||
|
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
|
||||||
|
for (var i = 0; i < publishers.Length; i++)
|
||||||
|
{
|
||||||
|
var report = Assert.Single(publishers[i].Entries).report;
|
||||||
|
Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_PublishersCanTimeout()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sink = new TestSink();
|
||||||
|
var unblock = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Wait = unblock.Task, },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, sink: sink, configure: (options) =>
|
||||||
|
{
|
||||||
|
options.Timeout = TimeSpan.FromMilliseconds(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var running = service.RunAsync();
|
||||||
|
|
||||||
|
await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
await AssertCancelledAsync(publishers[0].Entries[0].cancellationToken);
|
||||||
|
|
||||||
|
unblock.SetResult(null);
|
||||||
|
|
||||||
|
await running.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(service.IsTimerRunning);
|
||||||
|
Assert.False(service.IsStopping);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
sink.Writes,
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherTimeout, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_CanFilterHealthChecks()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher(),
|
||||||
|
new TestPublisher(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, configure: (options) =>
|
||||||
|
{
|
||||||
|
options.Predicate = (r) => r.Name == "one";
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (var i = 0; i < publishers.Length; i++)
|
||||||
|
{
|
||||||
|
var report = Assert.Single(publishers[i].Entries).report;
|
||||||
|
Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_HandlesExceptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sink = new TestSink();
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, sink: sink);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
sink.Writes,
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherError, entry.EventId); },
|
||||||
|
entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherProcessingEnd, entry.EventId); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not testing logging here to avoid flaky ordering issues
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_HandlesExceptions_Multiple()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sink = new TestSink();
|
||||||
|
var publishers = new TestPublisher[]
|
||||||
|
{
|
||||||
|
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||||
|
new TestPublisher(),
|
||||||
|
new TestPublisher() { Exception = new InvalidTimeZoneException(), },
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = CreateService(publishers, sink: sink);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await service.StopAsync();
|
||||||
|
Assert.False(service.IsTimerRunning);
|
||||||
|
Assert.True(service.IsStopping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HealthCheckPublisherHostedService CreateService(
|
||||||
|
IHealthCheckPublisher[] publishers,
|
||||||
|
Action<HealthCheckPublisherOptions> configure = null,
|
||||||
|
TestSink sink = null)
|
||||||
|
{
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
serviceCollection.AddOptions();
|
||||||
|
serviceCollection.AddLogging();
|
||||||
|
serviceCollection.AddHealthChecks()
|
||||||
|
.AddCheck("one", () => { return HealthCheckResult.Passed(); })
|
||||||
|
.AddCheck("two", () => { return HealthCheckResult.Passed(); });
|
||||||
|
|
||||||
|
// Choosing big values for tests to make sure that we're not dependent on the defaults.
|
||||||
|
// All of the tests that rely on the timer will set their own values for speed.
|
||||||
|
serviceCollection.Configure<HealthCheckPublisherOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Delay = TimeSpan.FromMinutes(5);
|
||||||
|
options.Period = TimeSpan.FromMinutes(5);
|
||||||
|
options.Timeout = TimeSpan.FromMinutes(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (publishers != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < publishers.Length; i++)
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton<IHealthCheckPublisher>(publishers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configure != null)
|
||||||
|
{
|
||||||
|
serviceCollection.Configure(configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sink != null)
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton<ILoggerFactory>(new TestLoggerFactory(sink, enabled: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = serviceCollection.BuildServiceProvider();
|
||||||
|
return services.GetServices<IHostedService>().OfType< HealthCheckPublisherHostedService>().Single();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AssertCancelledAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPublisher : IHealthCheckPublisher
|
||||||
|
{
|
||||||
|
private TaskCompletionSource<object> _started;
|
||||||
|
|
||||||
|
public TestPublisher()
|
||||||
|
{
|
||||||
|
_started = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<(HealthReport report, CancellationToken cancellationToken)> Entries { get; } = new List<(HealthReport report, CancellationToken cancellationToken)>();
|
||||||
|
|
||||||
|
public Exception Exception { get; set; }
|
||||||
|
|
||||||
|
public Task Started => _started.Task;
|
||||||
|
|
||||||
|
public Task Wait { get; set; }
|
||||||
|
|
||||||
|
public async Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Entries.Add((report, cancellationToken));
|
||||||
|
|
||||||
|
// Signal that we've started
|
||||||
|
_started.SetResult(null);
|
||||||
|
|
||||||
|
if (Wait != null)
|
||||||
|
{
|
||||||
|
await Wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Exception != null)
|
||||||
|
{
|
||||||
|
throw Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue