You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Threading/ScheduledTask.cs

160 lines
4.5 KiB

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