Connection open count survives the reset of a pooled DbContext, so later leases keep the connection open
Maintainer thường phản hồi trong vòng 2 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 42/100
Hướng nghiên cứu
Start in src/EFCore.Relational/Storage/RelationalConnection.cs and compare ResetState with ResetStateAsync, especially the handling of _openedCount and openedInternally. Then read the existing RelationalConnectionTest cases and the DbContextPoolingTest cases for Handle_open_connection_when_returning_to_pool_for*. Add coverage for a later lease after returning a context with the connection open; done means both reset paths clear EF's open count while preserving the expected state of owned and external connections.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Bug description
When a pooled context goes back to the pool while EF still counts its connection as open (for example after Database.OpenConnectionAsync() without a matching CloseConnectionAsync()), the reset closes the DbConnection but leaves RelationalConnection's _openedCount and _openedInternally unchanged. Every later lease of that context instance then starts with the count at 1: its first command opens the connection and raises the count to 2, and the close after the command only lowers it to 1, so the connection stays open until the lease ends. As a result:
- each later lease holds a physical connection for its whole duration instead of per command;
DbConnectionInterceptor.ConnectionOpening/ConnectionOpenedrun only for the first command of such a lease instead of for every command;- the stale count carries over from lease to lease, so every later lease of that instance is affected (in 10.0, until a lease is returned through the synchronous path).
One lease that misses the close, for example after an exception, is enough. A connection opened directly with GetDbConnection().Open() does not trigger it, since EF does not count that open.
Which reset path is affected depends on the version:
- 10.0 (10.0.13; 8.0 and 9.0 have the same code): only the asynchronous path.
ResetState(disposeDbConnection: false)resets both fields (release/10.0, lines 1043-1044), butResetStateAsync(disposeDbConnection: false)only does so insideif (disposeDbConnection)(lines 1068-1092). So the context is affected afterDisposeAsync()/await using, not afterDispose(). WithAddDbContextPoolin ASP.NET Core the asynchronous path is the usual one, since request scopes are disposed asynchronously.ResetStateAsynchas not reset the count since #24207 separated it fromDisposein 6.0; since 7.0 (#28519, for #27308) it closes an owned connection, which leads to this symptom. - main and 11.0 (since 11.0.0-preview.3, including rc.2): both paths. #37916 removed the unconditional
_openedCount = 0; _openedInternally = false;fromResetState(main, lines 1027-1047), soDispose()is now affected too, a regression from 10.0. The Copilot review on that PR pointed out this consequence. The same reset runs whenSetDbConnectionreplaces a connection that the context does not own, so in main and 11.0 a count EF still holds for the old connection carries over to the new one.
A command's close only closes the connection when ShouldClose() brings the count to zero (lines 994-998), which no longer happens once a lease starts with the count at 1.
The repro below prints, with 10.0.13 and DisposeAsync(), and with 11.0.0-rc.2 and either DisposeAsync() or Dispose():
Lease 2, command 1: Open, ConnectionOpened calls: 2
Lease 2, command 2: Open, ConnectionOpened calls: 2
Lease 3, command 1: Open, ConnectionOpened calls: 3
Lease 3, command 2: Open, ConnectionOpened calls: 3
Expected, as 10.0.13 prints with Dispose():
Lease 2, command 1: Closed, ConnectionOpened calls: 2
Lease 2, command 2: Closed, ConnectionOpened calls: 3
Lease 3, command 1: Closed, ConnectionOpened calls: 4
Lease 3, command 2: Closed, ConnectionOpened calls: 5
Workaround: call CloseConnection(Async) for every OpenConnection(Async) before the context is returned to the pool (closing the DbConnection directly does not lower EF's count); in 10.0, returning it with Dispose() also clears the count.
Proposed fix: reset _openedCount and _openedInternally in both ResetState and ResetStateAsync regardless of disposeDbConnection and of whether the context owns the connection, as ResetState did from 6.0 until #37916. I have this change with tests in RelationalConnectionTest and, in DbContextPoolingTest, a query in the second lease of the Handle_open_connection_when_returning_to_pool_for_* tests added for #27308: afterwards an owned connection must be closed and an external one still open. Those tests checked the state on return, but not what a later lease does. I'd be happy to send a pull request against main if this approach works for you. Since the synchronous part is a regression from 10.0 in 11.0, which hasn't shipped yet, you may want the fix in 11.0 as well; if you also fix this in 10.0, only the two lines in ResetStateAsync are needed there.
Related but different: #36218 (disposing a connection calls no closing interceptor) and #38983 (the concurrency detector was not reset when a pooled context was returned).
Your code
// Console app: <TargetFramework>net10.0</TargetFramework>, <ImplicitUsings>enable</ImplicitUsings>,
// <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
// (for 11.0: net11.0 and 11.0.0-rc.2.26504.105)
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
// true: DisposeAsync (affected in 10.0 and 11.0); false: Dispose (affected in 11.0)
var disposeAsync = true;
var interceptor = new ConnectionOpenedCounter();
var options = new DbContextOptionsBuilder<BlogContext>()
.UseSqlite("Data Source=:memory:")
.AddInterceptors(interceptor)
.Options;
var factory = new PooledDbContextFactory<BlogContext>(options);
// Lease 1: open the connection and return the context to the pool without closing it.
var context = factory.CreateDbContext();
await context.Database.OpenConnectionAsync();
await DisposeContext(context);
// Leases 2 and 3 get the same instance; each command should open and close the connection.
for (var lease = 2; lease <= 3; lease++)
{
context = factory.CreateDbContext();
var connection = context.Database.GetDbConnection();
for (var command = 1; command <= 2; command++)
{
await context.Database.ExecuteSqlRawAsync("SELECT 1");
Console.WriteLine($"Lease {lease}, command {command}: {connection.State}, ConnectionOpened calls: {interceptor.Count}");
}
await DisposeContext(context);
}
async Task DisposeContext(DbContext context)
{
if (disposeAsync)
{
await context.DisposeAsync();
}
else
{
context.Dispose();
}
}
public class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options);
public class ConnectionOpenedCounter : DbConnectionInterceptor
{
public int Count { get; private set; }
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
=> Count++;
public override Task ConnectionOpenedAsync(
DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default)
{
Count++;
return Task.CompletedTask;
}
}
Stack traces
Verbose output
EF Core version
10.0.13, 11.0.0-rc.2.26504.105
Database provider
Microsoft.EntityFrameworkCore.Sqlite (the code is in Microsoft.EntityFrameworkCore.Relational)
Target framework
.NET 10.0, .NET 11.0 (rc.1)
Operating system
Windows 11
IDE
dotnet CLI
- Ngôn ngữ chính
- C#
- Star
- 14.8k
- Fork
- 3.4k
- Merge trung bình
- 3 ngày 18 giờ
- Pull request đã merge (30 ngày)
- 159
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của dotnet/efcore
-
customer-reported
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 54/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Regression in 11.0: replacing an owned instance or re-adding an entity with the same key sets unchanged OnAdd properties to nullCó thể đã có người làm @seragAhmed đã nhận hôm nay. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
dotnet/efcore#39617 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
customer-reported
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Maintainer thường phản hồi trong vòng 2 ngày
Tất cả issue của dotnet/efcore
Issue tương tự
-
bug emitter:client:csharp
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
microsoft/typespec#12572 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
needs-area-label
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area-networking
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dotnet/aspnetcore#70545 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 87/100
Maintainer thường phản hồi trong vòng 1 ngày