Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix For Issue 258 - Thread Safety #274

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,111 @@
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 healthCheckLock = serviceProvider.GetRequiredService<HealthCheckLock>();
healthCheckLock.DoWorkInsideLock();

var testObject1 = serviceProvider.GetRequiredService<HealthCheckTestObject1>();
var testObject2 = serviceProvider.GetRequiredService<HealthCheckTestObject2>();
var testObject3 = serviceProvider.GetRequiredService<HealthCheckTestObject3>();
return new SuccessHealthCheck(registrationName);
}
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,103 @@
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;

public SuccessHealthCheck(string registrationName)
{
_registrationName = registrationName;
}

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(new HealthCheckResult(
HealthStatus.Healthy,
description: $"Health check successful for: {_registrationName}"
));
}
}

public class HealthCheckTestObject1
{
private readonly HealthCheckTestChild1 _child1;

public HealthCheckTestObject1(HealthCheckTestChild1 child1)
{
_child1 = child1 ?? throw new ArgumentNullException(nameof(child1));
}
}

public class HealthCheckTestChild1
{
public HealthCheckTestChild1()
{
}
}

public class HealthCheckTestObject2
{
private readonly HealthCheckTestChild2 _child2;

public HealthCheckTestObject2(HealthCheckTestChild2 child2)
{
_child2 = child2 ?? throw new ArgumentNullException(nameof(child2));
}
}

public class HealthCheckTestChild2
{
private readonly Context _context;

public HealthCheckTestChild2(Context context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
}

public class HealthCheckTestObject3
{
public HealthCheckTestObject3()
{
}
}

public class HealthCheckLock
{
private object _lock = new object();
private long? _result = null;

public long DoWorkInsideLock()
{
if (_result != null)
{
return _result.Value;
}

lock (_lock)
{
if (_result != null)
{
return _result.Value;
}

var stopWatch = new Stopwatch();
stopWatch.Start();
while (stopWatch.ElapsedMilliseconds < 2000)
{
continue;
}
stopWatch.Stop();
_result = stopWatch.ElapsedMilliseconds;

return _result.Value;
}
}
}
}
Loading