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:
Ryan Nowak 2018-10-09 13:41:53 -07:00 committed by GitHub
commit 0ce437875e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 944 additions and 25 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
});
}
}

View File

@ -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();
}
}
}
}