* Add cancellation handling for executing scheduled tasks Enhanced `ScheduledRecurringTask`, `ScheduledCronTask`, and `ScheduledSpecificInstantTask` to properly handle cancellation scenarios when tasks are executing. Introduced `_executing` and `_cancellationRequested` flags to ensure clean cancellation processes. * Refactor `ScheduledRecurringTask` to improve readability and fix formatting issues. * Add `SemaphoreSlim` for concurrent task execution control in scheduled tasks Introduce `SemaphoreSlim` to manage and safeguard concurrent executions in `ScheduledRecurringTask`, `ScheduledCronTask`, and `ScheduledSpecificInstantTask`. Enhances thread safety and prevents overlapping executions. Added exception handling and proper semaphore release to ensure robustness. * Add ILogger to scheduled tasks and improve error logging Integrated `ILogger` into `ScheduledRecurringTask` to enhance logging capabilities and replaced the generic comment-based error handling with proper logging for better traceability. Cleaned up and formatted `ScheduledCronTask` for improved code readability. * Dispose of semaphore fields in scheduled task classes to ensure proper resource cleanup. * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledRecurringTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Set `_executing` to `false` in task `finally` blocks to ensure state reset after execution. * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove redundant `_executing` assignment after `SendAsync` in scheduled task classes. * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledRecurringTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledRecurringTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledCronTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update semaphore logic to prevent blocked tasks when cancellation is requested Refactored `_executionSemaphore.WaitAsync` usage in `ScheduledRecurringTask`, `ScheduledCronTask`, and `ScheduledSpecificInstantTask` to use non-blocking semaphore acquisition with cancellation token support. This ensures graceful handling of pending tasks during cancellation scenarios. * Refactor delay condition checks to use `TimeSpan.Zero` for improved readability and precision. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
110 lines
3.3 KiB
C#
110 lines
3.3 KiB
C#
using Elsa.Common;
|
|
using Elsa.Mediator.Contracts;
|
|
using Elsa.Scheduling.Commands;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Timer = System.Timers.Timer;
|
|
|
|
namespace Elsa.Scheduling.ScheduledTasks;
|
|
|
|
/// <summary>
|
|
/// A task that is scheduled to execute at a specific instant.
|
|
/// </summary>
|
|
public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|
{
|
|
private readonly ITask _task;
|
|
private readonly ISystemClock _systemClock;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly ILogger<ScheduledSpecificInstantTask> _logger;
|
|
private readonly DateTimeOffset _startAt;
|
|
private readonly CancellationTokenSource _cancellationTokenSource;
|
|
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
|
private Timer? _timer;
|
|
private bool _executing;
|
|
private bool _cancellationRequested;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of <see cref="ScheduledSpecificInstantTask"/>.
|
|
/// </summary>
|
|
public ScheduledSpecificInstantTask(ITask task, DateTimeOffset startAt, ISystemClock systemClock, IServiceScopeFactory scopeFactory, ILogger<ScheduledSpecificInstantTask> logger)
|
|
{
|
|
_task = task;
|
|
_systemClock = systemClock;
|
|
_scopeFactory = scopeFactory;
|
|
_logger = logger;
|
|
_startAt = startAt;
|
|
_cancellationTokenSource = new();
|
|
|
|
Schedule();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Cancel()
|
|
{
|
|
_timer?.Dispose();
|
|
|
|
if (_executing)
|
|
{
|
|
_cancellationRequested = true;
|
|
return;
|
|
}
|
|
|
|
_cancellationTokenSource.Cancel();
|
|
}
|
|
|
|
private void Schedule()
|
|
{
|
|
var now = _systemClock.UtcNow;
|
|
var delay = _startAt - now;
|
|
|
|
if (delay <= TimeSpan.Zero)
|
|
delay = TimeSpan.FromMilliseconds(1);
|
|
|
|
_timer = new(delay.TotalMilliseconds)
|
|
{
|
|
Enabled = true
|
|
};
|
|
|
|
_timer.Elapsed += async (_, _) =>
|
|
{
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
|
var cancellationToken = _cancellationTokenSource.Token;
|
|
if (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
|
if (!acquired) return;
|
|
_executing = true;
|
|
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
|
|
|
if (_cancellationRequested)
|
|
{
|
|
_cancellationRequested = false;
|
|
_cancellationTokenSource.Cancel();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogError(e, "Error executing scheduled task");
|
|
}
|
|
finally
|
|
{
|
|
_executing = false;
|
|
_executionSemaphore.Release();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
void IDisposable.Dispose()
|
|
{
|
|
_cancellationTokenSource.Dispose();
|
|
_timer?.Dispose();
|
|
_executionSemaphore.Dispose();
|
|
}
|
|
} |