Quartz.NET 3.0 seems to launch all jobs in the same scope

◇◆丶佛笑我妖孽 提交于 2019-12-24 07:16:17

问题


I have a hard time using Quartz 3.0.7 with ASP.NET Core 2.2 after I have defined two jobs that rely on a scoped service (ScopedDataAccess) that is a wrapper upon my database context:

services.AddScoped<IScopedDataAccess, ScopedDataAccess>();

services.AddDbContext<AggregatorContext>(opt => opt.UseSqlServer(configuration.GetConnectionString("Default")));

The issue is that both jobs receive the same instance of the scoped service (and thus the same database context), thus crashing the context due to parallel usage.

My code is as follows:

Startup.cs

Jobs are defined as "scoped" and my expectation is for each instance to run in its own "scope"

private void ConfigureQuartz(IServiceCollection services, params Type[] jobs)
{
    services.AddSingleton<IJobFactory, QuartzJobFactory>();
    services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped)));

    services.AddSingleton(provider =>
    {
        var schedulerFactory = new StdSchedulerFactory();
        var scheduler = schedulerFactory.GetScheduler().Result;

        scheduler.JobFactory = provider.GetService<IJobFactory>();
        scheduler.Start();
        return scheduler;
    });
}

protected void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime)
{
    var scheduler = app.ApplicationServices.GetService<IScheduler>();
    var configService = app.ApplicationServices.GetService<IConfigurationService>();

    QuartzServicesUtilities.StartJob<ArticleXUserDataRefresherJob>(scheduler, 
        TimeSpan.FromSeconds(configService.ArticleXUserDataRefresherJobPeriod));
    QuartzServicesUtilities.StartJob<LinkDataFetchJob>(scheduler,
        TimeSpan.FromSeconds(configService.LinkDataJobPeriod));

    lifetime.ApplicationStarted.Register(() => scheduler.Start());
    lifetime.ApplicationStopping.Register(() => scheduler.Shutdown());
}

QuartzServicesUtilities

public class QuartzServicesUtilities
{
    public static void StartJob<TJob>(IScheduler scheduler, TimeSpan runInterval)
        where TJob : IJob
    {
        var jobName = typeof(TJob).FullName;

        var job = JobBuilder.Create<TJob>()
            .WithIdentity(jobName)
            .Build();

        var trigger = TriggerBuilder.Create()
            .WithIdentity($"{jobName}.trigger")
            .StartNow()
            .WithSimpleSchedule(scheduleBuilder =>
                scheduleBuilder
                    .WithInterval(runInterval)
                    .RepeatForever())
            .Build();

        scheduler.ScheduleJob(job, trigger);
    }
}

QuartzJobFactory

public class QuartzJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    public QuartzJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        var jobDetail = bundle.JobDetail;

        var job = (IJob)_serviceProvider.GetService(jobDetail.JobType);
        return job;
    }

    public void ReturnJob(IJob job) { }
}

Is there a way to use Quartz.NET to obtain different scopes for different jobs?


回答1:


As i know, this is not possible with Quartz, i struggled with the same issues and the only solution i found was to use a ServiceLocator and create the scope explicitly in the Job.

I ended with something like that:

// Pseudo-Code
public class MyJob : IJob
{
    private readonly IServiceLocator _serviceLocator;

    public MyJob(IServiceLocator serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public async Task Execute(JobExecutionContext context)
    {
        using(_serviceLocator.BeginScope())
        {
            var worker = _serviceLocator.GetService<MyWorker>();
            await worker.DoWorkAsync();
        }
    }
}

In this case, your worker is still scoped but the job isn't anymore. So you can still use your Worker in other places in your solution and the scope still works. You need to implement the ServiceLocator by yourself depending on the DI do you use and IServiceLocator must also be defined by you.

Edit

In one of our projects we use this:

/// <summary>
/// A simple service locator to hide the real IOC Container.
/// Lowers the anti-pattern of service locators a bit.
/// </summary>
public interface IServiceLocator
{
    /// <summary>
    /// Begins an new async scope.
    /// The scope should be disposed explicitly.
    /// </summary>
    /// <returns></returns>

    IDisposable BeginAsyncScope();
    /// <summary>
    /// Gets an instance of the given <typeparamref name="TService" />.
    /// </summary>
    /// <typeparam name="TService">Type of the requested service.</typeparam>
    /// <returns>The requested service instance.</returns>
    TService GetInstance<TService>() where TService : class;
}

We use mostly SimpleInjector with this implementation:

/// <summary>
/// SimpleInjector implementation of the service locator.
/// </summary>
public class ServiceLocator : IServiceLocator
{
    #region member vars

    /// <summary>
    /// The SimpleInjector container.
    /// </summary>
    private readonly Container _container;

    #endregion

    #region constructors and destructors

    public ServiceLocator(Container container)
    {
        _container = container;
    }

    #endregion

    #region explicit interfaces

    /// <inheritdoc />
    public IDisposable BeginAsyncScope()
    {
        return AsyncScopedLifestyle.BeginScope(_container);
    }

    /// <inheritdoc />
    public TService GetInstance<TService>()
        where TService : class
    {
        return _container.GetInstance<TService>();
    }
}

As you can see, this is just a simple wrapper but helps to hide the real DI Framework from the consumers. I hope this helps a little bit to understand your needed implementation.



来源:https://stackoverflow.com/questions/55211511/quartz-net-3-0-seems-to-launch-all-jobs-in-the-same-scope

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!