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 { public static readonly ExecutionState[,] ReusableTransitions; public static readonly ExecutionState[,] NonreusableTransitions; class StateBuilder { readonly ExecutionState[,] m_states; public ExecutionState[,] States { get { return m_states; } } public StateBuilder(ExecutionState[,] states) { m_states = states; } public StateBuilder() { m_states = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1]; } public StateBuilder Edge(ExecutionState s1, ExecutionState s2, Commands cmd) { m_states[(int)s1, (int)cmd] = s2; return this; } public StateBuilder Clone() { return new StateBuilder((ExecutionState[,])m_states.Clone()); } } static StateMachine() { ReusableTransitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1]; var common = new StateBuilder() .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.Failed, ExecutionState.Disposed, Commands.Dispose) .Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset) .Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail) .Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose) .Edge(ExecutionState.Disposed, ExecutionState.Disposed, Commands.Dispose); var reusable = common .Clone() .Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok); var nonreusable = common .Clone() .Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Ok); NonreusableTransitions = nonreusable.States; ReusableTransitions = reusable.States; } readonly ExecutionState[,] m_states; public ExecutionState State { get; private set; } public StateMachine(ExecutionState[,] states, ExecutionState initial) { State = initial; m_states = states; } public bool Move(Commands cmd) { var next = m_states[(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 StateChanged; /// /// Initializes component state. /// /// If set, the component initial state is and the component is ready to start, otherwise initialization is required. /// If set, the component may start after it has been stopped, otherwise the component is disposed after being stopped. protected RunnableComponent(bool initialized, bool reusable) { m_stateMachine = new StateMachine( reusable ? StateMachine.ReusableTransitions : StateMachine.NonreusableTransitions, initialized ? ExecutionState.Ready : ExecutionState.Created ); m_reusable = reusable; } /// /// Initializes component state. The component created with this constructor is not reusable, i.e. it will be disposed after stop. /// /// If set, the component initial state is and the component is ready to start, otherwise initialization is required. protected RunnableComponent(bool initialized) : this(initialized, false) { } 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; } /// /// Handles the state of the component change event, raises the event, handles /// the transition to the state (calls method). /// /// The previous state /// The current state /// The last error if any. /// /// /// If the previous state and the current state are same this method isn't called, such situiation is treated /// as the component hasn't changed it's state. /// /// /// When overriding this method ensure the call is made to the base implementation, otherwise it will lead to /// the wrong behavior of the component. /// /// protected virtual void OnStateChanged(ExecutionState previous, ExecutionState current, Exception error) { StateChanged.DispatchEvent( this, new StateChangeEventArgs { State = current, LastError = error } ); if (current == ExecutionState.Disposed) { GC.SuppressFinalize(this); Dispose(true); } } /// /// Moves the component from running to failed state. /// /// The exception which is describing the error. protected bool Fail(Exception error) { return MoveIfInState(Commands.Fail, null, error, ExecutionState.Running); } /// /// Tries to reset state to . /// /// True if component is reset to , false if the componet wasn't /// in state. /// /// This method checks the current state of the component and if it's in /// moves component to . /// The is called and if this method completes succesfully the component moved /// to state, otherwise the component is moved to /// state. If throws an exception it will be propagated by this method to the caller. /// 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; } } /// /// This method is called by to reinitialize component in the failed state. /// /// /// Default implementation throws which will cause the component /// fail to reset it's state and it left in state. /// If this method doesn't throw exceptions the component is moved to state. /// protected virtual void OnResetState() { throw new NotImplementedException(); } IPromise InvokeAsync(Commands cmd, Func action, Action chain) { IPromise promise = null; IPromise prev; var task = new ActionChainTask(action, null, null, true); Action 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() { return InvokeAsync(Commands.Stop, OnStop, StopPending); } protected virtual IPromise OnStop() { return Promise.Success; } /// /// Stops the current operation if one exists. /// /// Current. /// Stop. protected virtual void StopPending(IPromise current, IResolvable 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 /// /// Releases all resource used by the object. /// /// /// Will not try to stop the component, it will just release all resources. /// To cleanup the component gracefully use method. /// /// In normal cases the method shouldn't be called, the call to the /// method is sufficient to cleanup the component. Call only to cleanup after errors, /// especially if method is failed. Using this method insted of may /// lead to the data loss by the component. /// [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dipose(bool) and GC.SuppessFinalize are called")] public void Dispose() { Move(Commands.Dispose, null, null); } ~RunnableComponent() { Dispose(false); } #endregion /// /// Releases all resources used by the component, called automatically, override this method to implement your cleanup. /// /// true if this method is called during normal dispose process. /// The operation which is currenty pending protected virtual void Dispose(bool disposing) { } } }