|
|
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<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(
|
|
|
reusable ? StateMachine.ReusableTransitions : StateMachine.NonreusableTransitions,
|
|
|
initialized ? ExecutionState.Ready : ExecutionState.Created
|
|
|
);
|
|
|
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) {
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
/// <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>
|
|
|
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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <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, IResolvable> 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() {
|
|
|
return InvokeAsync(Commands.Stop, OnStop, StopPending);
|
|
|
}
|
|
|
|
|
|
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, 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
|
|
|
|
|
|
/// <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>
|
|
|
[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
|
|
|
|
|
|
/// <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>
|
|
|
protected virtual void Dispose(bool disposing) {
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|