RunnableComponent.cs
411 lines
| 16.2 KiB
| text/x-csharp
|
CSharpLexer
cin
|
r156 | using System; | ||
cin
|
r244 | using System.Diagnostics.CodeAnalysis; | ||
cin
|
r156 | namespace Implab.Components { | ||
cin
|
r185 | public abstract class RunnableComponent : IDisposable, IRunnable, IInitializable { | ||
cin
|
r184 | enum Commands { | ||
Ok = 0, | ||||
Fail, | ||||
Init, | ||||
Start, | ||||
Stop, | ||||
Dispose, | ||||
cin
|
r205 | Reset, | ||
Last = Reset | ||||
cin
|
r184 | } | ||
class StateMachine { | ||||
cin
|
r210 | public static readonly ExecutionState[,] ReusableTransitions; | ||
public static readonly ExecutionState[,] NonreusableTransitions; | ||||
cin
|
r244 | 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()); | ||||
} | ||||
cin
|
r210 | } | ||
cin
|
r184 | |||
static StateMachine() { | ||||
cin
|
r210 | ReusableTransitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1]; | ||
cin
|
r184 | |||
cin
|
r210 | var common = new StateBuilder() | ||
.Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init) | ||||
.Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose) | ||||
cin
|
r185 | |||
cin
|
r210 | .Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok) | ||
.Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail) | ||||
cin
|
r184 | |||
cin
|
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) | ||||
cin
|
r184 | |||
cin
|
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) | ||||
cin
|
r244 | .Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset) | ||
cin
|
r210 | .Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail) | ||
.Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose) | ||||
.Edge(ExecutionState.Disposed, ExecutionState.Disposed, Commands.Dispose); | ||||
cin
|
r184 | |||
cin
|
r210 | var reusable = common | ||
.Clone() | ||||
cin
|
r244 | .Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok); | ||
var nonreusable = common | ||||
.Clone() | ||||
.Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Ok); | ||||
cin
|
r210 | |||
NonreusableTransitions = nonreusable.States; | ||||
ReusableTransitions = reusable.States; | ||||
cin
|
r184 | |||
} | ||||
cin
|
r210 | readonly ExecutionState[,] m_states; | ||
cin
|
r184 | |||
public ExecutionState State { | ||||
get; | ||||
private set; | ||||
} | ||||
cin
|
r210 | public StateMachine(ExecutionState[,] states, ExecutionState initial) { | ||
cin
|
r184 | State = initial; | ||
cin
|
r210 | m_states = states; | ||
cin
|
r184 | } | ||
public bool Move(Commands cmd) { | ||||
cin
|
r210 | var next = m_states[(int)State, (int)cmd]; | ||
cin
|
r184 | if (next == ExecutionState.Undefined) | ||
return false; | ||||
State = next; | ||||
return true; | ||||
} | ||||
} | ||||
IPromise m_pending; | ||||
Exception m_lastError; | ||||
cin
|
r244 | 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) { | ||||
cin
|
r210 | m_stateMachine = new StateMachine( | ||
reusable ? StateMachine.ReusableTransitions : StateMachine.NonreusableTransitions, | ||||
initialized ? ExecutionState.Ready : ExecutionState.Created | ||||
); | ||||
cin
|
r205 | m_reusable = reusable; | ||
cin
|
r244 | } | ||
/// <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> | ||||
cin
|
r205 | protected RunnableComponent(bool initialized) : this(initialized, false) { | ||
cin
|
r184 | } | ||
void ThrowInvalidCommand(Commands cmd) { | ||||
cin
|
r185 | if (m_stateMachine.State == ExecutionState.Disposed) | ||
cin
|
r244 | throw new ObjectDisposedException(ToString()); | ||
cin
|
r205 | 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; | ||||
cin
|
r244 | lock (m_stateMachine) { | ||
if (m_stateMachine.State != state) | ||||
return false; | ||||
prev = m_stateMachine.State; | ||||
if (!m_stateMachine.Move(cmd)) | ||||
cin
|
r205 | ThrowInvalidCommand(cmd); | ||
current = m_stateMachine.State; | ||||
m_pending = pending; | ||||
m_lastError = error; | ||||
} | ||||
if (prev != current) | ||||
OnStateChanged(prev, current, error); | ||||
return true; | ||||
cin
|
r184 | } | ||
cin
|
r205 | bool MoveIfPending(Commands cmd, IPromise pending, Exception error, IPromise expected) { | ||
ExecutionState prev, current; | ||||
cin
|
r244 | lock (m_stateMachine) { | ||
if (m_pending != expected) | ||||
return false; | ||||
prev = m_stateMachine.State; | ||||
if (!m_stateMachine.Move(cmd)) | ||||
cin
|
r205 | ThrowInvalidCommand(cmd); | ||
current = m_stateMachine.State; | ||||
m_pending = pending; | ||||
m_lastError = error; | ||||
cin
|
r244 | } | ||
cin
|
r205 | if (prev != current) | ||
cin
|
r244 | OnStateChanged(prev, current, error); | ||
cin
|
r205 | return true; | ||
} | ||||
IPromise Move(Commands cmd, IPromise pending, Exception error) { | ||||
ExecutionState prev, current; | ||||
IPromise ret; | ||||
cin
|
r244 | lock (m_stateMachine) { | ||
prev = m_stateMachine.State; | ||||
if (!m_stateMachine.Move(cmd)) | ||||
cin
|
r205 | ThrowInvalidCommand(cmd); | ||
current = m_stateMachine.State; | ||||
ret = m_pending; | ||||
m_pending = pending; | ||||
cin
|
r244 | m_lastError = error; | ||
cin
|
r205 | } | ||
cin
|
r210 | if (prev != current) | ||
cin
|
r205 | OnStateChanged(prev, current, error); | ||
return ret; | ||||
} | ||||
cin
|
r244 | /// <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> | ||||
cin
|
r210 | /// <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> | ||||
cin
|
r244 | 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); | ||||
} | ||||
cin
|
r184 | } | ||
cin
|
r203 | /// <summary> | ||
/// Moves the component from running to failed state. | ||||
/// </summary> | ||||
/// <param name="error">The exception which is describing the error.</param> | ||||
cin
|
r244 | protected bool Fail(Exception error) { | ||
return MoveIfInState(Commands.Fail, null, error, ExecutionState.Running); | ||||
cin
|
r203 | } | ||
cin
|
r244 | /// <summary> | ||
/// Tries to reset <see cref="ExecutionState.Failed"/> state to <see cref="ExecutionState.Ready"/>. | ||||
/// </summary> | ||||
cin
|
r205 | /// <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> | ||||
cin
|
r244 | protected bool ResetState() { | ||
if (!MoveIfInState(Commands.Reset, null, null, ExecutionState.Failed)) | ||||
return false; | ||||
cin
|
r184 | try { | ||
cin
|
r205 | OnResetState(); | ||
Move(Commands.Ok, null, null); | ||||
return true; | ||||
cin
|
r184 | } catch (Exception err) { | ||
cin
|
r205 | Move(Commands.Fail, null, err); | ||
cin
|
r184 | throw; | ||
cin
|
r244 | } | ||
cin
|
r205 | } | ||
cin
|
r244 | /// <summary> | ||
/// This method is called by <see cref="ResetState"/> to reinitialize component in the failed state. | ||||
cin
|
r205 | /// </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> | ||||
cin
|
r244 | protected virtual void OnResetState() { | ||
throw new NotImplementedException(); | ||||
cin
|
r184 | } | ||
cin
|
r244 | IPromise InvokeAsync(Commands cmd, Func<IPromise> action, Action<IPromise, IResolvable> chain) { | ||
cin
|
r185 | IPromise promise = null; | ||
IPromise prev; | ||||
cin
|
r184 | |||
cin
|
r185 | var task = new ActionChainTask(action, null, null, true); | ||
cin
|
r244 | Action<Exception> errorOrCancel = e => { | ||
if (e == null) | ||||
e = new OperationCanceledException(); | ||||
MoveIfPending(Commands.Fail, null, e, promise); | ||||
throw new PromiseTransientException(e); | ||||
cin
|
r205 | }; | ||
cin
|
r184 | |||
cin
|
r205 | promise = task.Then( | ||
() => MoveIfPending(Commands.Ok, null, null, promise), | ||||
errorOrCancel, | ||||
cin
|
r244 | errorOrCancel | ||
cin
|
r205 | ); | ||
cin
|
r196 | |||
cin
|
r205 | prev = Move(cmd, promise, null); | ||
cin
|
r157 | |||
cin
|
r185 | if (prev == null) | ||
task.Resolve(); | ||||
else | ||||
chain(prev, task); | ||||
cin
|
r156 | |||
cin
|
r185 | return promise; | ||
cin
|
r156 | } | ||
cin
|
r184 | |||
cin
|
r156 | #region IInitializable implementation | ||
cin
|
r205 | public void Initialize() { | ||
cin
|
r244 | Move(Commands.Init, null, null); | ||
cin
|
r205 | try { | ||
OnInitialize(); | ||||
Move(Commands.Ok, null, null); | ||||
} catch (Exception err) { | ||||
Move(Commands.Fail, null, err); | ||||
throw; | ||||
} | ||||
cin
|
r184 | } | ||
protected virtual void OnInitialize() { | ||||
cin
|
r156 | } | ||
#endregion | ||||
#region IRunnable implementation | ||||
public IPromise Start() { | ||||
cin
|
r185 | return InvokeAsync(Commands.Start, OnStart, null); | ||
cin
|
r156 | } | ||
protected virtual IPromise OnStart() { | ||||
cin
|
r205 | return Promise.Success; | ||
cin
|
r156 | } | ||
cin
|
r185 | public IPromise Stop() { | ||
cin
|
r210 | return InvokeAsync(Commands.Stop, OnStop, StopPending); | ||
cin
|
r185 | } | ||
protected virtual IPromise OnStop() { | ||||
cin
|
r205 | return Promise.Success; | ||
cin
|
r156 | } | ||
cin
|
r185 | /// <summary> | ||
/// Stops the current operation if one exists. | ||||
/// </summary> | ||||
/// <param name="current">Current.</param> | ||||
/// <param name="stop">Stop.</param> | ||||
cin
|
r244 | protected virtual void StopPending(IPromise current, IResolvable stop) { | ||
cin
|
r185 | if (current == null) { | ||
stop.Resolve(); | ||||
} else { | ||||
cin
|
r187 | // связваем текущую операцию с операцией остановки | ||
current.On( | ||||
stop.Resolve, // если текущая операция заверщилась, то можно начинать остановку | ||||
stop.Reject, // если текущая операция дала ошибку - то все плохо, нельзя продолжать | ||||
e => stop.Resolve() // если текущая отменилась, то можно начинать остановку | ||||
); | ||||
// посылаем текущей операции сигнал остановки | ||||
cin
|
r185 | current.Cancel(); | ||
} | ||||
cin
|
r156 | } | ||
public ExecutionState State { | ||||
get { | ||||
cin
|
r203 | return m_stateMachine.State; | ||
cin
|
r156 | } | ||
} | ||||
public Exception LastError { | ||||
get { | ||||
cin
|
r185 | return m_lastError; | ||
cin
|
r156 | } | ||
} | ||||
#endregion | ||||
cin
|
r185 | |||
#region IDisposable implementation | ||||
cin
|
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> | ||||
cin
|
r208 | [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dipose(bool) and GC.SuppessFinalize are called")] | ||
cin
|
r185 | public void Dispose() { | ||
cin
|
r210 | Move(Commands.Dispose, null, null); | ||
cin
|
r185 | } | ||
~RunnableComponent() { | ||||
cin
|
r208 | Dispose(false); | ||
cin
|
r185 | } | ||
#endregion | ||||
cin
|
r244 | /// <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> | ||||
cin
|
r208 | protected virtual void Dispose(bool disposing) { | ||
cin
|
r185 | } | ||
cin
|
r156 | } | ||
} | ||||