< 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
91%
Covered lines: 80
Uncovered lines: 7
Coverable lines: 87
Total lines: 151
Line coverage: 91.9%
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%2291.07%
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    {
 960        var startAt = _startAt;
 961        var adjusted = false;
 62
 63        while (true)
 64        {
 1465            var now = _systemClock.UtcNow;
 1466            var delay = startAt - now;
 67
 1468            if (!adjusted && delay <= TimeSpan.Zero)
 69            {
 570                adjusted = true;
 571                continue;
 72            }
 73
 974            SetupTimer(delay);
 75            break;
 76        }
 977    }
 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
 983        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
 989        _timer = new(delay.TotalMilliseconds)
 990        {
 991            Enabled = true
 992        };
 93
 994        _timer.Elapsed += async (_, _) =>
 995        {
 196            _timer?.Dispose();
 197            _timer = null;
 998
 999            // Check if disposed before proceeding
 1100            if (_disposed) return;
 9101
 1102            _startAt = _systemClock.UtcNow + _interval;
 9103
 1104            using var scope = _scopeFactory.CreateScope();
 1105            var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
 9106
 9107            // Check disposed again before accessing CancellationTokenSource
 1108            if (_disposed) return;
 9109
 1110            var cancellationToken = _cancellationTokenSource.Token;
 1111            if (!cancellationToken.IsCancellationRequested)
 9112            {
 1113                var acquired = false;
 9114                try
 9115                {
 1116                    acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
 1117                    if (!acquired) return;
 1118                    _executing = true;
 1119                    await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
 9120
 1121                    if (_cancellationRequested)
 9122                    {
 0123                        _cancellationRequested = false;
 0124                        _cancellationTokenSource.Cancel();
 9125                    }
 1126                }
 0127                catch (Exception e)
 9128                {
 0129                    _logger.LogError(e, "Error executing scheduled task");
 0130                }
 9131                finally
 9132                {
 1133                    _executing = false;
 1134                    if (acquired && !_disposed)
 1135                        _executionSemaphore.Release();
 9136                }
 9137            }
 9138
 1139            if (!cancellationToken.IsCancellationRequested && !_disposed)
 1140                Schedule();
 10141        };
 9142    }
 143
 144    void IDisposable.Dispose()
 145    {
 11146        _disposed = true;
 11147        _timer?.Dispose();
 11148        _cancellationTokenSource.Dispose();
 11149        _executionSemaphore.Dispose();
 11150    }
 151}