Skip to content

Commit

Permalink
Issue #258 - Updated the integration tests cases to run a health chec…
Browse files Browse the repository at this point in the history
…k request using Microsoft's OOTB health check service which runs the health check registrations in parallel
  • Loading branch information
Ryan Thomas authored and jeremydmiller committed Feb 20, 2021
1 parent 3fa6197 commit b9b6756
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
Expand All @@ -9,9 +9,11 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Shouldly" Version="3.0.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
Expand All @@ -24,6 +26,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[5.0.0,5.9.0)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="[5.0.0,5.9.0)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="[5.0.0,5.9.0)" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="[5.0.0,5.9.0)" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="[5.0.0,5.9.0)" />
<PackageReference Include="Shouldly" Version="3.0.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System;

namespace Lamar.AspNetCoreTests.Integration.MultiThreadProblem.App.HealthChecks
{
}
Original file line number Diff line number Diff line change
@@ -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<Context>();

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<HealthCheckTestObjects1>();
var testObjects2 = serviceProvider.GetRequiredService<HealthCheckTestObjects2>();
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SerializableHealthCheckResultEntry> 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<string> Tags { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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<HealthCheckResult> 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
));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HealthCheckResult> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Context>();
services.AddDbContext<SecondContext>();
public class LamarStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<Context>();
services.AddDbContext<SecondContext>();

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<IBookService>().Use<BookService>().Transient();
services.For<IOtherService>().Use<OtherService>().Transient();
services.For<IContextFactory>().Use<ContextFactory>().Transient();
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(configure =>
{
configure.MapControllers();
configure.MapTestHealthChecks();
});
}

public void ConfigureContainer(ServiceRegistry services)
{
services.For<IBookService>().Use<BookService>().Transient();
services.For<IOtherService>().Use<OtherService>().Transient();
services.For<IContextFactory>().Use<ContextFactory>().Transient();

services.For<HealthCheckTestObjects1>().Use<HealthCheckTestObjects1>().Scoped();
services.For<HealthCheckTestObjects2>().Use<HealthCheckTestObjects2>().Scoped();
services.For<HealthCheckTestChild1>().Use<HealthCheckTestChild1>().Scoped();
services.For<HealthCheckTestChild2>().Use<HealthCheckTestChild2>().Scoped();
services.For<HealthCheckTestChild3>().Use<HealthCheckTestChild3>().Scoped();
}
}
}
Loading

0 comments on commit b9b6756

Please sign in to comment.