< Summary

Information
Class: Elsa.Scheduling.ScheduledTasks.ScheduledCronTask
Assembly: Elsa.Scheduling
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledCronTask.cs
Line coverage
84%
Covered lines: 111
Uncovered lines: 20
Coverable lines: 131
Total lines: 202
Line coverage: 84.7%
Branch coverage
78%
Covered branches: 11
Total branches: 14
Branch coverage: 78.5%
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%
TrySetupTimer(...)100%2266.66%
SetupTimer(...)100%1183.14%
System.IDisposable.Dispose()75%44100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledCronTask.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 task that is scheduled using a given cron expression.
 12/// </summary>
 13public class ScheduledCronTask : IScheduledTask, IDisposable
 14{
 15    private readonly ISystemClock _systemClock;
 16    private readonly ILogger _logger;
 17    private Timer? _timer;
 18    private readonly ITask _task;
 19    private readonly string _cronExpression;
 20    private readonly ICronParser _cronParser;
 21    private readonly IServiceScopeFactory _scopeFactory;
 22    private readonly CancellationTokenSource _cancellationTokenSource;
 823    private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
 24    private bool _executing;
 25    private bool _cancellationRequested;
 26    private bool _disposed;
 27
 28    /// <summary>
 29    /// Initializes a new instance of <see cref="ScheduledCronTask"/>.
 30    /// </summary>
 831    public ScheduledCronTask(ITask task, string cronExpression, ICronParser cronParser, IServiceScopeFactory scopeFactor
 32    {
 833        _task = task;
 834        _cronExpression = cronExpression;
 835        _cronParser = cronParser;
 836        _scopeFactory = scopeFactory;
 837        _systemClock = systemClock;
 838        _logger = logger;
 839        _cancellationTokenSource = new();
 40
 841        Schedule();
 842    }
 43
 44    /// <inheritdoc />
 45    public void Cancel()
 46    {
 147        _timer?.Dispose();
 48
 149        if (_executing)
 50        {
 151            _cancellationRequested = true;
 152            return;
 53        }
 54
 055        _cancellationTokenSource.Cancel();
 056    }
 57
 58    private void Schedule()
 59    {
 860        var adjusted = false;
 61
 62        while (true)
 63        {
 1264            var now = _systemClock.UtcNow;
 1265            var nextOccurence = _cronParser.GetNextOccurrence(_cronExpression);
 1266            var delay = nextOccurence - now;
 67
 1268            if (!adjusted && delay <= TimeSpan.Zero)
 69            {
 470                adjusted = true;
 471                continue;
 72            }
 73
 874            TrySetupTimer(delay);
 75            break;
 76        }
 877    }
 78
 79    private void TrySetupTimer(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        {
 285            _logger.LogWarning("Calculated delay is {Delay} which is not positive. Using minimum delay of 1ms to ensure 
 286            delay = TimeSpan.FromMilliseconds(1);
 87        }
 88
 89        try
 90        {
 891            SetupTimer(delay);
 892        }
 093        catch (ArgumentException e)
 94        {
 095            _logger.LogError(e, "Failed to setup timer for scheduled task");
 096        }
 897    }
 98
 99    private void SetupTimer(TimeSpan delay)
 100    {
 8101        _timer = new(delay.TotalMilliseconds)
 8102        {
 8103            Enabled = true
 8104        };
 105
 8106        _timer.Elapsed += async (_, _) =>
 8107        {
 1108            _timer?.Dispose();
 1109            _timer = null;
 8110
 8111            // Early exit if disposed to prevent accessing disposed resources
 1112            if (_disposed)
 0113                return;
 8114
 1115            IServiceScope? scope = null;
 8116            try
 8117            {
 1118                scope = _scopeFactory.CreateScope();
 1119            }
 0120            catch (ObjectDisposedException)
 8121            {
 8122                // Service provider was disposed, exit gracefully
 0123                return;
 8124            }
 8125
 1126            using (scope)
 8127            {
 1128                var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
 1129                var cancellationToken = _cancellationTokenSource.Token;
 8130
 1131                if (!cancellationToken.IsCancellationRequested)
 8132                {
 1133                    var acquired = false;
 8134                    try
 8135                    {
 1136                        acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
 1137                        if (!acquired) return;
 8138
 1139                        _executing = true;
 1140                        await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
 8141
 1142                        if (_cancellationRequested)
 8143                        {
 1144                            _cancellationRequested = false;
 1145                            _cancellationTokenSource.Cancel();
 8146                        }
 1147                    }
 0148                    catch (ObjectDisposedException)
 8149                    {
 8150                        // Semaphore was disposed during execution, exit gracefully
 0151                        return;
 8152                    }
 0153                    catch (Exception e)
 8154                    {
 8155                        // Only log if not disposed to avoid logging after test context is disposed
 0156                        if (!_disposed)
 8157                        {
 8158                            try
 8159                            {
 0160                                _logger.LogError(e, "Error executing scheduled task");
 0161                            }
 0162                            catch
 8163                            {
 8164                                // Ignore logging errors (e.g., when test output is no longer available)
 0165                            }
 8166                        }
 0167                    }
 8168                    finally
 8169                    {
 1170                        _executing = false;
 1171                        if (acquired)
 8172                        {
 8173                            try
 8174                            {
 1175                                _executionSemaphore.Release();
 1176                            }
 0177                            catch (ObjectDisposedException)
 8178                            {
 8179                                // Semaphore was disposed, ignore
 0180                            }
 8181                        }
 8182                    }
 8183                }
 8184
 8185                // Check again if disposed before scheduling next execution
 1186                if (!cancellationToken.IsCancellationRequested && !_disposed)
 0187                    Schedule();
 1188            }
 9189        };
 8190    }
 191
 192    void IDisposable.Dispose()
 193    {
 8194        if (_disposed)
 2195            return;
 196
 6197        _disposed = true;
 6198        _timer?.Dispose();
 6199        _cancellationTokenSource.Dispose();
 6200        _executionSemaphore.Dispose();
 6201    }
 202}