view Implab/Components/RunnableComponent.cs @ 205:8200ab154c8a v2

Added ResetState to RunnableComponent to reset in case of failure Added StateChanged event to IRunnable Renamed Promise.SUCCESS -> Promise.Success Added Promise.FromException Renamed Bundle -> PromiseAll in PromiseExtensions
author cin
date Tue, 25 Oct 2016 17:40:33 +0300
parents 4d9830a9bbb8
children 7d07503621fe
line wrap: on
line source

using System;

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>
        public void Dispose() {
            IPromise pending;

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

            GC.SuppressFinalize(this);
            if (pending != null) {
                pending.Cancel();
                pending.Timeout(DisposeTimeout).On(
                    () => Dispose(true, null),
                    err => Dispose(true, err),
                    reason => Dispose(true, new OperationCanceledException("The operation is cancelled", reason))
                );
            } else {
                Dispose(true, null);
            }
        }

        ~RunnableComponent() {
            Dispose(false, null);
        }

        #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="lastError">The last error which occured during the component stop.</param>
        protected virtual void Dispose(bool disposing, Exception lastError) {

        }

    }
}