namespace Connected.Threading; /// /// Represents asynchronous task which invokes action on predefined interval. /// /// /// This class is useful when performing tasks which must be completed in specified time but /// supports pinging or similar techniques which enable tasks that do not complete in time to /// prolong their execution. /// /// /// If queue task must complete in 30 seconds before becomes visible again, /// would be useful to act as a guard and prevent queue message to become visible before the processing /// is completed. We would set the to 25 seconds and call ping which will give /// us another 30 seconds to complete. /// public sealed class ScheduledTask : IDisposable { /// /// Creates a new instance of a . /// /// The action to be called when timeout occurs. /// The action to be called when task exceeds the execution. /// The timeout before the scheduledAction is called. /// The total time that is allowed this task to be run. /// The cancellation token to cancel the task. public ScheduledTask(Func scheduledAction, Func expiredAction, TimeSpan timeout, TimeSpan lifetime, CancellationToken cancel) { ScheduledAction = scheduledAction; ExpiredAction = expiredAction; Timeout = timeout; Lifetime = lifetime; CancelSource = new CancellationTokenSource(); cancel.Register(CancelSource.Cancel); } /// /// The internal cancellation source which is passed in the task to enable cancellation. /// private CancellationTokenSource CancelSource { get; } /// /// The action or callback which is called when timeout occurs. /// private Func ScheduledAction { get; } /// /// This action is called if the executes past . /// public Func ExpiredAction { get; } /// /// The actual Task which performs the logic. /// private Task? Task { get; set; } /// /// The timeout before the is called. /// private TimeSpan Timeout { get; } /// /// The total number that is allowe the task to be run. /// public TimeSpan Lifetime { get; } /// /// Gets or sets value which determines if the is /// currently running or not. /// private bool IsRunning { get; set; } /// /// Starts the if it is not already running. /// public void Start() { if (IsRunning) return; /* * Create new async task. */ Task = Task.Run(async () => { var start = DateTime.UtcNow; try { /* * We are calling the ScheduledAction repeatedly until the: * a) cancellation is requested * b) stop is called which sets IsRunning property to false */ while (!CancelSource.IsCancellationRequested || IsRunning) { /* * If Dispose has been called in the meantime Task would be null here */ if (Task is not null) { /* * First wait for the duration of the Timeout before triggering callback */ await Task.Delay(Timeout, CancelSource.Token).ConfigureAwait(false); /* * If the lifetime exceedes stop the task and call ExpiredAction instead on ScheduledAction. */ if (DateTime.UtcNow.Subtract(start) > Lifetime) { Stop(); await ExpiredAction(); return; } /* * Once the Timeout elapsed invoke the callback */ await ScheduledAction().ConfigureAwait(false); } } } finally { IsRunning = false; } }, CancelSource.Token); IsRunning = true; } /// /// Stops the execution loop. /// public void Stop() { if (CancelSource.IsCancellationRequested) return; try { /* * This will immeadiatelly stop the Task. */ CancelSource.Cancel(); } catch (OperationCanceledException) { } finally { IsRunning = false; } } /// /// Disposes the object by stopping the task if it is running. /// public void Dispose() { Stop(); CancelSource.Dispose(); Task?.Dispose(); Task = null; } }