RunnableComponent.cs
411 lines
| 16.3 KiB
| text/x-csharp
|
CSharpLexer
|
|
r156 | using System; | ||
|
|
r208 | using System.Diagnostics.CodeAnalysis; | ||
|
|
r156 | namespace Implab.Components { | ||
|
|
r185 | public abstract class RunnableComponent : IDisposable, IRunnable, IInitializable { | ||
|
|
r184 | enum Commands { | ||
| Ok = 0, | ||||
| Fail, | ||||
| Init, | ||||
| Start, | ||||
| Stop, | ||||
| Dispose, | ||||
|
|
r205 | Reset, | ||
| Last = Reset | ||||
|
|
r184 | } | ||
| class StateMachine { | ||||
|
|
r210 | 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()); | ||||
| } | ||||
| } | ||||
|
|
r184 | |||
| static StateMachine() { | ||||
|
|
r210 | ReusableTransitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1]; | ||
|
|
r184 | |||
|
|
r210 | var common = new StateBuilder() | ||
| .Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init) | ||||
| .Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose) | ||||
|
|
r185 | |||
|
|
r210 | .Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok) | ||
| .Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail) | ||||
|
|
r184 | |||
|
|
r210 | .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) | ||||
|
|
r184 | |||
|
|
r210 | .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); | ||||
|
|
r184 | |||
|
|
r210 | 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; | ||||
|
|
r184 | |||
| } | ||||
|
|
r210 | readonly ExecutionState[,] m_states; | ||
|
|
r184 | |||
| public ExecutionState State { | ||||
| get; | ||||
| private set; | ||||
| } | ||||
|
|
r210 | public StateMachine(ExecutionState[,] states, ExecutionState initial) { | ||
|
|
r184 | State = initial; | ||
|
|
r210 | m_states = states; | ||
|
|
r184 | } | ||
| public bool Move(Commands cmd) { | ||||
|
|
r210 | var next = m_states[(int)State, (int)cmd]; | ||
|
|
r184 | if (next == ExecutionState.Undefined) | ||
| return false; | ||||
| State = next; | ||||
| return true; | ||||
| } | ||||
| } | ||||
| IPromise m_pending; | ||||
| Exception m_lastError; | ||||
|
|
r205 | 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) { | ||||
|
|
r210 | m_stateMachine = new StateMachine( | ||
| reusable ? StateMachine.ReusableTransitions : StateMachine.NonreusableTransitions, | ||||
| initialized ? ExecutionState.Ready : ExecutionState.Created | ||||
| ); | ||||
|
|
r205 | m_reusable = reusable; | ||
| } | ||||
| /// <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) { | ||||
|
|
r184 | } | ||
| void ThrowInvalidCommand(Commands cmd) { | ||||
|
|
r185 | if (m_stateMachine.State == ExecutionState.Disposed) | ||
|
|
r205 | 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; | ||||
|
|
r184 | } | ||
|
|
r205 | 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; | ||||
|
|
r210 | m_lastError = error; | ||
|
|
r205 | } | ||
|
|
r210 | if (prev != current) | ||
|
|
r205 | OnStateChanged(prev, current, error); | ||
| return ret; | ||||
| } | ||||
|
|
r210 | /// <summary> | ||
| /// Handles the state of the component change event, raises the <see cref="StateChanged"/> event, handles | ||||
| /// the transition to the <see cref="ExecutionState.Disposed"/> state (calls <see cref="Dispose(bool)"/> method). | ||||
| /// </summary> | ||||
| /// <param name="previous">The previous state</param> | ||||
| /// <param name="current">The current state</param> | ||||
| /// <param name="error">The last error if any.</param> | ||||
| /// <remarks> | ||||
| /// <para> | ||||
| /// 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. | ||||
| /// </para> | ||||
| /// <para> | ||||
| /// When overriding this method ensure the call is made to the base implementation, otherwise it will lead to | ||||
| /// the wrong behavior of the component. | ||||
| /// </para> | ||||
| /// </remarks> | ||||
|
|
r205 | protected virtual void OnStateChanged(ExecutionState previous, ExecutionState current, Exception error) { | ||
|
|
r210 | StateChanged.DispatchEvent( | ||
| this, | ||||
| new StateChangeEventArgs { | ||||
|
|
r205 | State = current, | ||
| LastError = error | ||||
|
|
r210 | } | ||
| ); | ||||
| if (current == ExecutionState.Disposed) { | ||||
| GC.SuppressFinalize(this); | ||||
| Dispose(true); | ||||
| } | ||||
|
|
r184 | } | ||
|
|
r203 | /// <summary> | ||
| /// Moves the component from running to failed state. | ||||
| /// </summary> | ||||
| /// <param name="error">The exception which is describing the error.</param> | ||||
|
|
r205 | protected bool Fail(Exception error) { | ||
| return MoveIfInState(Commands.Fail, null, error, ExecutionState.Running); | ||||
|
|
r203 | } | ||
|
|
r205 | /// <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; | ||||
|
|
r184 | try { | ||
|
|
r205 | OnResetState(); | ||
| Move(Commands.Ok, null, null); | ||||
| return true; | ||||
|
|
r184 | } catch (Exception err) { | ||
|
|
r205 | Move(Commands.Fail, null, err); | ||
|
|
r184 | throw; | ||
|
|
r205 | } | ||
| } | ||||
| /// <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(); | ||||
|
|
r184 | } | ||
|
|
r185 | IPromise InvokeAsync(Commands cmd, Func<IPromise> action, Action<IPromise, IDeferred> chain) { | ||
| IPromise promise = null; | ||||
| IPromise prev; | ||||
|
|
r184 | |||
|
|
r185 | var task = new ActionChainTask(action, null, null, true); | ||
|
|
r205 | Action<Exception> errorOrCancel = e => { | ||
| if (e == null) | ||||
| e = new OperationCanceledException(); | ||||
| MoveIfPending(Commands.Fail, null, e, promise); | ||||
| throw new PromiseTransientException(e); | ||||
| }; | ||||
|
|
r184 | |||
|
|
r205 | promise = task.Then( | ||
| () => MoveIfPending(Commands.Ok, null, null, promise), | ||||
| errorOrCancel, | ||||
| errorOrCancel | ||||
| ); | ||||
|
|
r196 | |||
|
|
r205 | prev = Move(cmd, promise, null); | ||
|
|
r157 | |||
|
|
r185 | if (prev == null) | ||
| task.Resolve(); | ||||
| else | ||||
| chain(prev, task); | ||||
|
|
r156 | |||
|
|
r185 | return promise; | ||
|
|
r156 | } | ||
|
|
r184 | |||
|
|
r156 | #region IInitializable implementation | ||
|
|
r205 | public void Initialize() { | ||
| Move(Commands.Init, null, null); | ||||
| try { | ||||
| OnInitialize(); | ||||
| Move(Commands.Ok, null, null); | ||||
| } catch (Exception err) { | ||||
| Move(Commands.Fail, null, err); | ||||
| throw; | ||||
| } | ||||
|
|
r184 | } | ||
| protected virtual void OnInitialize() { | ||||
|
|
r156 | } | ||
| #endregion | ||||
| #region IRunnable implementation | ||||
| public IPromise Start() { | ||||
|
|
r185 | return InvokeAsync(Commands.Start, OnStart, null); | ||
|
|
r156 | } | ||
| protected virtual IPromise OnStart() { | ||||
|
|
r205 | return Promise.Success; | ||
|
|
r156 | } | ||
|
|
r185 | public IPromise Stop() { | ||
|
|
r210 | return InvokeAsync(Commands.Stop, OnStop, StopPending); | ||
|
|
r185 | } | ||
| protected virtual IPromise OnStop() { | ||||
|
|
r205 | return Promise.Success; | ||
|
|
r156 | } | ||
|
|
r185 | /// <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 { | ||||
|
|
r187 | // связваем текущую операцию с операцией остановки | ||
| current.On( | ||||
| stop.Resolve, // если текущая операция заверщилась, то можно начинать остановку | ||||
| stop.Reject, // если текущая операция дала ошибку - то все плохо, нельзя продолжать | ||||
| e => stop.Resolve() // если текущая отменилась, то можно начинать остановку | ||||
| ); | ||||
| // посылаем текущей операции сигнал остановки | ||||
|
|
r185 | current.Cancel(); | ||
| } | ||||
|
|
r156 | } | ||
| public ExecutionState State { | ||||
| get { | ||||
|
|
r203 | return m_stateMachine.State; | ||
|
|
r156 | } | ||
| } | ||||
| public Exception LastError { | ||||
| get { | ||||
|
|
r185 | return m_lastError; | ||
|
|
r156 | } | ||
| } | ||||
| #endregion | ||||
|
|
r185 | |||
| #region IDisposable implementation | ||||
|
|
r203 | /// <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> | ||||
|
|
r208 | [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dipose(bool) and GC.SuppessFinalize are called")] | ||
|
|
r185 | public void Dispose() { | ||
|
|
r210 | Move(Commands.Dispose, null, null); | ||
|
|
r185 | } | ||
| ~RunnableComponent() { | ||||
|
|
r208 | Dispose(false); | ||
|
|
r185 | } | ||
| #endregion | ||||
|
|
r205 | /// <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> | ||||
|
|
r208 | /// <param name="pending">The operation which is currenty pending</param> | ||
| protected virtual void Dispose(bool disposing) { | ||||
|
|
r185 | } | ||
|
|
r156 | } | ||
| } | ||||
