< Summary

Information
Class: Elsa.Scheduling.ScheduledTasks.ScheduledSpecificInstantTask
Assembly: Elsa.Scheduling
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs
Line coverage
95%
Covered lines: 71
Uncovered lines: 3
Coverable lines: 74
Total lines: 126
Line coverage: 95.9%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.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()100%44100%
Schedule()100%2294.33%
System.IDisposable.Dispose()50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.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 to execute at a specific instant.
 12/// </summary>
 13public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
 14{
 15    private readonly ITask _task;
 16    private readonly ISystemClock _systemClock;
 17    private readonly IServiceScopeFactory _scopeFactory;
 18    private readonly ILogger<ScheduledSpecificInstantTask> _logger;
 19    private readonly DateTimeOffset _startAt;
 20    private readonly CancellationTokenSource _cancellationTokenSource;
 2121    private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
 22    private Timer? _timer;
 23    private bool _executing;
 24    private bool _cancellationRequested;
 25    private bool _disposed;
 26
 27    /// <summary>
 28    /// Initializes a new instance of <see cref="ScheduledSpecificInstantTask"/>.
 29    /// </summary>
 2130    public ScheduledSpecificInstantTask(ITask task, DateTimeOffset startAt, ISystemClock systemClock, IServiceScopeFacto
 31    {
 2132        _task = task;
 2133        _systemClock = systemClock;
 2134        _scopeFactory = scopeFactory;
 2135        _logger = logger;
 2136        _startAt = startAt;
 2137        _cancellationTokenSource = new();
 38
 2139        Schedule();
 2140    }
 41
 42    /// <inheritdoc />
 43    public void Cancel()
 44    {
 1745        _timer?.Dispose();
 46
 1747        if (_executing)
 48        {
 1049            _cancellationRequested = true;
 1050            return;
 51        }
 52
 753        _cancellationTokenSource.Cancel();
 754    }
 55
 56    private void Schedule()
 57    {
 2158        var now = _systemClock.UtcNow;
 2159        var delay = _startAt - now;
 60
 61        // Handle edge cases where delay is zero or negative (e.g., due to clock drift, fast execution, or time alignmen
 62        // Instead of silently returning, use a minimum delay to ensure the timer fires and workflow continues schedulin
 2163        if (delay <= TimeSpan.Zero)
 64        {
 665            _logger.LogWarning("Calculated delay is {Delay} which is not positive. Using minimum delay of 1ms to ensure 
 666            delay = TimeSpan.FromMilliseconds(1);
 67        }
 68
 2169        _timer = new(delay.TotalMilliseconds)
 2170        {
 2171            Enabled = true
 2172        };
 73
 2174        _timer.Elapsed += async (_, _) =>
 2175        {
 1076            _timer?.Dispose();
 1077            _timer = null;
 2178
 2179            // Check if disposed before proceeding
 1080            if (_disposed) return;
 2181
 1082            using var scope = _scopeFactory.CreateScope();
 1083            var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
 2184
 2185            // Check disposed again before accessing CancellationTokenSource
 1086            if (_disposed) return;
 2187
 1088            var cancellationToken = _cancellationTokenSource.Token;
 1089            if (!cancellationToken.IsCancellationRequested)
 2190            {
 1091                var acquired = false;
 2192                try
 2193                {
 1094                    acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
 1095                    if (!acquired) return;
 1096                    _executing = true;
 1097                    await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
 2198
 1099                    if (_cancellationRequested)
 21100                    {
 10101                        _cancellationRequested = false;
 10102                        _cancellationTokenSource.Cancel();
 21103                    }
 10104                }
 0105                catch (Exception e)
 21106                {
 0107                    _logger.LogError(e, "Error executing scheduled task");
 0108                }
 21109                finally
 21110                {
 10111                    _executing = false;
 10112                    if (acquired && !_disposed)
 10113                        _executionSemaphore.Release();
 21114                }
 21115            }
 31116        };
 21117    }
 118
 119    void IDisposable.Dispose()
 120    {
 5121        _disposed = true;
 5122        _timer?.Dispose();
 5123        _cancellationTokenSource.Dispose();
 5124        _executionSemaphore.Dispose();
 5125    }
 126}