-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
ObjectDisposedException on retrieval of a pooled DbContext #26202
Comments
The workaround that worked in my case: https://github.com/servicetitan/Stl.Fusion/blob/net6/src/Stl.Fusion.EntityFramework/DbContextBase.cs#L19 |
@alexyakunin can you please submit a runnable code sample which shows exactly what you're doing to to get the ObjectDisposedException? The minimal code below works without any exceptions. Attempted reprovar optionsBuilder = new DbContextOptionsBuilder<BlogContext>();
optionsBuilder
.UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
var options = optionsBuilder.Options;
await using (var ctx = new BlogContext(options))
{
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
}
var factory = new PooledDbContextFactory<BlogContext>(options);
using (var ctx1 = factory.CreateDbContext())
{
Console.WriteLine($"Context1 ID: {ctx1.ContextId}");
_ = ctx1.Blogs.ToList();
}
using (var ctx2 = factory.CreateDbContext())
{
Console.WriteLine($"Context2 ID: {ctx2.ContextId}");
_ = ctx2.Blogs.ToList();
}
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public BlogContext(DbContextOptions options) : base(options) {}
}
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
} |
@roji sorry, I don't have enough time for this, but based on my description, you need a bit more than a basic example:
This code should fail pretty quickly. |
A short note about your code: I'd at least add a check that the second context is actually the same as the first one - otherwise you won't see the problem. I didn't dig into the pooling strategy, but since what's returned is actually up to the pool, it makes sense to at least check if the second attempt to rent the context produces the same instance. |
One other thing is: I ended up thinking it's really a bug after I asked Rider to find every usage of |
Finally, it worth mentioning I saw the bug on this test: Basically, it creates lots of DbContexts concurrently. The service it uses: https://github.com/servicetitan/Stl.Fusion/blob/net6/tests/Stl.Fusion.Tests/Services/UserService.cs And the |
And the fix I referenced above killed all |
That is what the outputting of the ContextId is about; the two GUIDs are identical, meaning that the same context instance is being reused.
SetLeaseInternal sets _disposed to false. The way context pooling works is that when the context is disposed, it is placed more or less immediately back into the pool. Before it is handed out again (e.g. from PooledDbContextFactory.CreateDbContext), its state is reset and _disposed is set to false. To summarize, I don't really see a bug at this point... I'm not saying there isn't one, but it's also possible to trigger an ObjectDisposedException through an application bug, e.g. if the same context instance is accidentally used concurrently from two threads. So some sort of code sample triggering this would be most helpful. |
I managed to repro this in a concurrent tight-loop scenario, see code below. This only seems to repro when nothing is actually executed on the context. I'll investigate this for 6.0 in the coming days. Stack trace
Reprovar optionsBuilder = new DbContextOptionsBuilder<BlogContext>();
optionsBuilder
.UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0");
var options = optionsBuilder.Options;
await using (var ctx = new BlogContext(options))
{
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
}
var factory = new PooledDbContextFactory<BlogContext>(options);
Console.WriteLine("Start");
await Task.WhenAll(
Enumerable.Range(0, 10).Select(i => Task.Run(() =>
{
for (var j = 0; j < 1_000_000; j++)
{
using (var ctx1 = factory.CreateDbContext())
{
// _ = ctx1.Blogs.ToList();
}
if (j % 1_000 == 0)
Console.WriteLine($"Task {i} reached iteration {j}");
}
})));
Console.WriteLine("Done");
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public BlogContext(DbContextOptions options) : base(options) {}
}
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
} |
@roji Race condition: Context is disposed: public virtual void Dispose()
{
var leaseActive = _lease.IsActive;
var contextDisposed = leaseActive && _lease.ContextDisposed();
if (DisposeSync(leaseActive, contextDisposed))
{
_serviceScope?.Dispose();
}
} Calls public bool ContextDisposed()
{
if (_standalone)
{
Release();
return true;
}
return false;
} Context is now back in the pool. Back to Dispose method, private bool DisposeSync(bool leaseActive, bool contextDisposed)
{
if (leaseActive)
{
if (contextDisposed)
{
_disposed = true;
_lease = DbContextLease.InactiveLease;
}
}
... Context is marked as disposed and lease is deactivated. But by this time the context may already have been pulled from the pool again, so we deactivate a context that is in use. |
@roji , @ajcvickers , huge thanks - I also wanted to post a working example, but just got back to this & was pleasantly surprised! |
Note: the same bug exists in 5.0. |
Note from triage: backport to EF Core 5.0.x, and possibly also to 3.1.x if it is also present there. |
FYI I don't think this bug exists in 3.1 - it doesn't seem like the DbContext is mutated in any way after being returned to the pool (code). |
@roji Can you prepare a PR cherry-picking this to 5.0.x? |
EF Core version: 6.0.0-rc1
Database provider: any
Target framework: .NET 6.0
Operating system: any
IDE: any
https://github.com/dotnet/efcore/blob/release/6.0/src/EFCore/DbContext.cs#L902 - looks like
_disposed
field should be set tofalse
here instead (and in the *Async version of the same method). The existing code causes anyDbContext
that was returned back to the pool to throwObjectDisposedException
on attempt to rent it again.The text was updated successfully, but these errors were encountered: