A while back, I blogged about a handy task scheduler and repeater. I use this code often and when I started using it with a heavy memory/data footprint, some serious memory leaks came to light.
Interestingly, the scheduler takes an action and runs it within a single repeating Task. However, for whatever reason, when I introduced my parent/child DI Ninject kernel within my Action, memory was not being released once the Action completed. This was very apparent when injecting an Entity Framework context and loading a few hundred thousand entities. It didn’t require much time for an out of memory exception to occur.
My first thought was that the issue was solely with Ninject. But, the problem would persist even without using Ninject. I could reproduce the problem even with the presence of a “using” block around my DbContext creation. After viewing a few memory profilers, it became evident that the loaded entities themselves were not being disposed on Action completion. I’m still not entirely sure what object reference is getting held within the repeating/scheduled Task.
At any rate, within my Action, I have a block of code like this:
try { // Isolated child kernel for context, repositories, and services.. IKernel taskKernel = new StandardKernel(); taskKernel.Load(new NinjectTaskModule(_kernel), new NinjectCommonModule()); var svcThatUsesEFContext = (ISomeService)taskKernel.GetService(typeof(ISomeService)); svcThatUsesEFContext.GetABunchOfData(); taskKernel.Dispose(); } catch (Exception ex) { // Handle Exception }
Despite the disposal of the “taskKernel,” memory wasn’t being released! In reading up on Tasks on MSDN, I decided to wrap the Action within its own Task and then dispose of the Task directly as an experiment. Within the “TaskScheduler,” I added this method and call it, rather than calling the Action directly, where the Action needed to be executed:
public void ExecuteAction() { var task = Task.Run(() => { _action(); }, ScheduleToken); task.Wait(); task.Dispose(); GC.Collect(); }
With that slight modification in place, the Action is now outside of the scope of the repeating/scheduled Task and memory is released as expected. I’m still unsure if this is a problem specific to Entity Framework, but the problem didn’t appear to manifest itself when using large POCO’s, outside of EF, within the Action.
Did you ever determine the root cause of this problem? I’m running into the same problem with DI.
I didn’t discover root cause, so I don’t know if it was a Ninject issue, EF issue, or simply my own mismanagement of in-memory objects – but it was something that only happened with EF 6.1, it seemed. Things could have improved with 6.2.
Since moving away from Ninject in favor of .NET Core and it’s built-in DI, I have found behavior is a bit more predictable.