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>
|
||||
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsLoggingPackageVersion>
|
||||
<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>
|
||||
<MicrosoftExtensionsRazorViewsSourcesPackageVersion>3.0.0-alpha1-10549</MicrosoftExtensionsRazorViewsSourcesPackageVersion>
|
||||
<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
|
||||
{
|
||||
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(
|
||||
LogLevel.Debug,
|
||||
EventIds.HealthCheckProcessingBegin,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
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>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsHostingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.NonCapturingTimer.Sources" Version="$(MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion)" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||
<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.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
|
|
@ -19,7 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.AddHealthChecks();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(services,
|
||||
Assert.Collection(services.OrderBy(s => s.ServiceType.FullName),
|
||||
actual =>
|
||||
{
|
||||
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
|
||||
|
|
@ -27,6 +29,14 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
Assert.Equal(typeof(DefaultHealthCheckService), actual.ImplementationType);
|
||||
Assert.Null(actual.ImplementationInstance);
|
||||
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