#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; }
}
На всякий случай, если кому-то понадобится такое решение.