diff --git a/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs index 7fb5beb0e9..9ab497257e 100644 --- a/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs +++ b/src/HealthChecks/Abstractions/ref/Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.netstandard2.0.cs @@ -11,11 +11,14 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks public sealed partial class HealthCheckRegistration { public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { } + public HealthCheckRegistration(string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan? timeout) { } public HealthCheckRegistration(string name, System.Func factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { } + public HealthCheckRegistration(string name, System.Func factory, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan? timeout) { } public System.Func Factory { get { throw null; } set { } } public Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus FailureStatus { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string Name { get { throw null; } set { } } public System.Collections.Generic.ISet Tags { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.TimeSpan Timeout { get { throw null; } set { } } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public partial struct HealthCheckResult diff --git a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs index 9291c38846..8ee11e3195 100644 --- a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs +++ b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -24,6 +24,22 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks { private Func _factory; private string _name; + private TimeSpan _timeout; + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// The instance. + /// + /// The that should be reported upon failure of the health check. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + : this(name, instance, failureStatus, tags, default) + { + } /// /// Creates a new for an existing instance. @@ -35,7 +51,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// is null, then will be reported. /// /// A list of tags that can be used for filtering health checks. - public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags) + /// An optional representing the timeout of the check. + public HealthCheckRegistration(string name, IHealthCheck instance, HealthStatus? failureStatus, IEnumerable tags, TimeSpan? timeout) { if (name == null) { @@ -47,10 +64,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks throw new ArgumentNullException(nameof(instance)); } + if (timeout <= TimeSpan.Zero && timeout != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + Name = name; FailureStatus = failureStatus ?? HealthStatus.Unhealthy; Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); Factory = (_) => instance; + Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan; } /// @@ -68,6 +91,27 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks Func factory, HealthStatus? failureStatus, IEnumerable tags) + : this(name, factory, failureStatus, tags, default) + { + } + + /// + /// Creates a new for an existing instance. + /// + /// The health check name. + /// A delegate used to create the instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used for filtering health checks. + /// An optional representing the timeout of the check. + public HealthCheckRegistration( + string name, + Func factory, + HealthStatus? failureStatus, + IEnumerable tags, + TimeSpan? timeout) { if (name == null) { @@ -79,10 +123,16 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks throw new ArgumentNullException(nameof(factory)); } + if (timeout <= TimeSpan.Zero && timeout != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + Name = name; FailureStatus = failureStatus ?? HealthStatus.Unhealthy; Tags = new HashSet(tags ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); Factory = factory; + Timeout = timeout ?? System.Threading.Timeout.InfiniteTimeSpan; } /// @@ -107,6 +157,23 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks /// public HealthStatus FailureStatus { get; set; } + /// + /// Gets or sets the timeout used for the test. + /// + public TimeSpan Timeout + { + get => _timeout; + set + { + if (value <= TimeSpan.Zero && value != System.Threading.Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _timeout = value; + } + } + /// /// Gets or sets the health check name. /// diff --git a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs index 58bdc80602..a23961efdd 100644 --- a/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs +++ b/src/HealthChecks/HealthChecks/ref/Microsoft.Extensions.Diagnostics.HealthChecks.netstandard2.0.cs @@ -5,18 +5,25 @@ namespace Microsoft.Extensions.DependencyInjection { public static partial class HealthChecksBuilderAddCheckExtensions { - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null) { throw null; } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck instance, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default(Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?), System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, System.Collections.Generic.IEnumerable tags, System.TimeSpan timeout, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddTypeActivatedCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, params object[] args) where T : class, Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck { throw null; } } public static partial class HealthChecksBuilderDelegateExtensions { - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null) { throw null; } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null) { throw null; } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null) { throw null; } - public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddAsyncCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func> check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddCheck(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string name, System.Func check, System.Collections.Generic.IEnumerable tags = null, System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } } public static partial class HealthCheckServiceCollectionExtensions { diff --git a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs index d5d71d9cb4..56ce966d18 100644 --- a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs +++ b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs @@ -40,74 +40,118 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks CancellationToken cancellationToken = default) { var registrations = _options.Value.Registrations; + if (predicate != null) + { + registrations = registrations.Where(predicate).ToArray(); + } + var totalTime = ValueStopwatch.StartNew(); + Log.HealthCheckProcessingBegin(_logger); + + var tasks = new Task[registrations.Count]; + var index = 0; using (var scope = _scopeFactory.CreateScope()) { - var context = new HealthCheckContext(); - var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var totalTime = ValueStopwatch.StartNew(); - Log.HealthCheckProcessingBegin(_logger); - foreach (var registration in registrations) { - if (predicate != null && !predicate(registration)) - { - continue; - } - - cancellationToken.ThrowIfCancellationRequested(); - - var healthCheck = registration.Factory(scope.ServiceProvider); - - // If the health check does things like make Database queries using EF or backend HTTP calls, - // it may be valuable to know that logs it generates are part of a health check. So we start a scope. - using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) - { - var stopwatch = ValueStopwatch.StartNew(); - context.Registration = registration; - - Log.HealthCheckBegin(_logger, registration); - - HealthReportEntry entry; - try - { - var result = await healthCheck.CheckHealthAsync(context, cancellationToken); - var duration = stopwatch.GetElapsedTime(); - - entry = new HealthReportEntry( - status: result.Status, - description: result.Description, - duration: duration, - exception: result.Exception, - data: result.Data); - - Log.HealthCheckEnd(_logger, registration, entry, duration); - Log.HealthCheckData(_logger, registration, entry); - } - - // Allow cancellation to propagate. - catch (Exception ex) when (ex as OperationCanceledException == null) - { - var duration = stopwatch.GetElapsedTime(); - entry = new HealthReportEntry( - status: HealthStatus.Unhealthy, - description: ex.Message, - duration: duration, - exception: ex, - data: null); - - Log.HealthCheckError(_logger, registration, ex, duration); - } - - entries[registration.Name] = entry; - } + tasks[index++] = RunCheckAsync(scope, registration, cancellationToken); } - var totalElapsedTime = totalTime.GetElapsedTime(); - var report = new HealthReport(entries, totalElapsedTime); - Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); - return report; + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + index = 0; + var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var registration in registrations) + { + entries[registration.Name] = tasks[index++].Result; + } + + var totalElapsedTime = totalTime.GetElapsedTime(); + var report = new HealthReport(entries, totalElapsedTime); + Log.HealthCheckProcessingEnd(_logger, report.Status, totalElapsedTime); + return report; + } + + private async Task RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken) + { + await Task.Yield(); + + cancellationToken.ThrowIfCancellationRequested(); + + var healthCheck = registration.Factory(scope.ServiceProvider); + + // If the health check does things like make Database queries using EF or backend HTTP calls, + // it may be valuable to know that logs it generates are part of a health check. So we start a scope. + using (_logger.BeginScope(new HealthCheckLogScope(registration.Name))) + { + var stopwatch = ValueStopwatch.StartNew(); + var context = new HealthCheckContext { Registration = registration }; + + Log.HealthCheckBegin(_logger, registration); + + HealthReportEntry entry; + CancellationTokenSource timeoutCancellationTokenSource = null; + try + { + HealthCheckResult result; + + var checkCancellationToken = cancellationToken; + if (registration.Timeout > TimeSpan.Zero) + { + timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCancellationTokenSource.CancelAfter(registration.Timeout); + checkCancellationToken = timeoutCancellationTokenSource.Token; + } + + result = await healthCheck.CheckHealthAsync(context, checkCancellationToken); + + var duration = stopwatch.GetElapsedTime(); + + entry = new HealthReportEntry( + status: result.Status, + description: result.Description, + duration: duration, + exception: result.Exception, + data: result.Data); + + Log.HealthCheckEnd(_logger, registration, entry, duration); + Log.HealthCheckData(_logger, registration, entry); + } + + catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + var duration = stopwatch.GetElapsedTime(); + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: "A timeout occured while running check.", + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + + // Allow cancellation to propagate if it's not a timeout. + catch (Exception ex) when (ex as OperationCanceledException == null) + { + var duration = stopwatch.GetElapsedTime(); + entry = new HealthReportEntry( + status: HealthStatus.Unhealthy, + description: ex.Message, + duration: duration, + exception: ex, + data: null); + + Log.HealthCheckError(_logger, registration, ex, duration); + } + + finally + { + timeoutCancellationTokenSource?.Dispose(); + } + + return entry; } } diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs index 9508889054..51b7815438 100644 --- a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs +++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderAddCheckExtensions.cs @@ -24,12 +24,37 @@ namespace Microsoft.Extensions.DependencyInjection /// /// A list of tags that can be used to filter health checks. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + IHealthCheck instance, + HealthStatus? failureStatus, + IEnumerable tags) + { + return AddCheck(builder, name, instance, failureStatus, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// An instance. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// An optional representing the timeout of the check. + /// The . public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, IHealthCheck instance, HealthStatus? failureStatus = null, - IEnumerable tags = null) + IEnumerable tags = null, + TimeSpan? timeout = null) { if (builder == null) { @@ -46,7 +71,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(instance)); } - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags, timeout)); } /// @@ -63,15 +88,45 @@ namespace Microsoft.Extensions.DependencyInjection /// The . /// /// This method will use to create the health check - /// instance when needed. If a service of type is registred in the dependency injection container - /// with any liftime it will be used. Otherwise an instance of type will be constructed with + /// instance when needed. If a service of type is registered in the dependency injection container + /// with any lifetime it will be used. Otherwise an instance of type will be constructed with + /// access to services from the dependency injection container. + /// + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags) where T : class, IHealthCheck + { + return AddCheck(builder, name, failureStatus, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// An optional representing the timeout of the check. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. If a service of type is registered in the dependency injection container + /// with any lifetime it will be used. Otherwise an instance of type will be constructed with /// access to services from the dependency injection container. /// public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, HealthStatus? failureStatus = null, - IEnumerable tags = null) where T : class, IHealthCheck + IEnumerable tags = null, + TimeSpan? timeout = null) where T : class, IHealthCheck { if (builder == null) { @@ -83,7 +138,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags)); + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.GetServiceOrCreateInstance(s), failureStatus, tags, timeout)); } // NOTE: AddTypeActivatedCheck has overloads rather than default parameters values, because default parameter values don't @@ -113,7 +168,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null); + return AddTypeActivatedCheck(builder, name, failureStatus: null, tags: null, args); } /// @@ -148,7 +203,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(name)); } - return AddTypeActivatedCheck(builder, name, failureStatus, tags: null); + return AddTypeActivatedCheck(builder, name, failureStatus, tags: null, args); } /// @@ -187,5 +242,44 @@ namespace Microsoft.Extensions.DependencyInjection return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags)); } + + /// + /// Adds a new type activated health check with the specified name and implementation. + /// + /// The health check implementation type. + /// The . + /// The name of the health check. + /// + /// The that should be reported when the health check reports a failure. If the provided value + /// is null, then will be reported. + /// + /// A list of tags that can be used to filter health checks. + /// Additional arguments to provide to the constructor. + /// A representing the timeout of the check. + /// The . + /// + /// This method will use to create the health check + /// instance when needed. Additional arguments can be provided to the constructor via . + /// + public static IHealthChecksBuilder AddTypeActivatedCheck( + this IHealthChecksBuilder builder, + string name, + HealthStatus? failureStatus, + IEnumerable tags, + TimeSpan timeout, + params object[] args) where T : class, IHealthCheck + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return builder.Add(new HealthCheckRegistration(name, s => ActivatorUtilities.CreateInstance(s, args), failureStatus, tags, timeout)); + } } } diff --git a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs index d7dfdd90ae..ba27ab5554 100644 --- a/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs +++ b/src/HealthChecks/HealthChecks/src/DependencyInjection/HealthChecksBuilderDelegateExtensions.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -22,11 +22,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, Func check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -44,7 +64,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check())); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -55,11 +75,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddCheck( this IHealthChecksBuilder builder, string name, Func check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddCheck( + this IHealthChecksBuilder builder, + string name, + Func check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -77,7 +117,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => Task.FromResult(check(ct))); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -88,11 +128,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddAsyncCheck( this IHealthChecksBuilder builder, string name, Func> check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddAsyncCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -110,7 +170,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check()); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } /// @@ -121,11 +181,31 @@ namespace Microsoft.Extensions.DependencyInjection /// A list of tags that can be used to filter health checks. /// A delegate that provides the health check implementation. /// The . + // 2.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH public static IHealthChecksBuilder AddAsyncCheck( this IHealthChecksBuilder builder, string name, Func> check, - IEnumerable tags = null) + IEnumerable tags) + { + return AddAsyncCheck(builder, name, check, tags, default); + } + + /// + /// Adds a new health check with the specified name and implementation. + /// + /// The . + /// The name of the health check. + /// A list of tags that can be used to filter health checks. + /// A delegate that provides the health check implementation. + /// An optional representing the timeout of the check. + /// The . + public static IHealthChecksBuilder AddAsyncCheck( + this IHealthChecksBuilder builder, + string name, + Func> check, + IEnumerable tags = null, + TimeSpan? timeout = default) { if (builder == null) { @@ -143,7 +223,7 @@ namespace Microsoft.Extensions.DependencyInjection } var instance = new DelegateHealthCheck((ct) => check(ct)); - return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags)); + return builder.Add(new HealthCheckRegistration(name, instance, failureStatus: null, tags, timeout)); } } } diff --git a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs index 9ab991204e..ba0a2f32d5 100644 --- a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs @@ -6,6 +6,7 @@ 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.Logging; using Microsoft.Extensions.Logging.Testing; @@ -375,6 +376,80 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks }); } + [Fact] + public async Task CheckHealthAsync_ChecksAreRunInParallel() + { + // Arrange + var input1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var input2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var output1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var output2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("test1", + async () => + { + output1.SetResult(null); + await input1.Task; + return HealthCheckResult.Healthy(); + }); + b.AddAsyncCheck("test2", + async () => + { + output2.SetResult(null); + await input2.Task; + return HealthCheckResult.Healthy(); + }); + }); + + // Act + var checkHealthTask = service.CheckHealthAsync(); + await Task.WhenAll(output1.Task, output2.Task).TimeoutAfter(TimeSpan.FromSeconds(10)); + input1.SetResult(null); + input2.SetResult(null); + await checkHealthTask; + + // Assert + Assert.Collection(checkHealthTask.Result.Entries, + entry => + { + Assert.Equal("test1", entry.Key); + Assert.Equal(HealthStatus.Healthy, entry.Value.Status); + }, + entry => + { + Assert.Equal("test2", entry.Key); + Assert.Equal(HealthStatus.Healthy, entry.Value.Status); + }); + } + + [Fact] + public async Task CheckHealthAsync_TimeoutReturnsUnhealthy() + { + // Arrange + var service = CreateHealthChecksService(b => + { + b.AddAsyncCheck("timeout", async (ct) => + { + await Task.Delay(2000, ct); + return HealthCheckResult.Healthy(); + }, timeout: TimeSpan.FromMilliseconds(100)); + }); + + // Act + var results = await service.CheckHealthAsync(); + + // Assert + Assert.Collection( + results.Entries, + actual => + { + Assert.Equal("timeout", actual.Key); + Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status); + }); + } + private static DefaultHealthCheckService CreateHealthChecksService(Action configure) { var services = new ServiceCollection(); diff --git a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs index 94687efcb8..099944a473 100644 --- a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs @@ -211,8 +211,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks 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.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, @@ -321,8 +321,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks 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.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); }, @@ -399,8 +399,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks 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.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, + entry => { Assert.Contains(entry.EventId, new[] { DefaultHealthCheckService.EventIds.HealthCheckBegin, DefaultHealthCheckService.EventIds.HealthCheckEnd }); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckEnd, entry.EventId); }, entry => { Assert.Equal(DefaultHealthCheckService.EventIds.HealthCheckProcessingEnd, entry.EventId); }, entry => { Assert.Equal(HealthCheckPublisherHostedService.EventIds.HealthCheckPublisherBegin, entry.EventId); },