Quartz.NET 3.0, похоже, запускает все задания в одной и той же области

#c# #quartz.net #asp.net-core-2.2

#c# #quartz.net #asp.net-core-2.2

Вопрос:

Мне трудно использовать Quartz 3.0.7 с ASP.NET Ядро 2.2 после того, как я определил два задания, которые полагаются на службу с ограниченной областью (ScopedDataAccess), которая является оболочкой для контекста моей базы данных:

 services.AddScoped<IScopedDataAccess, ScopedDataAccess>();

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

Проблема в том, что оба задания получают один и тот же экземпляр службы с ограниченной областью действия (и, следовательно, один и тот же контекст базы данных), что приводит к сбою контекста из-за параллельного использования.

Мой код выглядит следующим образом:

Startup.cs

Задания определяются как «ограниченные», и я ожидаю, что каждый экземпляр будет выполняться в своей собственной «области»

 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().Resu<

        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) { }
}
  

Есть ли способ использовать Quartz.NET чтобы получить разные области для разных заданий?

Ответ №1:

Как я знаю, это невозможно с Quartz, я боролся с теми же проблемами, и единственным решением, которое я нашел, было использовать ServiceLocator и явно создать область в задании.

Я закончил чем-то вроде этого:

 // 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();
        }
    }
}
  

В этом случае ваш рабочий все еще ограничен, но задания больше нет. Таким образом, вы все еще можете использовать свой Worker в других местах вашего решения, и область все еще работает.
Вам необходимо реализовать ServiceLocator самостоятельно в зависимости от используемого вами DI, и IServiceLocator он также должен быть определен вами.

Редактировать

В одном из наших проектов мы используем это:

 /// <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;
}
  

В этой реализации мы используем в основном SimpleInjector:

 /// <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>();
    }
}
  

Как вы можете видеть, это всего лишь простая оболочка, но помогает скрыть реальную структуру DI от потребителей.
Я надеюсь, что это немного поможет понять вашу необходимую реализацию.

Комментарии:

1. Если я правильно понял, я сохраняю задания как ограниченные ( services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Scoped))); ) и использую service locator для создания отдельной области. Я использую встроенный DI, но неясно, как реализовать ServiceLocator service. Я искал и нашел службы поиска на основе HttpContext. ApplicationServices. но HttpContext недоступен в этом контексте (он не связан ни с каким запросом).

2. Я протестирую предлагаемое решение, как только разберусь с локатором служб. Спасибо.

3. @Alexey Задания больше не ограничены, потому что вам это не нужно, если вы открываете область и разрешаете службы вручную.

4. Да, это имеет смысл. Я повторю предоставленную реализацию и дам вам знать. Спасибо.

5. Поскольку я не использую SimpleInjector, я создал статическую ссылку в Startup.cs, которую я использую для создания новой области для каждого задания: QuartzScopedProvider = services.BuildServiceProvider() и service locator будет использовать это для создания новых областей: _serviceScope = Startup.QuartzScopedProvider.CreateScope(); . Имеет ли это смысл? В любом случае, ваш ответ действительно помог мне понять, как все работает.