< Summary

Information
Class: Elsa.Scheduling.ScheduledTasks.ScheduledRecurringTask
Assembly: Elsa.Scheduling
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledRecurringTask.cs
Line coverage
67%
Covered lines: 59
Uncovered lines: 28
Coverable lines: 87
Total lines: 151
Line coverage: 67.8%
Branch coverage
75%
Covered branches: 9
Total branches: 12
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Cancel()50%5466.66%
Schedule()100%44100%
SetupTimer(...)100%2253.57%
System.IDisposable.Dispose()50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledRecurringTask.cs

#LineLine coverage
 1using Elsa.Common;
 2using Elsa.Mediator.Contracts;
 3using Elsa.Scheduling.Commands;
 4using Microsoft.Extensions.DependencyInjection;
 5using Microsoft.Extensions.Logging;
 6using Timer = System.Timers.Timer;
 7
 8namespace Elsa.Scheduling.ScheduledTasks;
 9
 10/// <summary>
 11/// A scheduled recurring task.
 12/// </summary>
 13public class ScheduledRecurringTask : IScheduledTask, IDisposable
 14{
 15    private readonly ITask _task;
 16    private readonly ISystemClock _systemClock;
 17    private readonly IServiceScopeFactory _scopeFactory;
 18    private readonly ILogger<ScheduledRecurringTask> _logger;
 19    private readonly TimeSpan _interval;
 20    private readonly CancellationTokenSource _cancellationTokenSource;
 821    private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
 22    private DateTimeOffset _startAt;
 23    private Timer? _timer;
 24    private bool _executing;
 25    private bool _cancellationRequested;
 26    private bool _disposed;
 27
 28    /// <summary>
 29    /// Initializes a new instance of <see cref="ScheduledRecurringTask"/>.
 30    /// </summary>
 831    public ScheduledRecurringTask(ITask task, DateTimeOffset startAt, TimeSpan interval, ISystemClock systemClock, IServ
 32    {
 833        _task = task;
 834        _systemClock = systemClock;
 835        _scopeFactory = scopeFactory;
 836        _logger = logger;
 837        _startAt = startAt;
 838        _interval = interval;
 839        _cancellationTokenSource = new();
 40
 841        Schedule();
 842    }
 43
 44    /// <inheritdoc />
 45    public void Cancel()
 46    {
 147        _timer?.Dispose();
 48
 149        if (_executing)
 50        {
 051            _cancellationRequested = true;
 052            return;
 53        }
 54
 155        _cancellationTokenSource.Cancel();
 156    }
 57
 58    private void Schedule()
 59    {
 860        var startAt = _startAt;
 861        var adjusted = false;
 62
 63        while (true)
 64        {
 1365            var now = _systemClock.UtcNow;
 1366            var delay = startAt - now;
 67
 1368            if (!adjusted && delay <= TimeSpan.Zero)
 69            {
 570                adjusted = true;
 571                continue;
 72            }
 73
 874            SetupTimer(delay);
 75            break;
 76        }
 877    }
 78
 79    private void SetupTimer(TimeSpan delay)
 80    {
 81        // Handle edge cases where delay is zero or negative (e.g., due to clock drift, fast execution, or time alignmen
 82        // Instead of silently returning, use a minimum delay to ensure the timer fires and workflow continues schedulin
 883        if (delay <= TimeSpan.Zero)
 84        {
 585            _logger.LogWarning("Calculated delay is {Delay} which is not positive. Using minimum delay of 1ms to ensure 
 586            delay = TimeSpan.FromMilliseconds(1);
 87        }
 88
 889        _timer = new(delay.TotalMilliseconds)
 890        {
 891            Enabled = true
 892        };
 93
 894        _timer.Elapsed += async (_, _) =>
 895        {
 096            _timer?.Dispose();
 097            _timer = null;
 898
 899            // Check if disposed before proceeding
 0100            if (_disposed) return;
 8101
 0102            _startAt = _systemClock.UtcNow + _interval;
 8103
 0104            using var scope = _scopeFactory.CreateScope();
 0105            var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
 8106
 8107            // Check disposed again before accessing CancellationTokenSource
 0108            if (_disposed) return;
 8109
 0110            var cancellationToken = _cancellationTokenSource.Token;
 0111            if (!cancellationToken.IsCancellationRequested)
 8112            {
 0113                var acquired = false;
 8114                try
 8115                {
 0116                    acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
 0117                    if (!acquired) return;
 0118                    _executing = true;
 0119                    await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
 8120
 0121                    if (_cancellationRequested)
 8122                    {
 0123                        _cancellationRequested = false;
 0124                        _cancellationTokenSource.Cancel();
 8125                    }
 0126                }
 0127                catch (Exception e)
 8128                {
 0129                    _logger.LogError(e, "Error executing scheduled task");
 0130                }
 8131                finally
 8132                {
 0133                    _executing = false;
 0134                    if (acquired && !_disposed)
 0135                        _executionSemaphore.Release();
 8136                }
 8137            }
 8138
 0139            if (!cancellationToken.IsCancellationRequested && !_disposed)
 0140                Schedule();
 8141        };
 8142    }
 143
 144    void IDisposable.Dispose()
 145    {
 11146        _disposed = true;
 11147        _timer?.Dispose();
 11148        _cancellationTokenSource.Dispose();
 11149        _executionSemaphore.Dispose();
 11150    }
 151}