view Implab/Components/PollingComponent.cs @ 238:bdfdba6b645b v2

fixed unpredictable Safe.Dispose behaviour
author cin
date Fri, 01 Dec 2017 01:28:56 +0300
parents 7d07503621fe
children
line wrap: on
line source

using System;
using System.Threading;
using Implab.Diagnostics;

namespace Implab.Components {
    public class PollingComponent : RunnableComponent {
        readonly Timer m_timer;
        readonly Func<Func<ICancellationToken, IPromise>, IPromise> m_dispatcher;
        readonly TimeSpan m_interval;

        readonly object m_lock = new object();

        ActionTask m_pending;

        protected PollingComponent(TimeSpan interval, Func<Func<ICancellationToken, IPromise>, IPromise> dispatcher, bool initialized) : base(initialized) {
            m_timer = new Timer(OnInternalTick);

            m_interval = interval;
            m_dispatcher = dispatcher;
        }

        protected override IPromise OnStart() {
            m_timer.Change(TimeSpan.Zero, m_interval);

            return base.OnStart();
        }

        void OnInternalTick(object state) {
            if (StartTick()) {
                try {
                    if (m_dispatcher != null) {
                        var result = m_dispatcher(OnTick);
                        m_pending.CancellationRequested(result.Cancel);
                        AwaitTick(result);
                    } else {
                        AwaitTick(OnTick(m_pending));
                    }
                } catch (Exception error) {
                    HandleTickError(error);
                }
            }   
        }

        /// <summary>
        /// Checks wheather there is no running handler in the component and marks that the handler is starting.
        /// </summary>
        /// <returns>boolean value, true - the new tick handler may be invoked, false - a tick handler is already running or a component isn't running.</returns>
        /// <remarks>
        /// If the component is stopping no new handlers can be run. Every successful call to this method must be completed with either AwaitTick or HandleTickError handlers.
        /// </remarks>
        protected virtual bool StartTick() {
            lock (m_lock) {
                if (State != ExecutionState.Running || m_pending != null)
                    return false;
                // actually the component may be in a not running state here (stopping, disposed or what ever),
                // but OnStop method also locks on the same object and will handle operation in m_pending
                m_pending = new ActionTask(
                    () => {
                        // only one operation is running, it's safe to assing m_pending from it
                        m_pending = null;
                    },
                    ex => {
                        try {
                            OnTickError(ex);
                            // Analysis disable once EmptyGeneralCatchClause
                        } catch {
                        } finally {
                            m_pending = null;
                        }
                        // suppress error
                    },
                    ex => {
                        try {
                            OnTickCancel(ex);
                            // Analysis disable once EmptyGeneralCatchClause
                        } catch {
                        } finally {
                            m_pending = null;
                        }
                        // supress cancellation
                    },
                    false
                );
                return true;
            }
        }

        /// <summary>
        /// Awaits the tick.
        /// </summary>
        /// <param name="tick">Tick.</param>
        /// <remarks>
        /// This method is called only after StartTick method and m_pending will hold the promise which should be fulfilled.
        /// </remarks>
        void AwaitTick(IPromise tick) {
            if (tick == null) {
                m_pending.Resolve();
            } else {
                tick.On(
                    m_pending.Resolve,
                    m_pending.Reject,
                    m_pending.CancelOperation
                );
            }
        }

        /// <summary>
        /// Handles the tick error.
        /// </summary>
        /// <remarks>
        /// This method is called only after StartTick method and m_pending will hold the promise which should be fulfilled.
        /// </remarks>
        void HandleTickError(Exception error) {
            m_pending.Reject(error);
        }

        protected virtual void OnTickError(Exception error) {
        }

        protected virtual void OnTickCancel(Exception error) {
        }

        /// <summary>
        /// Invoked when the timer ticks, use this method to implement your logic
        /// </summary>
        protected virtual IPromise OnTick(ICancellationToken cancellationToken) {
            return Promise.Success;
        }

        protected override IPromise OnStop() {
            m_timer.Change(-1, -1);

            // the component is in the stopping state
            lock (m_lock) {
                // after this lock no more pending operations could be created
                var pending = m_pending;
                // m_pending could be fulfilled and set to null already
                if (pending != null) {
                    pending.Cancel();
                    return pending.Then(base.OnStop);
                }
            }

            return base.OnStop();
        }

        protected override void Dispose(bool disposing) {
            if (disposing)
                m_timer.Dispose();
            
            base.Dispose(disposing);
        }
    }
}