co89757
10/24/2017 - 5:38 PM

C# periodic action

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