|
|
|
|
namespace Connected.Threading;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents asynchronous task which invokes action on predefined interval.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// 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.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <example>
|
|
|
|
|
/// If queue task must complete in 30 seconds before becomes visible again, <see cref="ScheduledTask"/>
|
|
|
|
|
/// would be useful to act as a guard and prevent queue message to become visible before the processing
|
|
|
|
|
/// is completed. We would set the <see cref="Timeout"/> to 25 seconds and call ping which will give
|
|
|
|
|
/// us another 30 seconds to complete.
|
|
|
|
|
/// </example>
|
|
|
|
|
public sealed class ScheduledTask : IDisposable
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new instance of a <see cref="ScheduledTask"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="scheduledAction">The action to be called when timeout occurs.</param>
|
|
|
|
|
/// <param name="expiredAction">The action to be called when task exceeds the <see cref="Lifetime"/> execution.</param>
|
|
|
|
|
/// <param name="timeout">The timeout before the scheduledAction is called. </param>
|
|
|
|
|
/// <param name="lifetime">The total time that is allowed this task to be run. </param>
|
|
|
|
|
/// <param name="cancel">The cancellation token to cancel the task.</param>
|
|
|
|
|
public ScheduledTask(Func<Task> scheduledAction, Func<Task> expiredAction, TimeSpan timeout, TimeSpan lifetime, CancellationToken cancel)
|
|
|
|
|
{
|
|
|
|
|
ScheduledAction = scheduledAction;
|
|
|
|
|
ExpiredAction = expiredAction;
|
|
|
|
|
Timeout = timeout;
|
|
|
|
|
Lifetime = lifetime;
|
|
|
|
|
CancelSource = new CancellationTokenSource();
|
|
|
|
|
|
|
|
|
|
cancel.Register(CancelSource.Cancel);
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The internal cancellation source which is passed in the task to enable cancellation.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private CancellationTokenSource CancelSource { get; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The action or callback which is called when timeout occurs.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Func<Task> ScheduledAction { get; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This action is called if the <see cref="Task"/> executes past <see cref="Lifetime"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Func<Task> ExpiredAction { get; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The actual Task which performs the logic.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private Task? Task { get; set; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The timeout before the <see cref="ScheduledAction"/> is called.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private TimeSpan Timeout { get; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The total number that is allowe the task to be run.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public TimeSpan Lifetime { get; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets value which determines if the <see cref="Task"/> is
|
|
|
|
|
/// currently running or not.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private bool IsRunning { get; set; }
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts the <see cref="ScheduledTask"/> if it is not already running.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stops the execution loop.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Stop()
|
|
|
|
|
{
|
|
|
|
|
if (CancelSource.IsCancellationRequested)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* This will immeadiatelly stop the Task.
|
|
|
|
|
*/
|
|
|
|
|
CancelSource.Cancel();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
IsRunning = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Disposes the object by stopping the task if it is running.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Stop();
|
|
|
|
|
CancelSource.Dispose();
|
|
|
|
|
|
|
|
|
|
Task?.Dispose();
|
|
|
|
|
Task = null;
|
|
|
|
|
}
|
|
|
|
|
}
|