From b9b675625a370b6139ae2c971d7be48d3ea818fc Mon Sep 17 00:00:00 2001 From: Ryan Thomas Date: Wed, 27 Jan 2021 12:43:21 -0600 Subject: [PATCH] Issue #258 - Updated the integration tests cases to run a health check request using Microsoft's OOTB health check service which runs the health check registrations in parallel --- .../Lamar.AspNetCoreTests.Integration.csproj | 12 +- .../HealthChecks/HealthCheckTestObjects.cs | 5 + .../HealthChecks/IHealthCheckExtensions.cs | 107 ++++++++++++++++++ .../SerializableHealthCheckResult.cs | 53 +++++++++ .../HealthChecks/SetupFailedHealthCheck.cs | 28 +++++ .../App/HealthChecks/SuccessHealthCheck.cs | 85 ++++++++++++++ .../MultiThreadProblem/App/LamarStartup.cs | 58 ++++++---- .../App/MicrosoftDIStartup.cs | 43 ++++--- .../MultiThreadProblem/IntegrationTests.cs | 18 ++- 9 files changed, 357 insertions(+), 52 deletions(-) create mode 100644 src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/HealthCheckTestObjects.cs create mode 100644 src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/IHealthCheckExtensions.cs create mode 100644 src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SerializableHealthCheckResult.cs create mode 100644 src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SetupFailedHealthCheck.cs create mode 100644 src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SuccessHealthCheck.cs diff --git a/src/Lamar.AspNetCoreTests.Integration/Lamar.AspNetCoreTests.Integration.csproj b/src/Lamar.AspNetCoreTests.Integration/Lamar.AspNetCoreTests.Integration.csproj index 0c81edd1..30bcc40e 100644 --- a/src/Lamar.AspNetCoreTests.Integration/Lamar.AspNetCoreTests.Integration.csproj +++ b/src/Lamar.AspNetCoreTests.Integration/Lamar.AspNetCoreTests.Integration.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1;net5.0 @@ -9,9 +9,11 @@ - - - + + + + + @@ -24,6 +26,8 @@ + + diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/HealthCheckTestObjects.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/HealthCheckTestObjects.cs new file mode 100644 index 00000000..7cb1b737 --- /dev/null +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/HealthCheckTestObjects.cs @@ -0,0 +1,5 @@ +using System; + +namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks +{ +} \ No newline at end of file diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/IHealthCheckExtensions.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/IHealthCheckExtensions.cs new file mode 100644 index 00000000..f937ed05 --- /dev/null +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/IHealthCheckExtensions.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Text; + +namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks +{ + public static class IHealthCheckExtensions + { + public static IServiceCollection AddTestHealthChecks(this IServiceCollection services) + { + services.AddHttpContextAccessor(); + var healthCheckBuilder = services.AddHealthChecks(); + + healthCheckBuilder.AddDbContextCheck(); + + foreach (var index in Enumerable.Range(0, 20)) + { + // Add multiple test health checks to ensure concurrent resolver access doesn't blow up + healthCheckBuilder.AddTestHealthCheck($"TestHealthCheck{index}"); + } + + return services; + } + + public static IHealthChecksBuilder AddTestHealthCheck(this IHealthChecksBuilder builder, string registrationName) + { + builder.Add(new HealthCheckRegistration( + registrationName, + (serviceProvider) => + { + try + { + // Get some different objects from the service provider to try and trigger any thread + // safety issues in the container resolution logic + var testObjects1 = serviceProvider.GetRequiredService(); + var testObjects2 = serviceProvider.GetRequiredService(); + return new SuccessHealthCheck(registrationName, testObjects1, testObjects2); + } + catch (Exception exc) + { + return new SetupFailedHealthCheck(exc, registrationName); + } + }, + HealthStatus.Unhealthy, + default)); + + return builder; + } + + public static string CreateHealthReportPlainText(string key, HealthReportEntry entry) + { + var entryOutput = new StringBuilder($"{key}: {entry.Status} | {entry.Duration}\n"); + if (entry.Tags?.Any() == true) + { + entryOutput.Append("- Tags:"); + entryOutput.Append(string.Join(", ", entry.Tags)); + entryOutput.Append('\n'); + } + + if (!string.IsNullOrWhiteSpace(entry.Description)) + { + entryOutput.Append($"- Description: {entry.Description}\n\n"); + } + + if (entry.Exception != null) + { + entryOutput.Append($"- Exception: {entry.Exception}\n\n"); + } + + if (entry.Data?.Count > 0) + { + entryOutput.Append("- Data:\n"); + foreach (var keyValuePair in entry.Data) + { + entryOutput.Append($"\t{keyValuePair.Key}: {keyValuePair.Value}"); + } + entryOutput.Append('\n'); + } + + return entryOutput.ToString(); + } + + public static IEndpointRouteBuilder MapTestHealthChecks(this IEndpointRouteBuilder endpoints) + { + endpoints.MapHealthChecks("/health", new HealthCheckOptions + { + ResponseWriter = async (context, report) => + { + var serializableReport = new SerializableHealthCheckResult(report); + var resultString = JsonConvert.SerializeObject(serializableReport); + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(resultString).ConfigureAwait(false); + } + }); + + return endpoints; + } + } +} diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SerializableHealthCheckResult.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SerializableHealthCheckResult.cs new file mode 100644 index 00000000..83d2a72c --- /dev/null +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SerializableHealthCheckResult.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks +{ + public class SerializableHealthCheckResult + { + // Default constructor for json serialization / deserialization support + public SerializableHealthCheckResult() { } + + public SerializableHealthCheckResult(HealthReport healthReport) + { + _ = healthReport ?? throw new ArgumentNullException(nameof(healthReport)); + + Status = healthReport.Status; + TotalDuration = healthReport.TotalDuration; + + if (healthReport.Entries != null) + { + Entries = healthReport.Entries.Select(entry => new SerializableHealthCheckResultEntry(entry.Value, entry.Key)).ToList(); + } + } + + public List Entries { get; set; } + public HealthStatus Status { get; set; } + public TimeSpan TotalDuration { get; set; } + } + + public class SerializableHealthCheckResultEntry + { + // Default constructor for json serialization / deserialization support + public SerializableHealthCheckResultEntry() { } + + public SerializableHealthCheckResultEntry(HealthReportEntry entry, string name) + { + Description = entry.Description; + Duration = entry.Duration; + Exception = entry.Exception?.ToString(); + Name = name; + Status = entry.Status; + Tags = entry.Tags?.ToList(); + } + + public string Description { get; set; } + public TimeSpan Duration { get; set; } + public string Exception { get; set; } + public string Name { get; set; } + public HealthStatus Status { get; set; } + public List Tags { get; set; } + } +} diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SetupFailedHealthCheck.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SetupFailedHealthCheck.cs new file mode 100644 index 00000000..5a16c06c --- /dev/null +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SetupFailedHealthCheck.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks +{ + public class SetupFailedHealthCheck : IHealthCheck + { + private readonly Exception _exception; + private readonly string _registrationName; + + public SetupFailedHealthCheck(Exception exception, string registrationName) + { + _exception = exception; + _registrationName = registrationName; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(new HealthCheckResult( + HealthStatus.Unhealthy, + description: $"An exception occurred while attempting to construct the health check for registration: {_registrationName}", + exception: _exception + )); + } + } +} diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SuccessHealthCheck.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SuccessHealthCheck.cs new file mode 100644 index 00000000..677fd103 --- /dev/null +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/HealthChecks/SuccessHealthCheck.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks +{ + public class SuccessHealthCheck : IHealthCheck + { + private readonly string _registrationName; + private readonly HealthCheckTestObjects1 _testObjects1; + private readonly HealthCheckTestObjects2 _testObjects2; + + public SuccessHealthCheck(string registrationName, HealthCheckTestObjects1 testObjects1, HealthCheckTestObjects2 testObjects2) + { + _registrationName = registrationName; + _testObjects1 = testObjects1 ?? throw new ArgumentNullException(nameof(testObjects1)); + _testObjects2 = testObjects2 ?? throw new ArgumentNullException(nameof(testObjects2)); + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(new HealthCheckResult( + HealthStatus.Healthy, + description: $"Health check successful for: {_registrationName}" + )); + } + } + + public class HealthCheckTestObjects1 + { + private readonly HealthCheckTestChild1 _child1; + + public HealthCheckTestObjects1(HealthCheckTestChild1 child1) + { + _child1 = child1 ?? throw new ArgumentNullException(nameof(child1)); + } + } + + public class HealthCheckTestChild1 + { + private readonly HealthCheckTestChild2 _child2; + + public HealthCheckTestChild1(HealthCheckTestChild2 child2) + { + _child2 = child2 ?? throw new ArgumentNullException(nameof(child2)); + } + } + + public class HealthCheckTestChild2 + { + private readonly HealthCheckTestChild3 _child3; + + public HealthCheckTestChild2(HealthCheckTestChild3 child3) + { + _child3 = child3 ?? throw new ArgumentNullException(nameof(child3)); + } + } + + public class HealthCheckTestChild3 + { + private readonly Context _context; + + public HealthCheckTestChild3(Context context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + } + + public class HealthCheckTestObjects2 + { + public HealthCheckTestObjects2() + { + // Simulate some cpu bound setup work... + var stopWatch = new Stopwatch(); + stopWatch.Start(); + while (stopWatch.ElapsedMilliseconds < 5000) + { + continue; + } + stopWatch.Stop(); + } + } +} diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/LamarStartup.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/LamarStartup.cs index d84f3ca8..018a0314 100644 --- a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/LamarStartup.cs +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/LamarStartup.cs @@ -1,36 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App { - public class LamarStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(); - services.AddDbContext(); + public class LamarStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(); + services.AddDbContext(); - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); - } + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseRouting(); - app.UseEndpoints(configure => configure.MapControllers()); - } + services.AddTestHealthChecks(); + } - public void ConfigureContainer(ServiceRegistry services) - { - services.For().Use().Transient(); - services.For().Use().Transient(); - services.For().Use().Transient(); - } - } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + app.UseEndpoints(configure => + { + configure.MapControllers(); + configure.MapTestHealthChecks(); + }); + } + + public void ConfigureContainer(ServiceRegistry services) + { + services.For().Use().Transient(); + services.For().Use().Transient(); + services.For().Use().Transient(); + + services.For().Use().Scoped(); + services.For().Use().Scoped(); + services.For().Use().Scoped(); + services.For().Use().Scoped(); + services.For().Use().Scoped(); + } + } } diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/MicrosoftDIStartup.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/MicrosoftDIStartup.cs index ac4b69a0..16687019 100644 --- a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/MicrosoftDIStartup.cs +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/App/MicrosoftDIStartup.cs @@ -1,32 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App { - public class MicrosoftDIStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(); - services.AddDbContext(); + public class MicrosoftDIStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(); + services.AddDbContext(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); - } + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseRouting(); - app.UseEndpoints(configure => configure.MapControllers()); - } - } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + app.UseEndpoints(configure => + { + configure.MapControllers(); + }); + } + } } diff --git a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/IntegrationTests.cs b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/IntegrationTests.cs index 4fdd9b52..701a2b2c 100644 --- a/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/IntegrationTests.cs +++ b/src/Lamar.AspNetCoreTests.Integration/MultiThreadProblem/IntegrationTests.cs @@ -10,6 +10,10 @@ using Microsoft.Extensions.DependencyInjection; using Lamar.Microsoft.DependencyInjection; using Xunit; +using Shouldly; +using Newtonsoft.Json; +using Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks; +using Microsoft.Extensions.Diagnostics.HealthChecks; namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem { @@ -50,7 +54,7 @@ public IntegrationTestsLamar(CustomWebApplicationFactory factory) } [Fact] - public async void ExecutesInParallel_WithoutExceptions() + public async void ControllerRequest_ExecutesInParallel_WithoutExceptions() { var client = _factory.CreateClient(); var tasks = new List(); @@ -61,5 +65,17 @@ public async void ExecutesInParallel_WithoutExceptions() await Task.WhenAll(tasks); } + + [Fact] + public async Task HealthCheckRequest_completes_successfully() + { + var client = _factory.CreateClient(); + + var result = await client.GetAsync("health").ConfigureAwait(false); + + var responseString = await result.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseObject = JsonConvert.DeserializeObject(responseString); + responseObject.Status.ShouldBe(HealthStatus.Healthy); + } } }