##// END OF EJS Templates
added Safe.DispatchEvent() a legacy equivalent for '?.Invoke()'...
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

File last commit:

r205:8200ab154c8a v2
r207:558f34b2fb50 v2
Show More
RunnableComponent.cs
375 lines | 14.5 KiB | text/x-csharp | CSharpLexer
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) {
}
}
}