Created
October 24, 2017 17:38
-
-
Save co89757/2dd68ecc55bb39837add07ef8639ee57 to your computer and use it in GitHub Desktop.
C# periodic action
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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