My software using EF Core in combination with a SQLite database within an ASP.NET Core Web API using dependency injection, has a memory leak.
I have a background job using Quartz which gets called every 9 seconds.
My context looks like this:
public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext
{
public DbSet<ChargePrice> ChargePrices { get; set; } = null!;
public DbSet<HandledCharge> HandledCharges { get; set; } = null!;
public DbSet<PowerDistribution> PowerDistributions { get; set; } = null!;
public string DbPath { get; }
public void RejectChanges()
{
foreach (var entry in ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
case EntityState.Deleted:
entry.State = EntityState.Modified; //Revert changes made to deleted entity.
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
public TeslaSolarChargerContext()
{
}
public TeslaSolarChargerContext(DbContextOptions<TeslaSolarChargerContext> options)
: base(options)
{
}
}
With the interface
public interface ITeslaSolarChargerContext
{
DbSet<ChargePrice> ChargePrices { get; set; }
DbSet<HandledCharge> HandledCharges { get; set; }
DbSet<PowerDistribution> PowerDistributions { get; set; }
ChangeTracker ChangeTracker { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
DatabaseFacade Database { get; }
void RejectChanges();
}
In my Program.cs
I add the context and Quartz job to the dependency injection like that:
builder.Services.AddDbContext<ITeslaSolarChargerContext, TeslaSolarChargerContext>((provider, options) =>
{
options.UseSqlite(provider.GetRequiredService<IDbConnectionStringHelper>().GetTeslaSolarChargerDbPath());
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
}, ServiceLifetime.Transient, ServiceLifetime.Transient)
.AddTransient<IChargingCostService, ChargingCostService>();
builder.Services
.AddSingleton<JobManager>()
.AddTransient<PowerDistributionAddJob>()
.AddTransient<IJobFactory, JobFactory>()
.AddTransient<ISchedulerFactory, StdSchedulerFactory>();
I am using my own JobManager
because job intervalls can be configured in various ways so I inject a wrapper into my JobManager
and it is Singleton as I need to stop my jobs at any time as the job intervall can be updated during runtime, so I need to stop and start the jobs:
public class JobManager
{
private readonly ILogger<JobManager> _logger;
private readonly IJobFactory _jobFactory;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IConfigurationWrapper _configurationWrapper;
private IScheduler _scheduler;
public JobManager(ILogger<JobManager> logger, IJobFactory jobFactory, ISchedulerFactory schedulerFactory, IConfigurationWrapper configurationWrapper)
{
_logger = logger;
_jobFactory = jobFactory;
_schedulerFactory = schedulerFactory;
_configurationWrapper = configurationWrapper;
}
public async Task StartJobs()
{
_logger.LogTrace("{Method}()", nameof(StartJobs));
_scheduler = _schedulerFactory.GetScheduler().GetAwaiter().GetResult();
_scheduler.JobFactory = _jobFactory;
var powerDistributionAddJob = JobBuilder.Create<PowerDistributionAddJob>().Build();
var powerDistributionAddTrigger = TriggerBuilder.Create()
.WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)_configurationWrapper.JobIntervall().TotalSeconds)).Build();
var triggersAndJobs = new Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>>
{
{powerDistributionAddJob, new HashSet<ITrigger> {powerDistributionAddTrigger}},
};
await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false);
await _scheduler.Start().ConfigureAwait(false);
}
public async Task StopJobs()
{
await _scheduler.Shutdown(true).ConfigureAwait(false);
}
}
The job looks like this:
[DisallowConcurrentExecution]
public class PowerDistributionAddJob : IJob
{
private readonly ILogger<ChargeTimeUpdateJob> _logger;
private readonly IChargingCostService _service;
public PowerDistributionAddJob(ILogger<ChargeTimeUpdateJob> logger, IChargingCostService service)
{
_logger = logger;
_service = service;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogTrace("{method}({context})", nameof(Execute), context);
await _service.AddPowerDistributionForAllChargingCars().ConfigureAwait(false);
}
}
The context is injected to the service like that:
public ChargingCostService(ILogger<ChargingCostService> logger,
ITeslaSolarChargerContext teslaSolarChargerContext)
{
_logger = logger;
_teslaSolarChargerContext = teslaSolarChargerContext;
}
I use the context within a service and just call this method:
var chargePrice = await _teslaSolarChargerContext.ChargePrices
.FirstOrDefaultAsync().ConfigureAwait(false);
Calling this results in the app blowing using 1GB RAM after a week.
After analyzing a memory dump I found that after about 8 hours I have over two thousand instances of TeslaSolarChargerContext
.