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;
}
}