RunnableComponent.cs
375 lines
| 14.5 KiB
| text/x-csharp
|
CSharpLexer
cin
|
r156 | using System; | ||
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 { | ||||
static readonly ExecutionState[,] _transitions; | ||||
static StateMachine() { | ||||
_transitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1]; | ||||
cin
|
r185 | 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); | ||||
cin
|
r184 | |||
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); | ||||
cin
|
r205 | Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok); | ||
Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose); | ||||
cin
|
r185 | |||
Edge(ExecutionState.Failed, ExecutionState.Disposed, Commands.Dispose); | ||||
cin
|
r205 | Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset); | ||
cin
|
r184 | } | ||
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; | ||||
cin
|
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) { | ||||
cin
|
r184 | m_stateMachine = new StateMachine(initialized ? ExecutionState.Ready : ExecutionState.Created); | ||
cin
|
r205 | 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) { | ||||
cin
|
r184 | } | ||
cin
|
r203 | /// <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; | ||||
cin
|
r185 | } | ||
cin
|
r184 | void ThrowInvalidCommand(Commands cmd) { | ||
cin
|
r185 | if (m_stateMachine.State == ExecutionState.Disposed) | ||
cin
|
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; | ||||
cin
|
r184 | } | ||
cin
|
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; | ||||
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 | ||||
}); | ||||
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
|
r205 | protected bool Fail(Exception error) { | ||
return MoveIfInState(Commands.Fail, null, error, ExecutionState.Running); | ||||
cin
|
r203 | } | ||
cin
|
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; | ||||
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
|
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(); | ||||
cin
|
r184 | } | ||
cin
|
r185 | IPromise InvokeAsync(Commands cmd, Func<IPromise> action, Action<IPromise, IDeferred> chain) { | ||
IPromise promise = null; | ||||
IPromise prev; | ||||
cin
|
r184 | |||
cin
|
r185 | var task = new ActionChainTask(action, null, null, true); | ||
cin
|
r205 | Action<Exception> errorOrCancel = e => { | ||
if (e == null) | ||||
e = new OperationCanceledException(); | ||||
MoveIfPending(Commands.Fail, null, e, promise); | ||||
throw new PromiseTransientException(e); | ||||
}; | ||||
cin
|
r184 | |||
cin
|
r205 | promise = task.Then( | ||
() => MoveIfPending(Commands.Ok, null, null, promise), | ||||
errorOrCancel, | ||||
errorOrCancel | ||||
); | ||||
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() { | ||
Move(Commands.Init, null, null); | ||||
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
|
r205 | var pending = InvokeAsync(Commands.Stop, OnStop, StopPending); | ||
return m_reusable ? pending : pending.Then(Dispose); | ||||
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> | ||||
protected virtual void StopPending(IPromise current, IDeferred stop) { | ||||
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
|
r185 | public void Dispose() { | ||
IPromise pending; | ||||
cin
|
r205 | |||
cin
|
r185 | lock (m_stateMachine) { | ||
if (m_stateMachine.State == ExecutionState.Disposed) | ||||
return; | ||||
cin
|
r205 | pending = Move(Commands.Dispose, null, null); | ||
} | ||||
cin
|
r185 | |||
cin
|
r205 | GC.SuppressFinalize(this); | ||
cin
|
r185 | 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 { | ||||
cin
|
r205 | Dispose(true, null); | ||
cin
|
r185 | } | ||
} | ||||
~RunnableComponent() { | ||||
Dispose(false, null); | ||||
} | ||||
#endregion | ||||
cin
|
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> | ||||
/// <param name="lastError">The last error which occured during the component stop.</param> | ||||
cin
|
r185 | protected virtual void Dispose(bool disposing, Exception lastError) { | ||
} | ||||
cin
|
r156 | } | ||
} | ||||