Skip to content

Instantly share code, notes, and snippets.

@co89757
Created October 24, 2017 17:38
Show Gist options
  • Save co89757/2dd68ecc55bb39837add07ef8639ee57 to your computer and use it in GitHub Desktop.
Save co89757/2dd68ecc55bb39837add07ef8639ee57 to your computer and use it in GitHub Desktop.
C# periodic action
using System;
using System.Threading;
using Utility;
/// <summary>
/// Encapsulates a mechanism for executing an action continuously with a specified interval.
/// </summary>
/// <remarks>
/// Large portions copied from a class of the same name made by Markus.
/// </remarks>
public sealed class PeriodicAction : IDisposable
{
private static Logger log = new Logger("PeriodicAction");
private static int nextGlobalID;
private string id;
private TimeSpan initialDelay;
private Func<TimeSpan> delayBetweenRuns;
private Action<CancellationToken> action;
private ManualResetEventSlim terminationEvent;
private CancellationTokenSource terminationTokenSource;
private Thread executionThread;
/// <summary>
/// Initializes a new instance of the <see cref="PeriodicAction"/>
/// class with a consistent <see cref="TimeSpan"/> between calls.
/// </summary>
public PeriodicAction(
string description,
TimeSpan initialDelay,
TimeSpan delayBetweenRuns,
Action<CancellationToken> action)
: this(description, initialDelay, () => delayBetweenRuns, action)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PeriodicAction"/>
/// class with a method specifying the <see cref="TimeSpan"/> between calls.
/// </summary>
public PeriodicAction(
string description,
TimeSpan initialDelay,
Func<TimeSpan> delayBetweenRuns,
Action<CancellationToken> action)
{
if (description == null)
{
throw new ArgumentNullException(nameof(description));
}
if (initialDelay < TimeSpan.Zero)
{
throw new ArgumentException(nameof(initialDelay));
}
if (delayBetweenRuns == null)
{
throw new ArgumentNullException(nameof(delayBetweenRuns));
}
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
this.id = Interlocked.Increment(ref nextGlobalID) + "/" + description;
log.Debug($"#{this.id} begin starting new periodic action");
this.initialDelay = initialDelay;
this.delayBetweenRuns = delayBetweenRuns;
this.action = action;
// this allows us to notify the action that it should stop
this.terminationTokenSource = new CancellationTokenSource();
// this allows us to wake up the sleeping background thread
this.terminationEvent = new ManualResetEventSlim();
// use a dedicated thread with higher than normal priority
// because at the moment periodic background actions are used
// to renew azure leases and they are much more important
// to run on time than the IGS jobs.
// with using Tasks, we observed starvation leading to lost leases
this.executionThread = new Thread(() => Run(
this.id,
this.initialDelay,
this.delayBetweenRuns,
this.action,
this.terminationEvent,
this.terminationTokenSource.Token));
this.executionThread.Name = this.id;
this.executionThread.Priority = ThreadPriority.AboveNormal;
this.executionThread.Start();
log.Debug($"#{this.id} end starting new periodic action");
}
public void Dispose()
{
// ask periodic task/action to stop
log.Debug($"#{this.id} dispose: asking operation and thread to stop");
this.terminationTokenSource.Cancel();
this.terminationEvent.Set();
// block until it's stopped
log.Debug($"#{this.id} dispose: begin waiting for thread termination");
try
{
this.executionThread.Join();
}
catch (Exception e)
{
log.Error($"#{this.id} dispose: failed waiting for thread termination with {e.ToString()}");
throw;
}
this.terminationTokenSource.Dispose();
this.terminationTokenSource = null;
this.terminationEvent.Dispose();
this.terminationEvent = null;
log.Debug($"#{this.id} dispose: end waiting for thread termination");
}
private static void Run(
string id,
TimeSpan initialDelay,
Func<TimeSpan> delayBetweenRuns,
Action<CancellationToken> action,
ManualResetEventSlim terminationEvent,
CancellationToken terminationToken)
{
try
{
// initial wait
log.Debug($"#{id} initial wait {initialDelay}");
terminationEvent.Wait(initialDelay);
if (terminationToken.IsCancellationRequested)
{
log.Debug($"#{id} thread was canceled during initial delay, exiting normally");
return;
}
// loop
while (true)
{
// execute action and pass it a cancellation token
// so it knows when to cooperatively terminate
log.Debug($"#{id} executing action");
try
{
action(terminationToken);
}
catch (OperationCanceledException e)
{
if (e.CancellationToken == terminationToken)
{
// the correct cancellation token was thrown by the action
log.Debug($"#{id} action was canceled, exiting normally");
return;
}
else
{
// cancellation thrown from incorrect token
log.Error($"#{id} action threw OperationCanceledException with wrong token");
// do not terminate the execution thread. It will run again in the future
}
}
catch (Exception e)
{
// action threw an exception, but it wasn't canceled. log, sleep, try again later
log.Error($"#{id} action temporarily failed with {e.ToString()}");
// do not terminate the execution thread. It will run again in the future
}
// subsequent wait
var delayTime = TimeSpan.FromMinutes(1); // default
try
{
delayTime = delayBetweenRuns();
}
catch (Exception e)
{
log.Error($"#{id} error getting subsequent delay interval {e.ToString()}");
}
log.Debug($"#{id} subsequent wait {delayTime}");
terminationEvent.Wait(delayTime);
if (terminationToken.IsCancellationRequested)
{
log.Debug($"#{id} thread was canceled during subsequent delay, exiting normally");
return;
}
// loop again
}
}
catch (Exception e)
{
// this could be a ThreadAbortException in the action that got handled and re-thrown
log.Error($"#{id} thread failed for unknown reason, aborting with {e.ToString()}");
throw;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment