Search code examples
c#.net-corememory-leaksentity-framework-coreautofac

EF core Memory leak in multithreaded console application


I created a small console application, where I am simulating a memory leak issue I have using EF core in an Azure Service Bus messaging application.

The setup is as follows:

I inherit from DbContext in MyContext

public class MyContext : DbContext
{
    private readonly Assembly configurationAssembly;

    public MyContext(Assembly configurationAssembly, DbContextOptions dbContextOptions) : base(dbContextOptions)
    {
        this.configurationAssembly = configurationAssembly;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(this.configurationAssembly);
    }
}

I have a POCO class:

public class Job
{
    public int Id { get; set; }
    public string Applicationcode { get; set; }
}

And an EntityTypeConfiguration:

public class JobConfig : IEntityTypeConfiguration<Job>
{
    public void Configure(EntityTypeBuilder<Job> builder)
    {
        builder.ToTable("Job_OP");
    }
}

MyContext uses this configuration in the OnModelCreating override (ApplyConfigurationFromAssembly)

Finally in Program I have the following code:

class Program
{
    private static IContainer container;
    static void Main(string[] args)
    {
        var builder = new ContainerBuilder(); //Autofac dependency injection

        var dbContextOptionsBuilder = new DbContextOptionsBuilder().UseSqlServer("Data Source=localhost;Initial Catalog=TestEFThreads;Integrated Security=True");

        builder.Register(ctx => new MyContext(
            Assembly.Load("TestEFThreads"),
            dbContextOptionsBuilder.Options));
        //I tried to use different lifetime scopes here, but EF doesn't like using the same dbContext concurrently in different threads.
        //Currently each resolve instantiates a new MyContext

        container = builder.Build();

        var input = ShowMenu();
        var nrOfSimulatedMessages = 0;
        while (!string.Equals(input, "q", StringComparison.OrdinalIgnoreCase))
        {
            try
            {
                nrOfSimulatedMessages = Convert.ToInt32(input);
                PerformSimulation(maxThreads: 10, nrOfSimulatedMessages);
            }
            catch (Exception e)
            {
                Console.WriteLine($"Incorrect input or exception, {e}");
            }
            input = ShowMenu().ToLower();
        }
    }

    static string ShowMenu()
    {
        Console.WriteLine("Enter number of simulated messages or q to quit");
        return Console.ReadLine();
    }

    private static void PerformSimulation(int maxThreads, int nrOfSimulatedMessages)
    {
        var messageCount = 0;

        var tasks = new Task[maxThreads];

        while (messageCount < nrOfSimulatedMessages)
        {
            for (int i = 0; i < maxThreads && messageCount < nrOfSimulatedMessages; i++)
            {
                Console.WriteLine($"Creating thread {i} for message {messageCount}");
                tasks[i] = new Task(StartProcessing);
                tasks[i].Start();
                messageCount++;
            }
            Console.WriteLine("Waiting for all threads to finish");
            Task.WaitAll(tasks.Where(a=>a !=null).ToArray());
        }
    }

    private static void StartProcessing()
    {
        using (var lifeTime = container.BeginLifetimeScope()) //does not help
        {
            var myContext = container.Resolve<MyContext>();
            var job = myContext.Set<Job>().SingleOrDefault(a => a.Id == 1);
            Console.WriteLine($"Found job with applicationCode {job.Applicationcode}");
            myContext.Dispose(); //memory leak even with dispose
        }
    }
}

So this application does a simple read from a db, and does so a specified number of times, each in a separate thread.

When I run it for about 50000 'messages' the memory keeps increasing as can be seen in the diagnostics. It does not seem to be garbage collected.

Diagnostics

Does anybody have an idea on how to fix this memory leak?

Update: It seems to be related to the Autofac dependency injection. If I just use new MyContext iso resolving it memory usage is fine.

    private static void StartProcessing()
    {
        var myContext = new MyContext(Assembly.Load("TestEFThreads"), dbContextOptionsBuilder.Options);

        var job = myContext.Set<Job>().SingleOrDefault(a => a.Id == 1);
        Console.WriteLine($"Found job with applicationCode {job.Applicationcode}");
    }

I do need to use dependency injection however.


Solution

  • Using ExternallyOwned seems to do the trick.

        builder.Register(ctx => new MyContext(
            Assembly.Load("TestEFThreads"),
            dbContextOptionsBuilder.Options)).ExternallyOwned();
    

    Normal memory consumption