Skip to content

Disposable job is disposed twice when using UseMicrosoftDependencyInjectionScopedJobFactory #1120

Description

@ArturDorochowicz

Describe the bug

Quartz job that implements IDisposable is disposed twice when using DependencyInjectionJobFactory with CreateScope enabled (e.g. via UseMicrosoftDependencyInjectionScopedJobFactory extension method).

This shouldn't be a big problem, because properly implemented Dispose can be called multiple times safely according to Framework Design Guidelines. But still, it's something that could be improved.

The reason the duplicate disposal happens is because the job is first disposed when disposing the DI scope and then explicitly by ScopedJob:

public void Dispose()
{
scope.Dispose();
(innerJob as IDisposable)?.Dispose();
}

Version used

3.2.4

To Reproduce

Prepare a test project:

dotnet new nunit --name QuartzTests
cd QuartzTests
dotnet add package Quartz.Extensions.DependencyInjection --version 3.2.4
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging

Set UnitTest1.cs to:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Quartz;

namespace QuartzTests
{
    public class Tests
    {
        [Test]
        public async Task Test1()
        {
            var services = new ServiceCollection();
            services.AddLogging();
            services.AddQuartz(cfg =>
            {
                cfg.UseMicrosoftDependencyInjectionScopedJobFactory();
                cfg.ScheduleJob<MyJob>(t => { });
            });
            var sp = services.BuildServiceProvider();
            var schedulerFactory = sp.GetRequiredService<ISchedulerFactory>();
            var scheduler = await schedulerFactory.GetScheduler();

            await scheduler.Start();
            await scheduler.Shutdown(waitForJobsToComplete: true);

            Assert.AreEqual(1, MyJob.ExecuteCount);
            // FAILS, ACTUAL = 2
            Assert.AreEqual(1, MyJob.DisposeCount);
        }

        class MyJob : IJob, IDisposable
        {
            static int _executeCount;
            static int _disposeCount;

            public Task Execute(IJobExecutionContext context)
            {
                Interlocked.Increment(ref _executeCount);
                return Task.CompletedTask;
            }

            public void Dispose()
            {
                Interlocked.Increment(ref _disposeCount);
            }

            public static int ExecuteCount => _executeCount;

            public static int DisposeCount => _disposeCount;
        }
    }
}

Expected behavior

Job should be disposed only once.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions