Co-authored-by: Kvyatkovsky, Petr <pkvyatkovsky@qulix.com>
(cherry picked from commit 3a9ef3810b)
179 lines
6.8 KiB
C#
179 lines
6.8 KiB
C#
// Copyright (c) Umbraco.
|
|
// See LICENSE for more details.
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Umbraco.Cms.Core;
|
|
using Umbraco.Cms.Core.Configuration;
|
|
|
|
namespace Umbraco.Cms.Infrastructure.HostedServices;
|
|
|
|
/// <summary>
|
|
/// Provides a base class for recurring background tasks implemented as hosted services.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// See: <see href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks"/>.
|
|
/// </remarks>
|
|
public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The default delay to use for recurring tasks for the first run after application start-up if no alternative is
|
|
/// configured.
|
|
/// </summary>
|
|
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
|
|
|
|
private readonly TimeSpan _delay;
|
|
|
|
private readonly ILogger? _logger;
|
|
private bool _disposedValue;
|
|
private TimeSpan _period;
|
|
private Timer? _timer;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="RecurringHostedServiceBase" /> class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger.</param>
|
|
/// <param name="period">Timespan representing how often the task should recur.</param>
|
|
/// <param name="delay">
|
|
/// Timespan representing the initial delay after application start-up before the first run of the task
|
|
/// occurs.
|
|
/// </param>
|
|
protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay)
|
|
{
|
|
_logger = logger;
|
|
_period = period;
|
|
_delay = delay;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
|
|
/// configuration for the first run time is available.
|
|
/// </summary>
|
|
/// <param name="firstRunTime">The configured time to first run the task in crontab format.</param>
|
|
/// <param name="cronTabParser">An instance of <see cref="ICronTabParser"/></param>
|
|
/// <param name="logger">The logger.</param>
|
|
/// <param name="defaultDelay">The default delay to use when a first run time is not configured.</param>
|
|
/// <returns>The delay before first running the recurring task.</returns>
|
|
protected static TimeSpan GetDelay(
|
|
string firstRunTime,
|
|
ICronTabParser cronTabParser,
|
|
ILogger logger,
|
|
TimeSpan defaultDelay) => GetDelay(firstRunTime, cronTabParser, logger, DateTime.Now, defaultDelay);
|
|
|
|
/// <summary>
|
|
/// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
|
|
/// configuration for the first run time is available.
|
|
/// </summary>
|
|
/// <param name="firstRunTime">The configured time to first run the task in crontab format.</param>
|
|
/// <param name="cronTabParser">An instance of <see cref="ICronTabParser"/></param>
|
|
/// <param name="logger">The logger.</param>
|
|
/// <param name="now">The current datetime.</param>
|
|
/// <param name="defaultDelay">The default delay to use when a first run time is not configured.</param>
|
|
/// <returns>The delay before first running the recurring task.</returns>
|
|
/// <remarks>Internal to expose for unit tests.</remarks>
|
|
internal static TimeSpan GetDelay(
|
|
string firstRunTime,
|
|
ICronTabParser cronTabParser,
|
|
ILogger logger,
|
|
DateTime now,
|
|
TimeSpan defaultDelay)
|
|
{
|
|
// If first run time not set, start with just small delay after application start.
|
|
if (string.IsNullOrEmpty(firstRunTime))
|
|
{
|
|
return defaultDelay;
|
|
}
|
|
|
|
// If first run time not a valid cron tab, log, and revert to small delay after application start.
|
|
if (!cronTabParser.IsValidCronTab(firstRunTime))
|
|
{
|
|
logger.LogWarning("Could not parse {FirstRunTime} as a crontab expression. Defaulting to default delay for hosted service start.", firstRunTime);
|
|
return defaultDelay;
|
|
}
|
|
|
|
// Otherwise start at scheduled time according to cron expression, unless within the default delay period.
|
|
DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now);
|
|
TimeSpan delay = firstRunOccurance - now;
|
|
return delay < defaultDelay
|
|
? defaultDelay
|
|
: delay;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null)
|
|
{
|
|
_timer = new Timer(ExecuteAsync, null, _delay, _period);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_period = Timeout.InfiniteTimeSpan;
|
|
_timer?.Change(Timeout.Infinite, 0);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes the task.
|
|
/// </summary>
|
|
/// <param name="state">The task state.</param>
|
|
public virtual async void ExecuteAsync(object? state)
|
|
{
|
|
try
|
|
{
|
|
// First, stop the timer, we do not want tasks to execute in parallel
|
|
_timer?.Change(Timeout.Infinite, 0);
|
|
|
|
// Delegate work to method returning a task, that can be called and asserted in a unit test.
|
|
// Without this there can be behaviour where tests pass, but an error within them causes the test
|
|
// running process to crash.
|
|
// Hat-tip: https://stackoverflow.com/a/14207615/489433
|
|
await PerformExecuteAsync(state);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType());
|
|
logger.LogError(ex, "Unhandled exception in recurring hosted service.");
|
|
}
|
|
finally
|
|
{
|
|
// Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay.
|
|
// So first execution is after _delay, and the we wait _period between each
|
|
_timer?.Change(_period, _period);
|
|
}
|
|
}
|
|
|
|
public abstract Task PerformExecuteAsync(object? state);
|
|
|
|
/// <summary>
|
|
/// Change the period between operations.
|
|
/// </summary>
|
|
/// <param name="newPeriod">The new period between tasks</param>
|
|
protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod;
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!_disposedValue)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_timer?.Dispose();
|
|
}
|
|
|
|
_disposedValue = true;
|
|
}
|
|
}
|
|
}
|