简体   繁体   中英

Autofac and memory leak with BeginLifetimeScope / DbContext has been disposed / C# asp.net

I am using NServiceBus Scheduler that's why I'm forced to use BeginLifetimeScope to avoid memory leak.

Method:

public void Start()
{
    using (var scope = _lifetimeScope.BeginLifetimeScope())
    {
        var jobs = scope.Resolve<IEnumerable<IJob>>();

        foreach (var job in jobs)
        {
            _schedule.Every(job.GetTimeInterval(), () =>
            {
                job.Run();
            });
        }
    }
}

And Autofac Configuration for context:

private static void RegisterContext(ContainerBuilder builder)
{
    builder.Register(c => new MyContext(GetConnectionString())).InstancePerLifetimeScope();
}

If MyContext has InstancePerLifetimeScope then whilst I'm trying to use it - I have an error: System.InvalidOperationException: 'The operation cannot be completed because the DbContext has been disposed.'

So if I'll change it to SingleInstance() then of course everything works but context isn't disposed and I can have memory leak...


@EDIT

I have fixed the problem, here is solution:

public void Start()
{
    List<Type> jobTypes = new List<Type> { typeof(ExpiryDateTask) };

    foreach (var jobType in jobTypes)
    {
        _schedule.Every(TimeSpan.FromSeconds(30), () =>
        {
            using (var scope = _lifetimeScope.BeginLifetimeScope())
            {
                var job = scope.Resolve<IJob>();
                job.Run();
            }
        });
    }
}

But I wondering how can I refactor this part:

  1. List<Type> jobTypes = new List<Type> { typeof(ExpiryDateTask) }; - that list should be filled somehow by all Types of Tasks that implement IJob interface.
  2. var job = scope.Resolve<IJob>(); I think this is wrong and should looks more like var job = resolveJob(jobType) - so basically based on the type.

It happens most likely because the job is run asynchronously, that is job.Run() is called after execution exits the using statement. At the time job.Run() is called, the lifetime scope is already disposed including underlying database context.

Try moving the using statement inside of the scheduler callback. After all it makes most sense to execute each scheduled job in a separate lifetime context.

More details:

I suppose MyContext is injected into a constructor of a class that implements IJob , right? With respect to your original code I think there are two contradicting goals:

  1. you instantiate each job upfront to GetTimeInterval and schedule it
  2. each job can be run multiple times and each execution should have it own DB context and dispose of it when it's done.

It's not possible to get both at the same time. You need to separate the scheduling part from the execution part.

One way to do it is to use Autofac's Metadata . It allows you to take contextual info about a component into account before actually instantiating it. I've written up a working code sample that shows how to do it.

It boils down to:

  1. registering a job with metadata

     builder.RegisterType<JobImplementation>() .WithMetadata<JobMetadata>(conf => conf.For(meta => meta.Interval, TimeSpan.FromSeconds(30)) .For(meta => meta.Type, typeof(JobImplementation)) ) .As<IJob>() .InstancePerLifetimeScope(); 
  2. resolving the specific job instance in scheduler callback using this metadata

      using (var scope = container.BeginLifetimeScope()) { //Select this one specific job by its metadata. var specificJobInitializer = scope .Resolve<IEnumerable<Meta<Func<IJob>, JobMetadata>>>() .Single(jobInitializer => jobInitializer.Metadata.Type == jobDefinition.Type); IJob job = specificJobInitializer.Value(); job.Run(); } 

I will extend on pbalaga answer. To avoid problem with disposed DbContext , one option is to use some kind of factory for DbContext and create DbContext inside job. Another option is to use different scheduler that offers delayed job resolution, we used Quartz.NET with MassTransit and Topshelf, and there was option to provide own Scheduler like this:

ScheduleJobServiceConfiguratorExtensions.SchedulerFactory = () => container.Resolve<IScheduler>();

and if I recall correctly, job was resolved at the moment when it was supposed to be fired:

serviceConfigurator.ScheduleQuartzJob(q =>
            {
                q.WithJob(JobBuilder.Create<EventsJob>()
                    .WithIdentity("Events processing", "Events")
                    .Build);
                q.AddTrigger(() => TriggerBuilder.Create().StartAt(DateTimeOffset.Now.AddSeconds(30))
                    .WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(1)).Build());
            });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM