Asp.Net 核心健康检查并发 Entity Framework DbContext 错误
Asp.Net Core HealthChecks Concurrent Entity Framework DbContext Errors
我读到这个:https://github.com/dotnet/aspnetcore/issues/14453 看来我也遇到了同样的问题。
我的解决方案中有大约 50 个项目,每个项目都有自己的 RegisterServices.cs,它定义了数据库上下文
services.AddDbContextPool<Db.ACME.Context>(
options => options.UseSqlServer(configuration.GetConnectionString("ACME")));
在那里我还添加了健康检查,但是一旦我添加了超过 1 个,例如在项目“PROJECTS”中,一个用于检查重复的项目名称,一个用于检查重复的任务名称。
services.AddHealthChecks()
.AddCheck<DuplicateTaskNamesHealthCheck>("Duplicate Task Names", HealthStatus.Unhealthy,
tags: new[] { "org_project" });
services.AddHealthChecks()
.AddCheck<DuplicateProjectNamesHealthCheck>("Duplicate Project Names", HealthStatus.Unhealthy,
tags: new[] { "org_project" });
Healthcheck 只是调用服务“projectservice”和另一个“taskservice”(都在同一个 vs 解决方案项目中)
它会因已知的原因而失败
A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
Healthchecks 本身不包含任何逻辑,只是调用逻辑(EF linq 查询)所在的特定服务。当我评论其中一个时,它们会起作用。
似乎唯一的解决方案是从健康检查内部的服务中复制功能,但令人不安的是这并不是很方便。健康检查越多,越多的方法将被复制过来,DRY 超出了 window.
还有其他已知的解决方法吗?
在项目服务中调用失败的方法示例
public async Task<List<string>> GetDuplicateProjectNamesAsync()
{
IQueryable<IGrouping<string, PROJECT>> DuplicateProjectNames = _context
.PROJECT
.AsNoTracking()
.GroupBy(x => x.PROJECTNAME)
.Where(x => x.Count() > 1);
/* next line fails */
var hasItems = await DuplicateProjectNames.AnyAsync();
if (hasItems)
{
return await DuplicateProjectNames.Select(x=>x.Key).ToListAsync();
}
else
{
return new List<string>();
}
}
健康检查示例
namespace ACME.Org.Project.Healthchecks
{
public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
public IACME6_PROJECT _ACME6_PROJECT;
public Settings _settings;
public DuplicateProjectNamesHealthCheck(IACME6_PROJECT PROJECT, IOptions<Settings> settings)
{
_ACME6_PROJECT = PROJECT;
_settings = settings.Value;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
List<string> duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();
if (duplicateProjectNames.Count > 0)
{
if (_settings.AutoFixDuplicateProjectNames)
{
await _ACME6_PROJECT.FixDuplicateProjectNamesAsync();
}
else
{
string errorMessage = "Duplicate Project Names found: ";
foreach (string projectName in duplicateProjectNames)
{
errorMessage += projectName + ") |";
}
errorMessage += "To autofix this set AutoFixDuplicateProjectNames to true in settings.";
return new HealthCheckResult(status: context.Registration.FailureStatus, errorMessage);
}
}
return HealthCheckResult.Healthy("OK");
}
}
}
和带注释的行(见上文)236 完整的错误消息要求:
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at ACME.Org.Project.Services.ACME6.ACME6_PROJECT.GetDuplicateProjectNamesAsync() in E:\ACME\repo\acme-7-api\ACME.Org.Project\Services\ACME6\ACME6_PROJECT\ACME6_PROJECT.cs:line 236
at ACME.Org.Project.Healthchecks.DuplicateProjectNamesHealthCheck.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) in E:\ACME\repo\acme-7-api\ACME.Org.Project\Healthchecks\DuplicateProjectNamesHealthCheck.cs:line 23
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken)
Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService: Error: Health check "Duplicate Project Names" threw an unhandled exception after 562.888ms
原因是AddCheck
will create and use a single instance of the healthcheck type
return builder.Add(new HealthCheckRegistration(name,
s => ActivatorUtilities.GetServiceOrCreateInstance<T>(s),
failureStatus, tags, timeout));
ActivatorUtilities.GetServiceOrCreateInstance<T>(s)
将尝试从 DI 中检索该类型的实例或创建一个新实例。在任何一种情况下,这都意味着 DuplicateTaskNamesHealthCheck
表现为单例,任何注入其中的 DbContext
实例将保持活动状态直到关闭。
为了解决这个问题,健康检查的构造函数应该接受 IServiceProvider
或 IDbContextFactory
并在需要时创建上下文实例。
例如:
public class ExampleHealthCheck : IHealthCheck
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public ExampleHealthCheck(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
}
IServiceProvider
应该用于使用像 IACME6_PROJECT
这样的作用域或瞬态服务。 IServiceProvider
可用于直接检索瞬态服务或创建范围以使用范围服务:
public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
public IServiceProvider _serviceProvider;
public Settings _settings;
public DuplicateProjectNamesHealthCheck(IServiceProvider serviceProvider, IOptions<Settings> settings)
{
_serviceProvider=serviceProvider;
_settings = settings.Value;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var scope = _serviceProvider.CreateScope())
{
var project = scope.ServiceProvider.GetRequiredService< IACME6_PROJECT>();
var duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();
...
}
}
我读到这个:https://github.com/dotnet/aspnetcore/issues/14453 看来我也遇到了同样的问题。
我的解决方案中有大约 50 个项目,每个项目都有自己的 RegisterServices.cs,它定义了数据库上下文
services.AddDbContextPool<Db.ACME.Context>(
options => options.UseSqlServer(configuration.GetConnectionString("ACME")));
在那里我还添加了健康检查,但是一旦我添加了超过 1 个,例如在项目“PROJECTS”中,一个用于检查重复的项目名称,一个用于检查重复的任务名称。
services.AddHealthChecks()
.AddCheck<DuplicateTaskNamesHealthCheck>("Duplicate Task Names", HealthStatus.Unhealthy,
tags: new[] { "org_project" });
services.AddHealthChecks()
.AddCheck<DuplicateProjectNamesHealthCheck>("Duplicate Project Names", HealthStatus.Unhealthy,
tags: new[] { "org_project" });
Healthcheck 只是调用服务“projectservice”和另一个“taskservice”(都在同一个 vs 解决方案项目中)
它会因已知的原因而失败
A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
Healthchecks 本身不包含任何逻辑,只是调用逻辑(EF linq 查询)所在的特定服务。当我评论其中一个时,它们会起作用。
似乎唯一的解决方案是从健康检查内部的服务中复制功能,但令人不安的是这并不是很方便。健康检查越多,越多的方法将被复制过来,DRY 超出了 window.
还有其他已知的解决方法吗?
在项目服务中调用失败的方法示例
public async Task<List<string>> GetDuplicateProjectNamesAsync()
{
IQueryable<IGrouping<string, PROJECT>> DuplicateProjectNames = _context
.PROJECT
.AsNoTracking()
.GroupBy(x => x.PROJECTNAME)
.Where(x => x.Count() > 1);
/* next line fails */
var hasItems = await DuplicateProjectNames.AnyAsync();
if (hasItems)
{
return await DuplicateProjectNames.Select(x=>x.Key).ToListAsync();
}
else
{
return new List<string>();
}
}
健康检查示例
namespace ACME.Org.Project.Healthchecks
{
public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
public IACME6_PROJECT _ACME6_PROJECT;
public Settings _settings;
public DuplicateProjectNamesHealthCheck(IACME6_PROJECT PROJECT, IOptions<Settings> settings)
{
_ACME6_PROJECT = PROJECT;
_settings = settings.Value;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
List<string> duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();
if (duplicateProjectNames.Count > 0)
{
if (_settings.AutoFixDuplicateProjectNames)
{
await _ACME6_PROJECT.FixDuplicateProjectNamesAsync();
}
else
{
string errorMessage = "Duplicate Project Names found: ";
foreach (string projectName in duplicateProjectNames)
{
errorMessage += projectName + ") |";
}
errorMessage += "To autofix this set AutoFixDuplicateProjectNames to true in settings.";
return new HealthCheckResult(status: context.Registration.FailureStatus, errorMessage);
}
}
return HealthCheckResult.Healthy("OK");
}
}
}
和带注释的行(见上文)236 完整的错误消息要求:
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at ACME.Org.Project.Services.ACME6.ACME6_PROJECT.GetDuplicateProjectNamesAsync() in E:\ACME\repo\acme-7-api\ACME.Org.Project\Services\ACME6\ACME6_PROJECT\ACME6_PROJECT.cs:line 236
at ACME.Org.Project.Healthchecks.DuplicateProjectNamesHealthCheck.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) in E:\ACME\repo\acme-7-api\ACME.Org.Project\Healthchecks\DuplicateProjectNamesHealthCheck.cs:line 23
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken)
Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService: Error: Health check "Duplicate Project Names" threw an unhandled exception after 562.888ms
原因是AddCheck
will create and use a single instance of the healthcheck type
return builder.Add(new HealthCheckRegistration(name,
s => ActivatorUtilities.GetServiceOrCreateInstance<T>(s),
failureStatus, tags, timeout));
ActivatorUtilities.GetServiceOrCreateInstance<T>(s)
将尝试从 DI 中检索该类型的实例或创建一个新实例。在任何一种情况下,这都意味着 DuplicateTaskNamesHealthCheck
表现为单例,任何注入其中的 DbContext
实例将保持活动状态直到关闭。
为了解决这个问题,健康检查的构造函数应该接受 IServiceProvider
或 IDbContextFactory
并在需要时创建上下文实例。
例如:
public class ExampleHealthCheck : IHealthCheck
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public ExampleHealthCheck(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
}
IServiceProvider
应该用于使用像 IACME6_PROJECT
这样的作用域或瞬态服务。 IServiceProvider
可用于直接检索瞬态服务或创建范围以使用范围服务:
public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
public IServiceProvider _serviceProvider;
public Settings _settings;
public DuplicateProjectNamesHealthCheck(IServiceProvider serviceProvider, IOptions<Settings> settings)
{
_serviceProvider=serviceProvider;
_settings = settings.Value;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var scope = _serviceProvider.CreateScope())
{
var project = scope.ServiceProvider.GetRequiredService< IACME6_PROJECT>();
var duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();
...
}
}