Mercurial > pub > ImplabNet
view Implab/Components/RunnableComponent.cs @ 207:558f34b2fb50 v2
added Safe.DispatchEvent() a legacy equivalent for '?.Invoke()'
added Safe.Dispose(IEnumerable)
added PromiseExtensions.CancellationPoint to add a cancellation point to the chain of promises
added IPromise<T> PromiseExtensions.Then<T>(this IPromise<T> that, Action<T> success) overloads
added PromiseExtensions.Error() overloads to handle a error or(and) a cancellation
author | cin |
---|---|
date | Wed, 09 Nov 2016 12:03:22 +0300 |
parents | 8200ab154c8a |
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) { } } }