-
Notifications
You must be signed in to change notification settings - Fork 255
Description
I've originally opened this issue in npsql repo, but after further research I think the issue is in this package, particularly in the fact that connectionString is being used as a key for cache of DataSource
The issue:
There appears to be a memory leak when using EF + NpgsqlDataSource with password provider.
Versions:
Npgsql.EntityFrameworkCore.PostgreSQL - 9.0.4
Npgsql - 9.0.3
Reproducible example attached - MemoryLeak.zip (includes simple docker-compose with postgres for comfort)
Relevant code breakdown:
AppDbContext.cs
internal class AppDbContext : DbContext
{
public DbSet<SomeModel> SomeModels { get; set; }
public DbSet<SomeOtherModel> SomeOtherModels { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeModel>(b =>
{
b.HasKey(e => e.Id);
b.OwnsOne(e => e.JsonModel).ToJson();
});
modelBuilder.Entity<SomeOtherModel>(b =>
{
b.HasKey(e => e.Id);
});
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(@"User ID=postgres;Server=localhost;Port=5433;Database=postgres_db;Pooling=true;", sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(maxRetryCount: 3);
sqlOptions.ConfigureDataSource(dsBuilder =>
{
dsBuilder.ConnectionStringBuilder.Host = "localhost";
dsBuilder.Name = "postgres_db";
dsBuilder.UsePasswordProvider(_ => Guid.NewGuid().ToString(), (_, _) => ValueTask.FromResult(Guid.NewGuid().ToString()));
});
});
optionsBuilder.UseSnakeCaseNamingConvention();
base.OnConfiguring(optionsBuilder);
}
}
Program.cs
using MemoryLeak;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContext<AppDbContext>();
var app = builder.Build();
for(var i = 0; i < 1_000_000; ++i)
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await dbContext.Database.CanConnectAsync();
if (i % 100 == 0)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine($"Iteration: {i}");
await Task.Delay(3000);
}
}
From what I can understand, it seems like there is some cache based on ConnectionString, giving the fact that password provider changes the connection string (in this example all the time) - the cache grows indefinitely.
As soon as you change the password provider to return some constant string - everything goes to normal
(For example - dsBuilder.UsePasswordProvider(_ => "constant", (_, _) => ValueTask.FromResult("string"));) :

This is a critical issue, because it is a requirement for a lot of applications to use for example IAM Authorization to connect to RDS - hence use of RDSAuthTokenGenerator for password generation. The application memory grows "indefinitely" in this scenario. The rate of growth seems to be dependent on model size.
Thanks for the help.