Динамическое изменение интервала выполнения рабочей службы

#c#

Вопрос:

У меня есть рабочая служба с базовым классом для всех рабочих, которая принимает параметр IOptionsMonitor конструктора. Этот монитор содержит экземпляр объекта options со значением интервала выполнения. Вопрос в том, как динамически изменять интервал, даже когда await Task.Delay(Interval); он был вызван? Я имею в виду, что если Interval значение равно одному дню, а после Task.Delay вызова метода оно изменяется, например, на один час — мне все равно нужно подождать один день, и только при следующем вызове задержка будет обновлена. Как я могу отменить текущую задержку и начать новую, если значение свойства Interval было обновлено? Спасибо.

Пожалуйста, ознакомьтесь с кодом, прилагаемым ниже:

 public abstract class WorkerBase<TWorker, TWorkerOptions> : BackgroundService
    where TWorker : WorkerBase<TWorker, TWorkerOptions>
    where TWorkerOptions : IHaveIntervalProperty
{
    protected WorkerBase(IServiceProvider serviceProvider, ILogger<TWorker> logger, IOptionsMonitor<TWorkerOptions> options)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
        _workerName = typeof(TWorker).Name;
        Interval = options.CurrentValue.Interval;
        options.OnChange(UpdateOptions);
    }

    public TimeSpan Interval { get; private set; }

    public virtual void UpdateOptions(TWorkerOptions options)
        => Interval = options.Interval;

    public abstract Task DoWork(IServiceProvider provider);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation(Logs.InformationWorkerRunning, _workerName, DateTime.UtcNow);

            try
            {
                using var scope = _serviceProvider.CreateScope();
                await DoWork(scope.ServiceProvider);
            }
            catch (Exception e)
            {
                _logger.LogCritical(e, e.Message);
            }
            finally
            {
                await Task.Delay(Interval, stoppingToken);
            }
        }
    }
}
 

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

1. Не используйте await Task.Delay . Вместо этого используйте таймер. Если вы измените внутренний таймер, новое значение будет применено немедленно. Кроме того, Task.Delay использует сам таймер. Если вы не избавитесь от возвращаемой задачи, этот таймер утечет. Это означает, что текущий код пропускает таймеры

2. @PanagiotisKanavos, вау, я совсем забыл о таймерах. Спасибо вам за вашу помощь. Вы можете опубликовать ответ, и я отмечу его как правильный.

3. @PanagiotisKanavos, но, кстати, не могли бы вы приложить небольшой пример кода? В настоящее время я немного запутался в том, какой класс использовать Timer DispatcherTimer или что-то еще. Спасибо

Ответ №1:

Итак, основываясь на комментарии @Panagiotis Kanavos, я придумал следующий код:

 public abstract class RepeatableWorker<TWorker, TOptions> : IHostedService, IDisposable
    where TWorker : RepeatableWorker<TWorker, TOptions>
    where TOptions : IHaveIntervalProperty
{
    #region Fields
    private readonly IServiceProvider _serviceProvider;
    private protected readonly ILogger<TWorker> _logger;
    private readonly string _workerName;
    private Timer? _executionTimer;
    private TimeSpan _interval;
    #endregion

    #region Constructors
    protected RepeatableWorker(IServiceProvider serviceProvider,
                               ILogger<TWorker> logger,
                               IOptionsMonitor<TOptions> options)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
        _workerName = typeof(TWorker).Name;
        _interval = options.CurrentValue.Interval;
        options.OnChange(UpdateOptions);
    }
    #endregion

    #region Properties
    public TimeSpan Interval
    {
        get => _interval;
        private set
        {
            if (value != _interval)
            {
                _executionTimer?.Change(TimeSpan.Zero, value);
                _interval = value;
            }
        }
    }
    #endregion

    #region Public methods
    public virtual void UpdateOptions(TOptions options)
        => Interval = options.Interval;

    public abstract void DoWork(IServiceProvider serviceProvider);

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(Logs.InformationWorkerStarting, _workerName, DateTime.UtcNow);
        _executionTimer = new(DoWorkInternal, null, TimeSpan.Zero, Interval);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(Logs.InformationWorkerStopping, _workerName, DateTime.UtcNow);
        _executionTimer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        _executionTimer?.Dispose();
    }
    #endregion

    #region Private methods
    private void DoWorkInternal(object? state)
    {
        try
        {
            _logger.LogInformation("Worker {0} running at {1}.", _workerName, DateTime.UtcNow);
            using var scope = _serviceProvider.CreateScope();
            DoWork(scope.ServiceProvider);
        }
        catch (Exception e)
        {
            _logger.LogCritical(e, e.Message);
        }
    }
    #endregion
}
 

И IHaveIntervalProperty интерфейс:

 public interface IHaveIntervalProperty
{
    TimeSpan Interval { get; set; }
}
 

На всякий случай, если кому-то понадобится такое решение.