Skip to content

DbContext from pool contains many InternalEntityEntry instances after clearing change tracker #33354

@mu88

Description

@mu88

Ask a question

I've implemented a job scheduler that processes many records from our PostgreSQL database every few minutes. Within one of our Grafana dashboards, I noticed that the Gen2 size slowly increases over time and finally reaches a plateau:
image

So I was wondering what causes Gen2 to become that large and created a memory dump. To my surprise, there are thousands of InternalEntityEntry objects which retain more than a million objects from being GCed:
image

To be honest, this comes to my surprise as I'd have expected that context.ChangeTracker.Clear() discards those elements after saving them - but maybe I'm misunderstanding something? 🤔

So I'd like to know whether
a) that is normal/expected behavior and if yes,
b) why is it that there are so many InternalEntityEntry objects ending up in Gen2?

Include your code

public class InteractionPoint
{
    public int Key { get; set; }
    public ProcessState State { get; set; } = ProcessState.Received;
}

public enum ProcessState
{
    Received = 0,
    Incomplete = 1,
    Processed = 2,
    TombstoneReceived = 3,
    TombstoneProcessed = 4
}

public class DeliveryPlanDbContext : DbContext
{
    public DeliveryPlanDbContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet<InteractionPoint> InteractionPoints { get; set; }
}

public async Task Foo(IDbContextFactory<MyDbContext> factory, ILogger logger)
{
    var context = factory.CreateDbContext();
    
    var strategy = context.Database.CreateExecutionStrategy();
    await strategy.ExecuteAsync(async () => 
    {
        using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
        
        var entities = await dbContext.Set<InteractionPoint>()
            .Where(entity => entity.State == ProcessState.Received)
            .OrderBy(entity => entity.Key)
            .Take(1000)
            .AsTracking()
            .ToListAsync(ct);
        await HandleEntitiesAsync(entities, ct);
        
        await context.SaveChangesAsync(ct);
        context.ChangeTracker.Clear();
        await transaction.CommitAsync();
    });
}

public async Task HandleEntitiesAsync(List<InteractionPoint> entities, CancellationToken ct)
{
    foreach (var entity in entities)
    {
        // do some processing
        entity.State = ProcessState.Processed;
    }
}

...and the configuration:

void Options(IServiceProvider provider, DbContextOptionsBuilder builder)
{
    builder
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
        .UseNpgsql(
            connectionString,
            sqlOptions => sqlOptions.CommandTimeout(40));
}

services.AddDbContextPool<DeliveryPlanDbContext>(Options);
services.AddPooledDbContextFactory<DeliveryPlanDbContext>(Options);

For a full repro case, see this repo: https://github.com/mu88/Repro_EFCore_MemoryUsage

Include stack traces

There are no errors or exceptions.

Include verbose output

Using project 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj'.
Using startup project 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj'.
Writing 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\obj\MyCompany.MyApp.Service.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\MyUser\AppData\Local\Temp\tmp5chq3h.tmp /verbosity:quiet /nologo C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj
Writing 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\obj\MyCompany.MyApp.Service.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=C:\Users\MyUser\AppData\Local\Temp\tmpj3tm2x.tmp /verbosity:quiet /nologo C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj
Build started...
dotnet build C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj /verbosity:quiet /nologo /p:PublishAot=false

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:05.15
Build succeeded.
dotnet exec --depsfile C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\bin\Debug\net7.0\MyCompany.MyApp.Service.deps.json --additionalprobingpath C:\Users\MyUser\.nuget\packages --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" --additionalprobingpath "C:\Program Files\dotnet\sdk\NuGetFallbackFolder" --runtimeconfig C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\bin\Debug\net7.0\MyCompany.MyApp.Service.runtimeconfig.json C:\Users\MyUser\.dotnet\tools\.store\dotnet-ef\8.0.1\dotnet-ef\8.0.1\tools\net8.0\any\tools\netcoreapp2.0\any\ef.dll dbcontext list --assembly C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\bin\Debug\net7.0\MyCompany.MyApp.Service.dll --project C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj --startup-assembly C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\bin\Debug\net7.0\MyCompany.MyApp.Service.dll --startup-project C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\MyCompany.MyApp.Service.csproj --project-dir C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\ --root-namespace MyCompany.MyApp.Service --language C# --framework net7.0 --nullable --working-dir C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service --verbose
Using assembly 'MyCompany.MyApp.Service'.
Using startup assembly 'MyCompany.MyApp.Service'.
Using application base 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\bin\Debug\net7.0'.
Using working directory 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service'.
Using root namespace 'MyCompany.MyApp.Service'.
Using project directory 'C:\work\Bitbucket\MyOrg\my-app\src\MyCompany.MyApp.Service\'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider in assembly 'MyCompany.MyApp.Service'...
Finding Microsoft.Extensions.Hosting service provider...
Using environment 'Development'.
Using application service provider from Microsoft.Extensions.Hosting.
Found DbContext 'DeliveryPlanDbContext'.
Finding DbContext classes in the project...
MyCompany.MyApp.Core.Persistence.DeliveryPlanDbContext

Include provider and version information

EF Core version: 7.0
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL
Target framework: .NET 7.0
Operating system: Alpine Linux
IDE: not relevant as it happens on production

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions