view Implab/Components/RunnableComponent.cs @ 209:a867536c68fc v2

Bound promise to CancellationToken Added new states to ExecutionSate enum. Added Safe.Guard() method to handle cleanup of the result of the promise
author cin
date Wed, 16 Nov 2016 03:06:08 +0300
parents 7d07503621fe
children 5dc21f6a3222
line wrap: on
line source

using System;
using System.Diagnostics.CodeAnalysis;

namespace Implab.Components {
    public abstract class RunnableComponent : IDisposable, IRunnable, IInitializable {
        enum Commands {
            Ok = 0,
            Fail,
            Init,
            Start,
            Stop,
            Dispose,
            Reset,
            Last = Reset
        }

        class StateMachine {
            static readonly ExecutionState[,] _transitions;

            static StateMachine() {
                _transitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];

                Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init);
                Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose);

                Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok);
                Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail);

                Edge(ExecutionState.Ready, ExecutionState.Starting, Commands.Start);
                Edge(ExecutionState.Ready, ExecutionState.Disposed, Commands.Dispose);

                Edge(ExecutionState.Starting, ExecutionState.Running, Commands.Ok);
                Edge(ExecutionState.Starting, ExecutionState.Failed, Commands.Fail);
                Edge(ExecutionState.Starting, ExecutionState.Stopping, Commands.Stop);
                Edge(ExecutionState.Starting, ExecutionState.Disposed, Commands.Dispose);

                Edge(ExecutionState.Running, ExecutionState.Failed, Commands.Fail);
                Edge(ExecutionState.Running, ExecutionState.Stopping, Commands.Stop);
                Edge(ExecutionState.Running, ExecutionState.Disposed, Commands.Dispose);

                Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail);
                Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok);
                Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose);

                Edge(ExecutionState.Failed, ExecutionState.Disposed, Commands.Dispose);
                Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset);
            }

            static void Edge(ExecutionState s1, ExecutionState s2, Commands cmd) {
                _transitions[(int)s1, (int)cmd] = s2;
            }

            public ExecutionState State {
                get;
                private set;
            }

            public StateMachine(ExecutionState initial) {
                State = initial;
            }

            public bool Move(Commands cmd) {
                var next = _transitions[(int)State, (int)cmd];
                if (next == ExecutionState.Undefined)
                    return false;
                State = next;
                return true;
            }
        }

        IPromise m_pending;
        Exception m_lastError;

        readonly StateMachine m_stateMachine;
        readonly bool m_reusable;
        public event EventHandler<StateChangeEventArgs> StateChanged;

        /// <summary>
        /// Initializes component state. 
        /// </summary>
        /// <param name="initialized">If set, the component initial state is <see cref="ExecutionState.Ready"/> and the component is ready to start, otherwise initialization is required.</param>
        /// <param name="reusable">If set, the component may start after it has been stopped, otherwise the component is disposed after being stopped.</param>
        protected RunnableComponent(bool initialized, bool reusable) {
            m_stateMachine = new StateMachine(initialized ? ExecutionState.Ready : ExecutionState.Created);
            m_reusable = reusable;
            DisposeTimeout = 10000;
        }

        /// <summary>
        /// Initializes component state. The component created with this constructor is not reusable, i.e. it will be disposed after stop.
        /// </summary>
        /// <param name="initialized">If set, the component initial state is <see cref="ExecutionState.Ready"/> and the component is ready to start, otherwise initialization is required.</param>
        protected RunnableComponent(bool initialized) : this(initialized, false) {
        }

        /// <summary>
        /// Gets or sets the timeout to wait for the pending operation to complete. If the pending operation doesn't finish than the component will be disposed anyway.
        /// </summary>
        protected int DisposeTimeout {
            get;
            set;
        }

        void ThrowInvalidCommand(Commands cmd) {
            if (m_stateMachine.State == ExecutionState.Disposed)
                throw new ObjectDisposedException(ToString());

            throw new InvalidOperationException(String.Format("Command {0} is not allowed in the state {1}", cmd, m_stateMachine.State));
        }

        bool MoveIfInState(Commands cmd, IPromise pending, Exception error, ExecutionState state) {
            ExecutionState prev, current;
            lock (m_stateMachine) {
                if (m_stateMachine.State != state)
                    return false;

                prev = m_stateMachine.State;
                if (!m_stateMachine.Move(cmd))
                    ThrowInvalidCommand(cmd);
                current = m_stateMachine.State;

                m_pending = pending;
                m_lastError = error;
            }
            if (prev != current)
                OnStateChanged(prev, current, error);
            return true;
        }

        bool MoveIfPending(Commands cmd, IPromise pending, Exception error, IPromise expected) {
            ExecutionState prev, current;
            lock (m_stateMachine) {
                if (m_pending != expected)
                    return false;
                prev = m_stateMachine.State;
                if (!m_stateMachine.Move(cmd))
                    ThrowInvalidCommand(cmd);
                current = m_stateMachine.State;
                m_pending = pending;
                m_lastError = error;
            }
            if (prev != current)
                OnStateChanged(prev, current, error);
            return true;
        }

        IPromise Move(Commands cmd, IPromise pending, Exception error) {
            ExecutionState prev, current;
            IPromise ret;
            lock (m_stateMachine) {
                prev = m_stateMachine.State;
                if (!m_stateMachine.Move(cmd))
                    ThrowInvalidCommand(cmd);
                current = m_stateMachine.State;

                ret = m_pending;
                m_pending = pending;
                m_lastError = error;
                
            }
            if(prev != current)
                OnStateChanged(prev, current, error);
            return ret;
        }

        protected virtual void OnStateChanged(ExecutionState previous, ExecutionState current, Exception error) {
            var h = StateChanged;
            if (h != null)
                h(this, new StateChangeEventArgs {
                    State = current,
                    LastError = error
                });
        }

        /// <summary>
        /// Moves the component from running to failed state.
        /// </summary>
        /// <param name="error">The exception which is describing the error.</param>
        protected bool Fail(Exception error) {
            return MoveIfInState(Commands.Fail, null, error, ExecutionState.Running);
        }

        /// <summary>
        /// Tries to reset <see cref="ExecutionState.Failed"/> state to <see cref="ExecutionState.Ready"/>.
        /// </summary>
        /// <returns>True if component is reset to <see cref="ExecutionState.Ready"/>, false if the componet wasn't
        /// in <see cref="ExecutionState.Failed"/> state.</returns>
        /// <remarks>
        /// This method checks the current state of the component and if it's in <see cref="ExecutionState.Failed"/>
        /// moves component to <see cref="ExecutionState.Initializing"/>.
        /// The <see cref="OnResetState()"/> is called and if this method completes succesfully the component moved
        /// to <see cref="ExecutionState.Ready"/> state, otherwise the component is moved to <see cref="ExecutionState.Failed"/>
        /// state. If <see cref="OnResetState()"/> throws an exception it will be propagated by this method to the caller.
        /// </remarks>
        protected bool ResetState() {
            if (!MoveIfInState(Commands.Reset, null, null, ExecutionState.Failed))
                return false;

            try {
                OnResetState();
                Move(Commands.Ok, null, null);
                return true;
            } catch (Exception err) {
                Move(Commands.Fail, null, err);
                throw;
            }
        }

        /// <summary>
        /// This method is called by <see cref="ResetState"/> to reinitialize component in the failed state.
        /// </summary>
        /// <remarks>
        /// Default implementation throws <see cref="NotImplementedException"/> which will cause the component
        /// fail to reset it's state and it left in <see cref="ExecutionState.Failed"/> state.
        /// If this method doesn't throw exceptions the component is moved to <see cref="ExecutionState.Ready"/> state.
        /// </remarks>
        protected virtual void OnResetState() {
            throw new NotImplementedException();
        }

        IPromise InvokeAsync(Commands cmd, Func<IPromise> action, Action<IPromise, IDeferred> chain) {
            IPromise promise = null;
            IPromise prev;

            var task = new ActionChainTask(action, null, null, true);

            Action<Exception> errorOrCancel = e => {
                if (e == null)
                    e = new OperationCanceledException();
                MoveIfPending(Commands.Fail, null, e, promise);
                throw new PromiseTransientException(e);
            };

            promise = task.Then(
                () => MoveIfPending(Commands.Ok, null, null, promise),
                errorOrCancel,
                errorOrCancel
            );

            prev = Move(cmd, promise, null);

            if (prev == null)
                task.Resolve();
            else
                chain(prev, task);

            return promise;
        }


        #region IInitializable implementation

        public void Initialize() {
            Move(Commands.Init, null, null);

            try {
                OnInitialize();
                Move(Commands.Ok, null, null);
            } catch (Exception err) {
                Move(Commands.Fail, null, err);
                throw;
            }
        }

        protected virtual void OnInitialize() {
        }

        #endregion

        #region IRunnable implementation

        public IPromise Start() {
            return InvokeAsync(Commands.Start, OnStart, null);
        }

        protected virtual IPromise OnStart() {
            return Promise.Success;
        }

        public IPromise Stop() {
            var pending = InvokeAsync(Commands.Stop, OnStop, StopPending);
            return m_reusable ? pending : pending.Then(Dispose);
        }

        protected virtual IPromise OnStop() {
            return Promise.Success;
        }

        /// <summary>
        /// Stops the current operation if one exists.
        /// </summary>
        /// <param name="current">Current.</param>
        /// <param name="stop">Stop.</param>
        protected virtual void StopPending(IPromise current, IDeferred stop) {
            if (current == null) {
                stop.Resolve();
            } else {
                // связваем текущую операцию с операцией остановки
                current.On(
                    stop.Resolve, // если текущая операция заверщилась, то можно начинать остановку
                    stop.Reject, // если текущая операция дала ошибку - то все плохо, нельзя продолжать
                    e => stop.Resolve() // если текущая отменилась, то можно начинать остановку
                );
                // посылаем текущей операции сигнал остановки
                current.Cancel();
            }
        }

        public ExecutionState State {
            get {
                return m_stateMachine.State;
            }
        }

        public Exception LastError {
            get {
                return m_lastError;
            }
        }

        #endregion

        #region IDisposable implementation

        /// <summary>
        /// Releases all resource used by the <see cref="Implab.Components.RunnableComponent"/> object.
        /// </summary>
        /// <remarks>
        /// <para>Will not try to stop the component, it will just release all resources.
        /// To cleanup the component gracefully use <see cref="Stop()"/> method.</para>
        /// <para>
        /// In normal cases the <see cref="Dispose()"/> method shouldn't be called, the call to the <see cref="Stop()"/>
        /// method is sufficient to cleanup the component. Call <see cref="Dispose()"/> only to cleanup after errors,
        /// especially if <see cref="Stop"/> method is failed. Using this method insted of <see cref="Stop()"/> may
        /// lead to the data loss by the component.
        /// </para></remarks>
        [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dipose(bool) and GC.SuppessFinalize are called")]
        public void Dispose() {
            IPromise pending;

            lock (m_stateMachine) {
                if (m_stateMachine.State == ExecutionState.Disposed)
                    return;
                Move(Commands.Dispose, null, null);
            }

            GC.SuppressFinalize(this);
            Dispose(true);
        }

        ~RunnableComponent() {
            Dispose(false);
        }

        #endregion

        /// <summary>
        /// Releases all resources used by the component, called automatically, override this method to implement your cleanup.
        /// </summary>
        /// <param name="disposing">true if this method is called during normal dispose process.</param>
        /// <param name="pending">The operation which is currenty pending</param>
        protected virtual void Dispose(bool disposing) {

        }

    }
}