From 77a533021c33fda46280e32b67ab503b97a2bce7 Mon Sep 17 00:00:00 2001 From: ravengerUA Date: Sun, 12 Mar 2017 22:20:30 +0200 Subject: [PATCH 1/3] fsm update --- .../CoreAPISpec.ApproveCore.approved.txt | 49 +- ...oreAPISpec.ApprovePersistence.approved.txt | 8 +- .../Akka.Persistence/Fsm/PersistentFSMBase.cs | 10 +- .../Akka.Remote.TestKit/BarrierCoordinator.cs | 4 +- src/core/Akka.Tests/Actor/FSMActorSpec.cs | 566 +++++++++++++++ src/core/Akka.Tests/Actor/FSMTimingSpec.cs | 539 ++++++++------- .../Akka.Tests/Actor/FSMTransitionSpec.cs | 160 +++-- src/core/Akka.Tests/Akka.Tests.csproj | 1 + src/core/Akka/Actor/FSM.cs | 646 ++++++++++-------- src/core/Akka/Routing/Listeners.cs | 12 +- src/core/Akka/Util/Internal/Extensions.cs | 12 +- 11 files changed, 1381 insertions(+), 626 deletions(-) create mode 100644 src/core/Akka.Tests/Actor/FSMActorSpec.cs diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 657a2c726e6..e9fc197e3fd 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -720,6 +720,7 @@ namespace Akka.Actor public TState StateName { get; } public void CancelTimer(string name) { } public Akka.Actor.FSMBase.State GoTo(TState nextStateName) { } + [System.ObsoleteAttribute("This method is obsoleted. Use GoTo(nextStateName).Using(newStateData) [1.2.0]")] public Akka.Actor.FSMBase.State GoTo(TState nextStateName, TData stateData) { } public void Initialize() { } public bool IsTimerActive(string name) { } @@ -750,52 +751,59 @@ namespace Akka.Actor public abstract class FSMBase : Akka.Actor.ActorBase { protected FSMBase() { } - public class CurrentState + public sealed class CurrentState { public CurrentState(Akka.Actor.IActorRef fsmRef, TS state) { } public Akka.Actor.IActorRef FsmRef { get; } public TS State { get; } + public override bool Equals(object obj) { } + public override int GetHashCode() { } + public override string ToString() { } } - public class Event : Akka.Actor.INoSerializationVerificationNeeded + public sealed class Event : Akka.Actor.INoSerializationVerificationNeeded { public Event(object fsmEvent, TD stateData) { } public object FsmEvent { get; } public TD StateData { get; } public override string ToString() { } } - public class Failure : Akka.Actor.FSMBase.Reason + public sealed class Failure : Akka.Actor.FSMBase.Reason { public Failure(object cause) { } public object Cause { get; } } - public class LogEntry + public sealed class LogEntry { public LogEntry(TS stateName, TD stateData, object fsmEvent) { } public object FsmEvent { get; } public TD StateData { get; } public TS StateName { get; } + public override string ToString() { } } - public class Normal : Akka.Actor.FSMBase.Reason + public sealed class Normal : Akka.Actor.FSMBase.Reason { + [System.ObsoleteAttribute("This constructor is obsoleted. Use Normal.Instance [1.2.0]")] public Normal() { } + public static Akka.Actor.FSMBase.Normal Instance { get; } } public abstract class Reason { protected Reason() { } } - public class Shutdown : Akka.Actor.FSMBase.Reason + public sealed class Shutdown : Akka.Actor.FSMBase.Reason { + [System.ObsoleteAttribute("This constructor is obsoleted. Use Shutdown.Instance [1.2.0]")] public Shutdown() { } + public static Akka.Actor.FSMBase.Shutdown Instance { get; } } public class State : System.IEquatable> { - public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.List replies = null) { } - public System.Collections.Generic.List Replies { get; set; } + public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, bool notifies = True) { } + public System.Collections.Generic.IReadOnlyCollection Replies { get; set; } public TD StateData { get; } public TS StateName { get; } public Akka.Actor.FSMBase.Reason StopReason { get; } public System.Nullable Timeout { get; } - public Akka.Actor.FSMBase.State Copy(System.Nullable timeout, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.List replies = null) { } public bool Equals(Akka.Actor.FSMBase.State other) { } public override bool Equals(object obj) { } public Akka.Actor.FSMBase.State ForMax(System.TimeSpan timeout) { } @@ -804,31 +812,36 @@ namespace Akka.Actor public override string ToString() { } public Akka.Actor.FSMBase.State Using(TD nextStateData) { } } - public class StateTimeout + public sealed class StateTimeout { + [System.ObsoleteAttribute("This constructor is obsoleted. Use Shutdown.Instance [1.2.0]")] public StateTimeout() { } + public static Akka.Actor.FSMBase.StateTimeout Instance { get; } } - public class StopEvent : Akka.Actor.INoSerializationVerificationNeeded + public sealed class StopEvent : Akka.Actor.INoSerializationVerificationNeeded { public StopEvent(Akka.Actor.FSMBase.Reason reason, TS terminatedState, TD stateData) { } public Akka.Actor.FSMBase.Reason Reason { get; } public TD StateData { get; } public TS TerminatedState { get; } + public override string ToString() { } } - public class SubscribeTransitionCallBack + public sealed class SubscribeTransitionCallBack { public SubscribeTransitionCallBack(Akka.Actor.IActorRef actorRef) { } public Akka.Actor.IActorRef ActorRef { get; } } - public class Transition + public sealed class Transition { public Transition(Akka.Actor.IActorRef fsmRef, TS from, TS to) { } public TS From { get; } public Akka.Actor.IActorRef FsmRef { get; } public TS To { get; } + public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } - public class UnsubscribeTransitionCallBack + public sealed class UnsubscribeTransitionCallBack { public UnsubscribeTransitionCallBack(Akka.Actor.IActorRef actorRef) { } public Akka.Actor.IActorRef ActorRef { get; } @@ -3952,7 +3965,7 @@ namespace Akka.Routing protected CustomRouterConfig() { } protected CustomRouterConfig(string routerDispatcher) { } } - public class Deafen : Akka.Routing.ListenerMessage + public sealed class Deafen : Akka.Routing.ListenerMessage { public Deafen(Akka.Actor.IActorRef listener) { } public Akka.Actor.IActorRef Listener { get; } @@ -4020,7 +4033,7 @@ namespace Akka.Routing { Akka.Routing.ListenerSupport Listeners { get; } } - public class Listen : Akka.Routing.ListenerMessage + public sealed class Listen : Akka.Routing.ListenerMessage { public Listen(Akka.Actor.IActorRef listener) { } public Akka.Actor.IActorRef Listener { get; } @@ -4386,7 +4399,7 @@ namespace Akka.Routing public Akka.Util.ISurrogated FromSurrogate(Akka.Actor.ActorSystem system) { } } } - public class WithListeners : Akka.Routing.ListenerMessage + public sealed class WithListeners : Akka.Routing.ListenerMessage { public WithListeners(System.Action listenerFunction) { } public System.Action ListenerFunction { get; } @@ -5073,7 +5086,7 @@ namespace Akka.Util.Internal public static string BetweenDoubleQuotes(this string self) { } public static System.Collections.Generic.IEnumerable Concat(this System.Collections.Generic.IEnumerable enumerable, T item) { } public static System.Collections.Generic.IEnumerable Drop(this System.Collections.Generic.IEnumerable self, int count) { } - public static void ForEach(this System.Collections.Generic.IEnumerable enumerable, System.Action action) { } + public static void ForEach(this System.Collections.Generic.IEnumerable source, System.Action action) { } public static TValue GetOrElse(this System.Collections.Generic.IDictionary hash, TKey key, TValue elseValue) { } public static T GetOrElse(this T obj, T elseValue) { } public static T Head(this System.Collections.Generic.IEnumerable self) { } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index fef9e77039b..6df5e8e301d 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -830,14 +830,14 @@ namespace Akka.Persistence.Fsm public void WhenUnhandled(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction stateFunction) { } public class State : Akka.Actor.FSMBase.State { - public State(TState stateName, TData stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.List replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } + public State(TState stateName, TData stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } public System.Action AfterTransitionHandler { get; } public Akka.Util.ILinearSeq DomainEvents { get; } - public bool Notifies { get; set; } + public new bool Notifies { get; set; } public Akka.Persistence.Fsm.PersistentFSMBase.State AndThen(System.Action handler) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Applying(Akka.Util.ILinearSeq events) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Applying(TEvent e) { } - public Akka.Persistence.Fsm.PersistentFSMBase.State Copy(System.Nullable timeout, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.List replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } + public Akka.Persistence.Fsm.PersistentFSMBase.State Copy(System.Nullable timeout, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } public Akka.Persistence.Fsm.PersistentFSMBase.State ForMax(System.TimeSpan timeout) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Replying(object replyValue) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Using(TData nextStateData) { } @@ -854,7 +854,7 @@ namespace Akka.Persistence.Fsm public System.Action AfterTransitionHandler { get; } public Akka.Util.ILinearSeq DomainEvents { get; } public bool Notifies { get; set; } - public System.Collections.Generic.List Replies { get; } + public System.Collections.Generic.IReadOnlyCollection Replies { get; } public TData StateData { get; } public TState StateName { get; } public Akka.Actor.FSMBase.Reason StopReason { get; } diff --git a/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs b/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs index d52557a8cff..f2c2928b983 100644 --- a/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs +++ b/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs @@ -245,7 +245,7 @@ public State Stay() /// TBD public State Stop() { - return Stop(new FSMBase.Normal()); + return Stop(FSMBase.Normal.Instance); } /// @@ -659,7 +659,7 @@ protected override void PostStop() * Setting this instance's state to Terminated does no harm during restart, since * the new instance will initialize fresh using StartWith. */ - Terminate(Stay().WithStopReason(new FSMBase.Shutdown())); + Terminate(Stay().WithStopReason(FSMBase.Shutdown.Instance)); base.PostStop(); } @@ -1059,7 +1059,7 @@ public State ForMax(TimeSpan timeout) /// /// TBD /// - public List Replies + public IReadOnlyCollection Replies { get { @@ -1137,7 +1137,7 @@ public class State : FSMBase.State /// TBD /// TBD public State(TState stateName, TData stateData, TimeSpan? timeout = null, FSMBase.Reason stopReason = null, - List replies = null, ILinearSeq domainEvents = null, Action afterTransitionDo = null) + IReadOnlyList replies = null, ILinearSeq domainEvents = null, Action afterTransitionDo = null) : base(stateName, stateData, timeout, stopReason, replies) { AfterTransitionHandler = afterTransitionDo; @@ -1208,7 +1208,7 @@ public State AndThen(Action handler) /// TBD /// TBD public State Copy(TimeSpan? timeout, FSMBase.Reason stopReason = null, - List replies = null, ILinearSeq domainEvents = null, Action afterTransitionDo = null) + IReadOnlyList replies = null, ILinearSeq domainEvents = null, Action afterTransitionDo = null) { return new State(StateName, StateData, timeout ?? Timeout, stopReason ?? StopReason, replies ?? Replies, diff --git a/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs b/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs index 8c8b81d8b86..de4df8ab7bf 100644 --- a/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs +++ b/src/core/Akka.Remote.TestKit/BarrierCoordinator.cs @@ -482,7 +482,7 @@ protected void InitFSM() //we only allow the deadlines to get shorter if (enterDeadline.TimeLeft < @event.StateData.Deadline.TimeLeft) { - SetTimer("Timeout", new StateTimeout(), enterDeadline.TimeLeft, false); + SetTimer("Timeout", StateTimeout.Instance, enterDeadline.TimeLeft, false); nextState = HandleBarrier(@event.StateData.Copy(arrived: together, deadline: enterDeadline)); } else @@ -516,7 +516,7 @@ protected void InitFSM() OnTransition((state, nextState) => { - if (state == State.Idle && nextState == State.Waiting) SetTimer("Timeout", new StateTimeout(), NextStateData.Deadline.TimeLeft, false); + if (state == State.Idle && nextState == State.Waiting) SetTimer("Timeout", StateTimeout.Instance, NextStateData.Deadline.TimeLeft, false); else if(state == State.Waiting && nextState == State.Idle) CancelTimer("Timeout"); }); diff --git a/src/core/Akka.Tests/Actor/FSMActorSpec.cs b/src/core/Akka.Tests/Actor/FSMActorSpec.cs new file mode 100644 index 00000000000..60474daaeb0 --- /dev/null +++ b/src/core/Akka.Tests/Actor/FSMActorSpec.cs @@ -0,0 +1,566 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2016 Lightbend Inc. +// Copyright (C) 2013-2016 Akka.NET project +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Event; +using Akka.TestKit; +using FluentAssertions; +using Xunit; +using static Akka.Actor.FSMBase; + +namespace Akka.Tests.Actor +{ + public class FSMActorSpec : AkkaSpec + { + #region Actors + public class Latches + { + public Latches(ActorSystem system) + { + UnlockedLatch = new TestLatch(); + LockedLatch = new TestLatch(); + UnhandledLatch = new TestLatch(); + TerminatedLatch = new TestLatch(); + TransitionLatch = new TestLatch(); + InitialStateLatch = new TestLatch(); + TransitionCallBackLatch = new TestLatch(); + } + + public TestLatch UnlockedLatch { get; } + + public TestLatch LockedLatch { get; } + + public TestLatch UnhandledLatch { get; } + + public TestLatch TerminatedLatch { get; } + + public TestLatch TransitionLatch { get; } + + public TestLatch InitialStateLatch { get; } + + public TestLatch TransitionCallBackLatch { get; } + } + public enum LockState + { + Locked, + Open + } + + public sealed class CodeState + { + public CodeState(string soFar, string code) + { + SoFar = soFar; + Code = code; + } + + public string SoFar { get; } + + public string Code { get; } + } + + public class Hello + { + public static Hello Instance { get; } = new Hello(); + + private Hello() { } + } + + public class Bye + { + public static Bye Instance { get; } = new Bye(); + + private Bye() { } + } + + public class Lock : FSM + { + private readonly Latches _latches; + private readonly ILoggingAdapter Log = Context.GetLogger(); + + public Lock(string code, TimeSpan timeout, Latches latches) + { + var code1 = code; + _latches = latches; + StartWith(LockState.Locked, new CodeState("", code1)); + + When(LockState.Locked, evt => + { + if (evt.FsmEvent is char) + { + var codeState = evt.StateData; + if (codeState.Code == code1) + { + DoUnlock(); + return GoTo(LockState.Open).Using(new CodeState("", codeState.Code)).ForMax(timeout); + } + } + else if (evt.FsmEvent.Equals("hello")) + { + return Stay().Replying("world"); + } + else if (evt.FsmEvent.Equals("bey")) + { + return Stop(Shutdown.Instance); + } + + return null; + }); + + When(LockState.Open, evt => + { + if (evt.FsmEvent is StateTimeout) + { + DoLock(); + return GoTo(LockState.Locked); + } + + return null; + }); + + WhenUnhandled(evt => + { + var msg = evt.FsmEvent; + Log.Warning($"unhandled event {msg} in state {StateName} with data {StateData}"); + latches.UnhandledLatch.Open(); + return Stay(); + }); + + OnTransition((state, nextState) => + { + if (state == LockState.Locked && nextState == LockState.Open) + { + _latches.TransitionLatch.Open(); + } + }); + + OnTermination(evt => + { + if (evt.Reason == Shutdown.Instance && evt.TerminatedState == LockState.Locked) + { + // stop is called from lockstate with shutdown as reason... + latches.TerminatedLatch.Open(); + } + }); + + Initialize(); + } + + private void DoLock() + { + _latches.LockedLatch.Open(); + } + + private void DoUnlock() + { + _latches.UnlockedLatch.Open(); + } + } + + public class TransitionTester : UntypedActor + { + private readonly Latches _latches; + + public TransitionTester(Latches latches) + { + _latches = latches; + } + + protected override void OnReceive(object message) + { + if (message is Transition) + { + _latches.TransitionCallBackLatch.Open(); + } + else if (message is CurrentState) + { + _latches.InitialStateLatch.Open(); + } + } + } + + public class AnswerTester : UntypedActor + { + private readonly TestLatch _answerLatch; + private readonly IActorRef _lockFsm; + + public AnswerTester(TestLatch answerLatch, IActorRef lockFsm) + { + _answerLatch = answerLatch; + _lockFsm = lockFsm; + } + + protected override void OnReceive(object message) + { + if (message is Hello) + { + _lockFsm.Tell("hello"); + } + else if (message.Equals("world")) + { + _answerLatch.Open(); + } + else if (message is Bye) + { + _lockFsm.Tell("bye"); + } + } + } + + public class ActorLogTermination : FSM + { + public ActorLogTermination() + { + StartWith(1, null); + + When(1, evt => + { + if (evt.FsmEvent.Equals("go")) + { + return GoTo(2); + } + return null; + }); + } + } + + public class ActorStopTermination : FSM + { + private readonly TestLatch _testLatch; + + public ActorStopTermination(TestLatch testLatch, IActorRef testActor) + { + _testLatch = testLatch; + + StartWith(1, null); + + When(1, evt => null); + + OnTermination(x => + { + testActor.Tell(x); + }); + } + + protected override void PreStart() + { + _testLatch.CountDown(); + } + } + + public class ActorStopReason : FSM + { + public ActorStopReason(string expected, IActorRef testActor) + { + StartWith(1, null); + + When(1, evt => + { + if (evt.FsmEvent.Equals(2)) + { + return Stop(Normal.Instance, expected); + } + + return null; + }); + + OnTermination(x => + { + if (x.Reason is Normal + && x.TerminatedState == 1 + && x.StateData.Equals(expected)) + { + testActor.Tell("green"); + } + }); + } + } + + public class StopTimersFSM : FSM + { + public StopTimersFSM(IActorRef testActor, List timerNames) + { + StartWith("not-started", null); + + When("not-started", evt => + { + if (evt.FsmEvent.Equals("start")) + { + return GoTo("started").Replying("starting"); + } + + return null; + }); + + When("started", evt => + { + if (evt.FsmEvent.Equals("stop")) + { + return Stop(); + } + + return null; + }, 10.Seconds()); + + OnTransition((state1, state2) => + { + if (state1.Equals("not-started") && state2.Equals("started")) + { + foreach (var timerName in timerNames) + { + SetTimer(timerName, new object(), 10.Seconds(), false); + } + } + }); + + OnTermination(x => + { + foreach (var timerName in timerNames) + { + IsTimerActive(timerName).Should().BeFalse(); + var intern = (IInternalSupportsTestFSMRef)this; + intern.IsStateTimerActive.Should().BeFalse(); + } + testActor.Tell("stopped"); + }); + } + } + + public class RollingEventLogFsm : FSM, ILoggingFSM + { + public RollingEventLogFsm() + { + StartWith(1, 0); + + When(1, evt => + { + if (evt.FsmEvent.Equals("count")) + { + return Stay().Using(evt.StateData + 1); + } + else if (evt.FsmEvent.Equals("log")) + { + return Stay().Replying("getlog"); + } + + return null; + }); + } + } + + public class TransformingStateFsm : FSM + { + public TransformingStateFsm() + { + StartWith(0, 0); + + When(0, evt => + { + return Transform(evt2 => + { + if (evt2.FsmEvent.Equals("go")) + return Stay(); + + return null; + }).Using(state => + { + return GoTo(1); + })(evt); + }); + + When(1, evt => + { + return Stay(); + }); + } + } + + public const string OverrideInitState = "init"; + public const string OverrideTimeoutToInf = "override-timeout-to-inf"; + public class CancelStateTimeoutFsm : FSM + { + public CancelStateTimeoutFsm(TestProbe p) + { + StartWith(OverrideInitState, ""); + + When(OverrideInitState, evt => + { + if (evt.FsmEvent is StateTimeout) + { + p.Ref.Tell(StateTimeout.Instance); + return Stay(); + } + + if (evt.FsmEvent.Equals(OverrideTimeoutToInf)) + { + p.Ref.Tell(OverrideTimeoutToInf); + return Stay().ForMax(TimeSpan.MaxValue); + } + + return null; + }, 1.Seconds()); + + Initialize(); + } + } + + #endregion + + [Fact(Skip = "Not implemented yet")] + public void FSMActor_must_unlock_the_lock() + { + var latches = new Latches(Sys); + var timeout = 2.Seconds(); + var lockFsm = Sys.ActorOf(Props.Create(() => new Lock("33221", 1.Seconds(), latches))); + var transitionTester = Sys.ActorOf(Props.Create(() => new TransitionTester(latches))); + lockFsm.Tell(new SubscribeTransitionCallBack(transitionTester)); + latches.InitialStateLatch.Ready(timeout); + + lockFsm.Tell('3'); + lockFsm.Tell('3'); + lockFsm.Tell('2'); + lockFsm.Tell('2'); + lockFsm.Tell('1'); + + latches.UnlockedLatch.Ready(timeout); + latches.TransitionLatch.Ready(timeout); + latches.TransitionCallBackLatch.Ready(timeout); + latches.LockedLatch.Ready(timeout); + + EventFilter.Warning("unhandled event").ExpectOne(() => + { + lockFsm.Tell("not_handled"); + latches.UnhandledLatch.Ready(timeout); + }); + + var answerLatch = new TestLatch(); + var tester = Sys.ActorOf(Props.Create(() => new AnswerTester(answerLatch, lockFsm))); + tester.Tell(Hello.Instance); + answerLatch.Ready(timeout); + + tester.Tell(Bye.Instance); + latches.TerminatedLatch.Ready(timeout); + } + + [Fact] + public void FSMActor_must_log_termination() + { + var actorRef = Sys.ActorOf(Props.Create(() => new ActorLogTermination())); + var name = actorRef.Path.ToStringWithUid(); + EventFilter.Error("Next state 2 does not exist").ExpectOne(() => + { + Sys.EventStream.Subscribe(TestActor, typeof(Error)); + actorRef.Tell("go"); + var error = ExpectMsg(1.Seconds()); + error.LogSource.Should().Contain(name); + error.Message.Should().Be("Next state 2 does not exist"); + Sys.EventStream.Unsubscribe(TestActor); + }); + } + + [Fact] + public void FSMActor_must_run_onTermination_upon_ActorRef_Stop() + { + var started = new TestLatch(1); + var actorRef = Sys.ActorOf(Props.Create(() => new ActorStopTermination(started, TestActor))); + started.Ready(); + Sys.Stop(actorRef); + var stopEvent = ExpectMsg>(1.Seconds()); + stopEvent.Reason.Should().BeOfType(); + stopEvent.TerminatedState.Should().Be(1); + } + + [Fact] + public void FSMActor_must_run_onTermination_with_updated_state_upon_stop() + { + var expected = "pigdog"; + var actorRef = Sys.ActorOf(Props.Create(() => new ActorStopReason(expected, TestActor))); + actorRef.Tell(2); + ExpectMsg("green"); + } + + [Fact] + public void FSMActor_must_cancel_all_timers_when_terminated() + { + var timerNames = new List {"timer-1", "timer-2", "timer-3"}; + + var fsmRef = new TestFSMRef(Sys, Props.Create( + () => new StopTimersFSM(TestActor, timerNames))); + + Action checkTimersActive = active => + { + foreach (var timer in timerNames) + { + fsmRef.IsTimerActive(timer).Should().Be(active); + fsmRef.IsStateTimerActive().Should().Be(active); + } + }; + + checkTimersActive(false); + + fsmRef.Tell("start"); + ExpectMsg("starting", 1.Seconds()); + checkTimersActive(true); + + fsmRef.Tell("stop"); + ExpectMsg("stopped", 1.Seconds()); + } + + [Fact(Skip = "Not implemented yet")] + public void FSMActor_must_log_events_and_transitions_if_asked_to_do_so() + { + } + + [Fact(Skip = "Does not pass due to LoggingFsm limitations")] + public void FSMActor_must_fill_rolling_event_log_and_hand_it_out() + { + var fsmRef = new TestActorRef(Sys, Props.Create()); + fsmRef.Tell("log"); + ExpectMsg(1.Seconds()); + fsmRef.Tell("count"); + fsmRef.Tell("log"); + ExpectMsg(1.Seconds()); + fsmRef.Tell("count"); + fsmRef.Tell("log"); + ExpectMsg(1.Seconds()); + } + + [Fact] + public void FSMActor_must_allow_transforming_of_state_results() + { + var fsmRef = Sys.ActorOf(Props.Create()); + fsmRef.Tell(new SubscribeTransitionCallBack(TestActor)); + fsmRef.Tell("go"); + ExpectMsg(new CurrentState(fsmRef, 0)); + ExpectMsg(new Transition(fsmRef, 0, 1)); + } + + [Fact(Skip = "Not implemented yet")] + public void FSMActor_must_allow_cancelling_stateTimeout_by_issuing_forMax() + { + var sys = ActorSystem.Create("fsmEvent", Sys.Settings.Config); + var p = CreateTestProbe(sys); + + var fsmRef = sys.ActorOf(Props.Create(() => new CancelStateTimeoutFsm(p))); + + try + { + p.ExpectMsg(); + fsmRef.Tell(OverrideTimeoutToInf); + p.ExpectMsg(OverrideTimeoutToInf); + p.ExpectNoMsg(3.Seconds()); + } + finally + { + sys.WhenTerminated.Wait(); + } + } + } +} diff --git a/src/core/Akka.Tests/Actor/FSMTimingSpec.cs b/src/core/Akka.Tests/Actor/FSMTimingSpec.cs index cf3bcd5a359..285aba56709 100644 --- a/src/core/Akka.Tests/Actor/FSMTimingSpec.cs +++ b/src/core/Akka.Tests/Actor/FSMTimingSpec.cs @@ -8,123 +8,101 @@ using System; using System.Threading.Tasks; using Akka.Actor; +using Akka.Event; using Akka.TestKit; using Akka.Util.Internal; +using FluentAssertions; using Xunit; +using static Akka.Actor.FSMBase; namespace Akka.Tests.Actor { - public class FSMTimingSpec : AkkaSpec { - public IActorRef Self { get { return TestActor; } } - - public IActorRef _fsm; - - public IActorRef fsm - { - get { return _fsm ?? (_fsm = Sys.ActorOf(Props.Create(() => new StateMachine(Self)), "fsm")); } - } + public IActorRef fsm { get; } public FSMTimingSpec() - //: base("akka.test.test-actor.dispatcher.type=Dispatcher" + FullDebugConfig) - //: base("akka.test.test-actor.dispatcher.type=Dispatcher" + FullDebugConfig) - //: base(FullDebugConfig) { - //initializes the Finite State Machine, so it doesn't affect any of the time-sensitive tests below - fsm.Tell(new FSMBase.SubscribeTransitionCallBack(Self)); - ExpectMsg(new FSMBase.CurrentState(fsm, State.Initial), FSMSpecHelpers.CurrentStateExpector(), TimeSpan.FromSeconds(1)); + fsm = Sys.ActorOf(Props.Create(() => new StateMachine(TestActor)), "fsm"); + fsm.Tell(new SubscribeTransitionCallBack(TestActor)); + ExpectMsg(new CurrentState(fsm, FsmState.Initial), 1.Seconds()); } [Fact] public void FSM_must_receive_StateTimeout() { - //arrange - - //act - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => { - Within(TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(1), () => + Within(500.Milliseconds(), 1.Seconds(), () => { - fsm.Tell(State.TestStateTimeout, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestStateTimeout), FSMSpecHelpers.TransitionStateExpector()); - ExpectMsg(new FSMBase.Transition(fsm, State.TestStateTimeout, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + fsm.Tell(FsmState.TestStateTimeout); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestStateTimeout)); + ExpectMsg(new Transition(fsm, FsmState.TestStateTimeout, FsmState.Initial)); }); - ExpectNoMsg(TimeSpan.FromMilliseconds(50)); - return true; + ExpectNoMsg(50.Milliseconds()); }); - - //assert } [Fact] public void FSM_must_cancel_a_StateTimeout() { - //arrange - - //act - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => { - fsm.Tell(State.TestStateTimeout, Self); - fsm.Tell(new Cancel(), Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestStateTimeout), FSMSpecHelpers.TransitionStateExpector()); + fsm.Tell(FsmState.TestStateTimeout); + fsm.Tell(Cancel.Instance); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestStateTimeout)); ExpectMsg(); - ExpectMsg(new FSMBase.Transition(fsm, State.TestStateTimeout, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - ExpectNoMsg(TimeSpan.FromMilliseconds(50)); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestStateTimeout, FsmState.Initial)); + ExpectNoMsg(50.Milliseconds()); }); + } - //assert + [Fact] + public void FSM_must_cancel_a_StateTimeout_when_actor_is_stopped() + { + var stoppingActor = Sys.ActorOf(Props.Create()); + Sys.EventStream.Subscribe(TestActor, typeof(DeadLetter)); + stoppingActor.Tell(FsmState.TestStoppingActorStateTimeout); + + Within(400.Milliseconds(), () => + { + ExpectNoMsg(300.Milliseconds()); + }); } [Fact] public void FSM_must_allow_StateTimeout_override() { - //arrange - - //act //the timeout in state TestStateTimeout is 800ms, then it will change back to Initial - Within(TimeSpan.FromMilliseconds(400), () => + Within(400.Milliseconds(), () => { - fsm.Tell(State.TestStateTimeoutOverride, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestStateTimeout), FSMSpecHelpers.TransitionStateExpector()); - ExpectNoMsg(TimeSpan.FromMilliseconds(300)); - return true; + fsm.Tell(FsmState.TestStateTimeoutOverride); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestStateTimeout)); + ExpectNoMsg(300.Milliseconds()); }); - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => { - fsm.Tell(new Cancel(), Self); + fsm.Tell(Cancel.Instance); ExpectMsg(); - ExpectMsg(new FSMBase.Transition(fsm, State.TestStateTimeout, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestStateTimeout, FsmState.Initial)); }); - - //assert } [Fact] public void FSM_must_receive_single_shot_timer() { - //arrange - - //act - Within(TimeSpan.FromSeconds(2), () => + Within(2.Seconds(), () => { - Within(TimeSpan.FromMilliseconds(450), TimeSpan.FromSeconds(1), () => + Within(500.Milliseconds(), 1.Seconds(), () => { - fsm.Tell(State.TestSingleTimer, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestSingleTimer), FSMSpecHelpers.TransitionStateExpector()); + fsm.Tell(FsmState.TestSingleTimer); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestSingleTimer)); ExpectMsg(); - ExpectMsg(new FSMBase.Transition(fsm, State.TestSingleTimer, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestSingleTimer, FsmState.Initial)); }); - ExpectNoMsg(TimeSpan.FromMilliseconds(500)); - return true; + ExpectNoMsg(500.Milliseconds()); }); - - //assert } [Fact] @@ -132,102 +110,120 @@ public void FSM_must_resubmit_single_shot_timer() { Within(TimeSpan.FromSeconds(2.5), () => { - Within(TimeSpan.FromMilliseconds(450), TimeSpan.FromSeconds(1), () => + Within(500.Milliseconds(), 1.Seconds(), () => { - fsm.Tell(State.TestSingleTimerResubmit, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestSingleTimerResubmit), FSMSpecHelpers.TransitionStateExpector()); + fsm.Tell(FsmState.TestSingleTimerResubmit); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestSingleTimerResubmit)); ExpectMsg(); - return true; }); - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => { ExpectMsg(); - ExpectMsg(new FSMBase.Transition(fsm, State.TestSingleTimerResubmit, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestSingleTimerResubmit, FsmState.Initial)); }); - ExpectNoMsg(TimeSpan.FromMilliseconds(500)); - return true; + ExpectNoMsg(500.Milliseconds()); }); } [Fact] public void FSM_must_correctly_cancel_a_named_timer() { - fsm.Tell(State.TestCancelTimer, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestCancelTimer), FSMSpecHelpers.TransitionStateExpector()); - Within(TimeSpan.FromMilliseconds(1500), () => + fsm.Tell(FsmState.TestCancelTimer); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestCancelTimer)); + Within(500.Milliseconds(), () => { - fsm.Tell(new Tick(), Self); + fsm.Tell(Tick.Instance); ExpectMsg(); - return true; }); - Within(TimeSpan.FromMilliseconds(300), TimeSpan.FromSeconds(1), () => + Within(300.Milliseconds(), 1.Seconds(), () => { ExpectMsg(); - return true; }); - fsm.Tell(new Cancel(), Self); - ExpectMsg(new FSMBase.Transition(fsm, State.TestCancelTimer, State.Initial), FSMSpecHelpers.TransitionStateExpector(), TimeSpan.FromSeconds(1)); + fsm.Tell(Cancel.Instance); + ExpectMsg(new Transition(fsm, FsmState.TestCancelTimer, FsmState.Initial), 1.Seconds()); } [Fact] public void FSM_must_not_get_confused_between_named_and_state_timers() { - fsm.Tell(State.TestCancelStateTimerInNamedTimerMessage, Self); - fsm.Tell(new Tick(), Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestCancelStateTimerInNamedTimerMessage), FSMSpecHelpers.TransitionStateExpector()); - ExpectMsg(TimeSpan.FromMilliseconds(500)); - Task.Delay(TimeSpan.FromMilliseconds(200)); + fsm.Tell(FsmState.TestCancelStateTimerInNamedTimerMessage); + fsm.Tell(Tick.Instance); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestCancelStateTimerInNamedTimerMessage)); + ExpectMsg(500.Milliseconds()); + Task.Delay(200.Milliseconds()); Resume(fsm); - ExpectMsg(new FSMBase.Transition(fsm, State.TestCancelStateTimerInNamedTimerMessage, State.TestCancelStateTimerInNamedTimerMessage2), FSMSpecHelpers.TransitionStateExpector(), TimeSpan.FromMilliseconds(500)); - fsm.Tell(new Cancel(), Self); - Within(TimeSpan.FromMilliseconds(500), () => + ExpectMsg(new Transition(fsm, FsmState.TestCancelStateTimerInNamedTimerMessage, FsmState.TestCancelStateTimerInNamedTimerMessage2), 500.Milliseconds()); + fsm.Tell(Cancel.Instance); + Within(500.Milliseconds(), () => { ExpectMsg(); - ExpectMsg(new FSMBase.Transition(fsm, State.TestCancelStateTimerInNamedTimerMessage2, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestCancelStateTimerInNamedTimerMessage2, FsmState.Initial)); }); } - /// - /// receiveWhile is currently broken - /// [Fact] public void FSM_must_receive_and_cancel_a_repeated_timer() { - fsm.Tell(State.TestRepeatedTimer, Self); - ExpectMsg(new FSMBase.Transition(fsm, State.Initial, State.TestRepeatedTimer), FSMSpecHelpers.TransitionStateExpector()); - var seq = ReceiveWhile(TimeSpan.FromSeconds(2), o => + fsm.Tell(FsmState.TestRepeatedTimer); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestRepeatedTimer)); + var seq = ReceiveWhile(2.Seconds(), o => { - if(o is Tick) return o; + if (o is Tick) + return o; return null; }); - - Assert.Equal(5, seq.Count); - Within(TimeSpan.FromMilliseconds(500), () => + seq.Should().HaveCount(5); + Within(500.Milliseconds(), () => { - ExpectMsg(new FSMBase.Transition(fsm, State.TestRepeatedTimer, State.Initial), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, FsmState.TestRepeatedTimer, FsmState.Initial)); }); } + [Fact] + public void FSM_must_notify_unhandled_messages() + { + // EventFilter + // .Warning("unhandled event Akka.Tests.Actor.FSMTimingSpec+Tick in state TestUnhandled", source: fsm.Path.ToString()) + // .And + // .Warning("unhandled event Akka.Tests.Actor.FSMTimingSpec+Unhandled in state TestUnhandled", source: fsm.Path.ToString()) + // .ExpectOne( + // () => + // { + fsm.Tell(FsmState.TestUnhandled); + ExpectMsg(new Transition(fsm, FsmState.Initial, FsmState.TestUnhandled)); + Within(3.Seconds(), () => + { + fsm.Tell(Tick.Instance); + fsm.Tell(SetHandler.Instance); + fsm.Tell(Tick.Instance); + ExpectMsg().Msg.Should().BeOfType(); + fsm.Tell(new Unhandled("test")); + fsm.Tell(Cancel.Instance); + var transition = ExpectMsg>(); + transition.FsmRef.Should().Be(fsm); + transition.From.Should().Be(FsmState.TestUnhandled); + transition.To.Should().Be(FsmState.Initial); + }); + // }); + } + #region Actors static void Suspend(IActorRef actorRef) { - actorRef.Match() - .With(l => l.Suspend()); + var l = actorRef as ActorRefWithCell; + l?.Suspend(); } static void Resume(IActorRef actorRef) { - actorRef.Match() - .With(l => l.Resume()); + var l = actorRef as ActorRefWithCell; + l?.Resume(); } - public enum State + public enum FsmState { Initial, TestStateTimeout, @@ -238,245 +234,248 @@ public enum State TestUnhandled, TestCancelTimer, TestCancelStateTimerInNamedTimerMessage, - TestCancelStateTimerInNamedTimerMessage2 + TestCancelStateTimerInNamedTimerMessage2, + TestStoppingActorStateTimeout } - public abstract class DebugName + public class Tick { - public override string ToString() - { - return GetType().Name; - } + private Tick() { } + public static Tick Instance { get; } = new Tick(); + } + + public class Tock + { + private Tock() { } + public static Tock Instance { get; } = new Tock(); + } + + public class Cancel + { + private Cancel() { } + public static Cancel Instance { get; } = new Cancel(); + } + + public class SetHandler + { + private SetHandler() { } + public static SetHandler Instance { get; } = new SetHandler(); } - public class Tick : DebugName { } - public class Tock : DebugName { } - public class Cancel : DebugName { } - public class SetHandler : DebugName { } - public class Unhandled : DebugName + public class Unhandled { public Unhandled(object msg) { Msg = msg; } - public object Msg { get; private set; } + public object Msg { get; } } - public static void StaticAwaitCond(Func evaluator, TimeSpan max, TimeSpan? interval) { InternalAwaitCondition(evaluator, max, interval,(format,args)=> XAssert.Fail(string.Format(format,args))); } - - public class StateMachine : FSM, ILoggingFSM + public class StateMachine : FSM, ILoggingFSM { public StateMachine(IActorRef tester) { Tester = tester; - StartWith(State.Initial, 0); - When(State.Initial, @event => + StartWith(FsmState.Initial, 0); + + When(FsmState.Initial, @event => { - State nextState = null; - if (@event.FsmEvent is State) + if (@event.FsmEvent is FsmState) { - var s = (State) @event.FsmEvent; + var s = (FsmState)@event.FsmEvent; switch (s) { - case State.TestSingleTimer: - SetTimer("tester", new Tick(), TimeSpan.FromMilliseconds(500), false); - nextState = GoTo(State.TestSingleTimer); - break; - case State.TestRepeatedTimer: - SetTimer("tester", new Tick(), TimeSpan.FromMilliseconds(100), true); - nextState = GoTo(State.TestRepeatedTimer).Using(4); - break; - case State.TestStateTimeoutOverride: - nextState = GoTo(State.TestStateTimeout).ForMax(TimeSpan.MaxValue); - break; + case FsmState.TestSingleTimer: + SetTimer("tester", Tick.Instance, 500.Milliseconds(), false); + return GoTo(FsmState.TestSingleTimer); + case FsmState.TestRepeatedTimer: + SetTimer("tester", Tick.Instance, 100.Milliseconds(), true); + return GoTo(FsmState.TestRepeatedTimer).Using(4); + case FsmState.TestStateTimeoutOverride: + return GoTo(FsmState.TestStateTimeout).ForMax(TimeSpan.MaxValue); default: - nextState = GoTo(s); - break; + return GoTo(s); } } - return nextState; + return null; }); - When(State.TestStateTimeout, @event => + When(FsmState.TestStateTimeout, @event => { - State nextState = null; - @event.FsmEvent.Match() - .With(s => - { - nextState = GoTo(State.Initial); - }) - .With(c => - { - nextState = GoTo(State.Initial).Replying(new Cancel()); - }); - return nextState; - - }, TimeSpan.FromMilliseconds(800)); + if (@event.FsmEvent is StateTimeout) + { + return GoTo(FsmState.Initial); + } + else if (@event.FsmEvent is Cancel) + { + return GoTo(FsmState.Initial).Replying(Cancel.Instance); + } + return null; + }, 800.Milliseconds()); - When(State.TestSingleTimer, @event => + When(FsmState.TestSingleTimer, @event => { - State nextState = null; if (@event.FsmEvent is Tick) { - Tester.Tell(new Tick()); - nextState = GoTo(State.Initial); + Tester.Tell(Tick.Instance); + return GoTo(FsmState.Initial); } - return nextState; + return null; }); - OnTransition((state, state1) => + OnTransition((state1, state2) => { - if(state == State.Initial && state1 == State.TestSingleTimerResubmit) - SetTimer("blah", new Tick(), TimeSpan.FromMilliseconds(500)); + if (state1 == FsmState.Initial && state2 == FsmState.TestSingleTimerResubmit) + SetTimer("blah", Tick.Instance, 500.Milliseconds()); }); - When(State.TestSingleTimerResubmit, @event => + When(FsmState.TestSingleTimerResubmit, @event => { - State nextState = null; - @event.FsmEvent.Match() - .With(tick => - { - Tester.Tell(new Tick()); - SetTimer("blah", new Tock(), TimeSpan.FromMilliseconds(500)); - nextState = Stay(); - }) - .With(tock => - { - Tester.Tell(new Tock()); - nextState = GoTo(State.Initial); - }); - - return nextState; + if (@event.FsmEvent is Tick) + { + Tester.Tell(Tick.Instance); + SetTimer("blah", Tock.Instance, 500.Milliseconds()); + return Stay(); + } + else if (@event.FsmEvent is Tock) + { + Tester.Tell(Tock.Instance); + return GoTo(FsmState.Initial); + } + return null; }); - When(State.TestCancelTimer, @event => + When(FsmState.TestCancelTimer, @event => { - State nextState = null; - - @event.FsmEvent.Match() - .With(tick => - { - var contextLocal = Context; - var numberOfMessages = ((ActorCell) contextLocal).Mailbox.NumberOfMessages; - SetTimer("hallo", new Tock(), TimeSpan.FromMilliseconds(1)); - StaticAwaitCond(() => contextLocal.AsInstanceOf().Mailbox.HasMessages, TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(50)); - CancelTimer("hallo"); - Sender.Tell(new Tick()); - SetTimer("hallo", new Tock(), TimeSpan.FromMilliseconds(500)); - nextState = Stay(); - }) - .With(tock => - { - Tester.Tell(new Tock()); - nextState = Stay(); - }) - .With(c => - { - CancelTimer("hallo"); - nextState = GoTo(State.Initial); - }); - - return nextState; + if (@event.FsmEvent is Tick) + { + var contextLocal = Context.AsInstanceOf(); + SetTimer("hallo", Tock.Instance, 1.Milliseconds()); + StaticAwaitCond(() => contextLocal.Mailbox.HasMessages, 1.Seconds(), 50.Milliseconds()); + CancelTimer("hallo"); + Sender.Tell(Tick.Instance); + SetTimer("hallo", Tock.Instance, 500.Milliseconds()); + return Stay(); + } + else if (@event.FsmEvent is Tock) + { + Tester.Tell(Tock.Instance); + return Stay(); + } + else if (@event.FsmEvent is Cancel) + { + CancelTimer("hallo"); + return GoTo(FsmState.Initial); + } + return null; }); - When(State.TestRepeatedTimer, @event => + When(FsmState.TestRepeatedTimer, @event => { - State nextState = null; - if (@event.FsmEvent is Tick) { var remaining = @event.StateData; - Tester.Tell(new Tick()); + Tester.Tell(Tick.Instance); if (remaining == 0) { CancelTimer("tester"); - nextState = GoTo(State.Initial); + return GoTo(FsmState.Initial); } else { - nextState = Stay().Using(remaining - 1); + return Stay().Using(remaining - 1); } } - - return nextState; + return null; }); - When(State.TestCancelStateTimerInNamedTimerMessage, @event => + When(FsmState.TestCancelStateTimerInNamedTimerMessage, @event => { - //FSM is suspended after processing this message and resumed 500s later - State nextState = null; - - @event.FsmEvent.Match() - .With(tick => - { - Suspend(Self); - SetTimer("named", new Tock(), TimeSpan.FromMilliseconds(1)); - var contextLocal = Context; - StaticAwaitCond(() =>((ActorCell) contextLocal).Mailbox.HasMessages, - TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(50)); - nextState = Stay().ForMax(TimeSpan.FromMilliseconds(1)).Replying(new Tick()); - }) - .With(tock => - { - nextState = GoTo(State.TestCancelStateTimerInNamedTimerMessage2); - }); - - return nextState; + // FSM is suspended after processing this message and resumed 500s later + if (@event.FsmEvent is Tick) + { + Suspend(Self); + SetTimer("named", Tock.Instance, 1.Milliseconds()); + var contextLocal = Context.AsInstanceOf(); + StaticAwaitCond(() => contextLocal.Mailbox.HasMessages, 1.Seconds(), 50.Milliseconds()); + return Stay().ForMax(1.Milliseconds()).Replying(Tick.Instance); + } + else if (@event.FsmEvent is Tock) + { + return GoTo(FsmState.TestCancelStateTimerInNamedTimerMessage2); + } + return null; }); - When(State.TestCancelStateTimerInNamedTimerMessage2, @event => + When(FsmState.TestCancelStateTimerInNamedTimerMessage2, @event => { - State nextState = null; - - @event.FsmEvent.Match() - .With(s => - { - nextState = GoTo(State.Initial); - }) - .With(c => - { - nextState = GoTo(State.Initial).Replying(new Cancel()); - }); - - return nextState; + if (@event.FsmEvent is StateTimeout) + { + return GoTo(FsmState.Initial); + } + else if (@event.FsmEvent is Cancel) + { + return GoTo(FsmState.Initial).Replying(Cancel.Instance); + } + return null; }); - When(State.TestUnhandled, @event => + When(FsmState.TestUnhandled, @event => { - State nextState = null; - - @event.FsmEvent.Match() - .With(s => + if (@event.FsmEvent is SetHandler) + { + WhenUnhandled(evt => { - WhenUnhandled(@event1 => + if (evt.FsmEvent is Tick) { - Tester.Tell(new Unhandled(new Tick())); + Tester.Tell(new Unhandled(Tick.Instance)); return Stay(); - }); - nextState = Stay(); - }) - .With(c => - { - WhenUnhandled(@event1 => null); - nextState = GoTo(State.Initial); - }); + } - return nextState; + return null; + }); + return Stay(); + } + else if (@event.FsmEvent is Cancel) + { + // whenUnhandled(NullFunction) + return GoTo(FsmState.Initial); + } + return null; }); } + public IActorRef Tester { get; } + } + public class StoppingActor : FSM + { + public StoppingActor() + { + StartWith(FsmState.Initial, 0); - public IActorRef Tester { get; private set; } + When(FsmState.Initial, evt => + { + if (evt.FsmEvent is FsmState) + { + var state = (FsmState)evt.FsmEvent; + if (state == FsmState.TestStoppingActorStateTimeout) + { + Context.Stop(Self); + return Stay(); + } + } + return null; + }, 200.Milliseconds()); + } } #endregion } } - diff --git a/src/core/Akka.Tests/Actor/FSMTransitionSpec.cs b/src/core/Akka.Tests/Actor/FSMTransitionSpec.cs index a1656de3c8d..566c3b52513 100644 --- a/src/core/Akka.Tests/Actor/FSMTransitionSpec.cs +++ b/src/core/Akka.Tests/Actor/FSMTransitionSpec.cs @@ -8,123 +8,176 @@ using System; using Akka.Actor; using Akka.TestKit; +using FluentAssertions; using Xunit; +using static Akka.Actor.FSMBase; namespace Akka.Tests.Actor { - public class FSMTransitionSpec : AkkaSpec { - public IActorRef Self { get { return TestActor; } } + [Fact] + public void FSMTransitionNotifier_must_not_trigger_onTransition_for_stay() + { + var fsm = Sys.ActorOf(Props.Create(() => new SendAnyTransitionFSM(TestActor))); + ExpectMsg(new Tuple(0, 0)); // caused by initialize(), OK. + fsm.Tell("stay"); // no transition event + ExpectNoMsg(500.Milliseconds()); + fsm.Tell("goto"); // goto(current state) + ExpectMsg(new Tuple(0, 0)); + } - - [Fact] public void FSMTransitionNotifier_must_notify_listeners() { - //arrange var fsm = Sys.ActorOf(Props.Create(() => new MyFSM(TestActor))); - //act - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => { - fsm.Tell(new FSMBase.SubscribeTransitionCallBack(TestActor)); - ExpectMsg(new FSMBase.CurrentState(fsm, 0), FSMSpecHelpers.CurrentStateExpector()); + fsm.Tell(new SubscribeTransitionCallBack(TestActor)); + ExpectMsg(new CurrentState(fsm, 0)); fsm.Tell("tick"); - ExpectMsg(new FSMBase.Transition(fsm, 0, 1), FSMSpecHelpers.TransitionStateExpector()); + ExpectMsg(new Transition(fsm, 0, 1)); fsm.Tell("tick"); - ExpectMsg(new FSMBase.Transition(fsm, 1, 0), FSMSpecHelpers.TransitionStateExpector()); - return true; + ExpectMsg(new Transition(fsm, 1, 0)); }); - - //assert } [Fact] public void FSMTransitionNotifier_must_not_fail_when_listener_goes_away() { - //arrange var forward = Sys.ActorOf(Props.Create(() => new Forwarder(TestActor))); var fsm = Sys.ActorOf(Props.Create(() => new MyFSM(TestActor))); - //act - Within(TimeSpan.FromSeconds(1), async () => + Within(1.Seconds(), () => { - fsm.Tell(new FSMBase.SubscribeTransitionCallBack(forward)); - ExpectMsg(new FSMBase.CurrentState(fsm, 0), FSMSpecHelpers.CurrentStateExpector()); - await forward.GracefulStop(TimeSpan.FromSeconds(5)); + fsm.Tell(new SubscribeTransitionCallBack(forward)); + ExpectMsg(new CurrentState(fsm, 0)); + forward.GracefulStop(5.Seconds()).Wait(); fsm.Tell("tick"); - ExpectNoMsg(TimeSpan.FromMilliseconds(300)); - return true; + ExpectNoMsg(200.Milliseconds()); }); - - //assert } [Fact] public void FSM_must_make_previous_and_next_state_data_available_in_OnTransition() { - //arrange var fsm = Sys.ActorOf(Props.Create(() => new OtherFSM(TestActor))); - //act - Within(TimeSpan.FromSeconds(1), () => + Within(1.Seconds(), () => + { + fsm.Tell("tick"); + ExpectMsg(new Tuple(0, 1)); + }); + } + + [Fact] + public void FSM_must_trigger_transition_event_when_goto_the_same_state() + { + var forward = Sys.ActorOf(Props.Create(() => new Forwarder(TestActor))); + var fsm = Sys.ActorOf(Props.Create(() => new OtherFSM(TestActor))); + + Within(1.Seconds(), () => { + fsm.Tell(new SubscribeTransitionCallBack(forward)); + ExpectMsg(new CurrentState(fsm, 0)); fsm.Tell("tick"); ExpectMsg(new Tuple(0, 1)); - return true; + ExpectMsg(new Transition(fsm, 0, 1)); + fsm.Tell("tick"); + ExpectMsg(new Tuple(1, 1)); + ExpectMsg(new Transition(fsm, 1, 1)); }); + } + + [Fact] + public void FSM_must_not_trigger_transition_event_on_stay() + { + var forward = Sys.ActorOf(Props.Create(() => new Forwarder(TestActor))); + var fsm = Sys.ActorOf(Props.Create(() => new OtherFSM(TestActor))); - //assert + Within(1.Seconds(), () => + { + fsm.Tell(new SubscribeTransitionCallBack(forward)); + ExpectMsg(new CurrentState(fsm, 0)); + fsm.Tell("stay"); + ExpectNoMsg(500.Milliseconds()); + }); } [Fact] public void FSM_must_not_leak_memory_in_nextState() { - //arrange var fsmref = Sys.ActorOf(); - //act - fsmref.Tell("switch", Self); + fsmref.Tell("switch"); ExpectMsg(Tuple.Create(0, 1)); - fsmref.Tell("test", Self); + fsmref.Tell("test"); ExpectMsg("ok"); - - //assert } #region Test actors + public class SendAnyTransitionFSM : FSM + { + public SendAnyTransitionFSM(IActorRef target) + { + Target = target; + + StartWith(0, 0); + + When(0, @event => + { + if (@event.FsmEvent.Equals("stay")) + return Stay(); + else + return GoTo(0); + }); + + OnTransition((state1, state2) => + { + Target.Tell(Tuple.Create(state1, state2)); + }); + + Initialize(); + } + + public IActorRef Target { get; } + } + public class MyFSM : FSM { public MyFSM(IActorRef target) { Target = target; + StartWith(0, new object()); + When(0, @event => { - if (@event.FsmEvent.Equals("tick")) return GoTo(1); + if (@event.FsmEvent.Equals("tick")) + return GoTo(1); return null; }); When(1, @event => { - if (@event.FsmEvent.Equals("tick")) return GoTo(0); + if (@event.FsmEvent.Equals("tick")) + return GoTo(0); return null; }); WhenUnhandled(@event => { - if (@event.FsmEvent.Equals("reply")) return Stay().Replying("reply"); + if (@event.FsmEvent.Equals("reply")) + return Stay().Replying("reply"); return null; }); Initialize(); } - - - public IActorRef Target { get; private set; } + public IActorRef Target { get; } protected override void PreRestart(Exception reason, object message) { @@ -137,21 +190,31 @@ public class OtherFSM : FSM public OtherFSM(IActorRef target) { Target = target; + StartWith(0, 0); + When(0, @event => { if (@event.FsmEvent.Equals("tick")) { return GoTo(1).Using(1); } + else if (@event.FsmEvent.Equals("stay")) + { + return Stay(); + } return null; }); - When(1, @event => Stay()); + When(1, @event => GoTo(1)); - OnTransition((state, i) => + OnTransition((state1, state2) => { - if (state == 0 && i == 1) target.Tell(Tuple.Create(StateData, NextStateData)); + if (state1 == 0 && state2 == 1) + target.Tell(Tuple.Create(StateData, NextStateData)); + + if (state1 == 1 && state2 == 1) + target.Tell(Tuple.Create(StateData, NextStateData)); }); } @@ -163,6 +226,7 @@ public class LeakyFSM : FSM public LeakyFSM() { StartWith(0, null); + When(0, @event => { if (@event.FsmEvent.Equals("switch")) @@ -173,9 +237,9 @@ public LeakyFSM() return null; }); - OnTransition((state, i) => + OnTransition((state1, state2) => { - NextStateData.Tell(Tuple.Create(state, i)); + NextStateData.Tell(Tuple.Create(state1, state2)); }); When(1, @event => @@ -184,7 +248,7 @@ public LeakyFSM() { try { - Sender.Tell(string.Format("failed: {0}", NextStateData)); + Sender.Tell($"failed: {NextStateData}"); } catch (InvalidOperationException) { @@ -205,7 +269,7 @@ public Forwarder(IActorRef target) Target = target; } - public IActorRef Target { get; private set; } + public IActorRef Target { get; } protected override void OnReceive(object message) { diff --git a/src/core/Akka.Tests/Akka.Tests.csproj b/src/core/Akka.Tests/Akka.Tests.csproj index dbde642b1cd..384ac447c96 100644 --- a/src/core/Akka.Tests/Akka.Tests.csproj +++ b/src/core/Akka.Tests/Akka.Tests.csproj @@ -127,6 +127,7 @@ + diff --git a/src/core/Akka/Actor/FSM.cs b/src/core/Akka/Actor/FSM.cs index d2e65b5d33f..37e967be9d6 100644 --- a/src/core/Akka/Actor/FSM.cs +++ b/src/core/Akka/Actor/FSM.cs @@ -9,9 +9,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using Akka.Actor.Internal; -using Akka.Dispatch.SysMsg; using Akka.Event; using Akka.Pattern; using Akka.Routing; @@ -31,10 +29,10 @@ public abstract class FSMBase : ActorBase /// before sending any messages. /// /// The type of the state being used in this finite state machine. - public class CurrentState + public sealed class CurrentState { /// - /// TBD + /// Initializes a new instance of the CurrentState /// /// TBD /// TBD @@ -47,12 +45,39 @@ public CurrentState(IActorRef fsmRef, TS state) /// /// TBD /// - public IActorRef FsmRef { get; private set; } + public IActorRef FsmRef { get; } /// /// TBD /// - public TS State { get; private set; } + public TS State { get; } + + #region Equality + private bool Equals(CurrentState other) + { + return Equals(FsmRef, other.FsmRef) && EqualityComparer.Default.Equals(State, other.State); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is CurrentState && Equals((CurrentState)obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((FsmRef != null ? FsmRef.GetHashCode() : 0) * 397) ^ EqualityComparer.Default.GetHashCode(State); + } + } + + public override string ToString() + { + return $"CurrentState <{State}>"; + } + #endregion } /// @@ -60,35 +85,59 @@ public CurrentState(IActorRef fsmRef, TS state) /// (use ) /// /// The type of state used - public class Transition + public sealed class Transition { /// - /// TBD + /// Initializes a new instance of the Transition /// /// TBD /// TBD /// TBD - public Transition(IActorRef fsmRef, TS @from, TS to) + public Transition(IActorRef fsmRef, TS from, TS to) { To = to; - From = @from; + From = from; FsmRef = fsmRef; } /// /// TBD /// - public IActorRef FsmRef { get; private set; } + public IActorRef FsmRef { get; } /// /// TBD /// - public TS From { get; private set; } + public TS From { get; } /// /// TBD /// - public TS To { get; private set; } + public TS To { get; } + + #region Equality + private bool Equals(Transition other) + { + return Equals(FsmRef, other.FsmRef) && EqualityComparer.Default.Equals(From, other.From) && EqualityComparer.Default.Equals(To, other.To); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Transition && Equals((Transition)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (FsmRef != null ? FsmRef.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(From); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(To); + return hashCode; + } + } /// /// TBD @@ -96,8 +145,10 @@ public Transition(IActorRef fsmRef, TS @from, TS to) /// TBD public override string ToString() { - return String.Format("Transition({0}, {1})", From, To); + return $"Transition({From}, {To})"; } + + #endregion } /// @@ -105,10 +156,10 @@ public override string ToString() /// followed by a series of updates. Cancel the subscription using /// . /// - public class SubscribeTransitionCallBack + public sealed class SubscribeTransitionCallBack { /// - /// TBD + /// Initializes a new instance of the SubscribeTransitionCallBack /// /// TBD public SubscribeTransitionCallBack(IActorRef actorRef) @@ -119,17 +170,17 @@ public SubscribeTransitionCallBack(IActorRef actorRef) /// /// TBD /// - public IActorRef ActorRef { get; private set; } + public IActorRef ActorRef { get; } } /// /// Unsubscribe from notifications which were /// initialized by sending the corresponding . /// - public class UnsubscribeTransitionCallBack + public sealed class UnsubscribeTransitionCallBack { /// - /// TBD + /// Initializes a new instance of the UnsubscribeTransitionCallBack /// /// TBD public UnsubscribeTransitionCallBack(IActorRef actorRef) @@ -140,7 +191,7 @@ public UnsubscribeTransitionCallBack(IActorRef actorRef) /// /// TBD /// - public IActorRef ActorRef { get; private set; } + public IActorRef ActorRef { get; } } /// @@ -151,23 +202,45 @@ public abstract class Reason { } /// /// Default if calling Stop(). /// - public class Normal : Reason { } + public sealed class Normal : Reason + { + [Obsolete("This constructor is obsoleted. Use Normal.Instance [1.2.0]")] + public Normal() { } + + /// + /// Singleton instance of Normal + /// +#pragma warning disable 618 + public static Normal Instance { get; } = new Normal(); +#pragma warning restore 618 + } /// - /// Reason given when someone as calling from outside; + /// Reason given when someone as calling from outside; /// also applies to supervision directive. /// - public class Shutdown : Reason { } + public sealed class Shutdown : Reason + { + [Obsolete("This constructor is obsoleted. Use Shutdown.Instance [1.2.0]")] + public Shutdown() { } + + /// + /// Singleton instance of Shutdown + /// +#pragma warning disable 618 + public static Shutdown Instance { get; } = new Shutdown(); +#pragma warning restore 618 + } /// /// Signifies that the is shutting itself down because of an error, /// e.g. if the state to transition into does not exist. You can use this to communicate a more /// precise cause to the block. /// - public class Failure : Reason + public sealed class Failure : Reason { /// - /// TBD + /// Initializes a new instance of the Failure /// /// TBD public Failure(object cause) @@ -178,22 +251,29 @@ public Failure(object cause) /// /// TBD /// - public object Cause { get; private set; } + public object Cause { get; } } /// /// Used in the event of a timeout between transitions /// - public class StateTimeout { } + public sealed class StateTimeout + { + [Obsolete("This constructor is obsoleted. Use Shutdown.Instance [1.2.0]")] + public StateTimeout() { } - /* - * INTERNAL API - used for ensuring that state changes occur on-time - */ + /// + /// Singleton instance of StateTimeout + /// +#pragma warning disable 618 + public static StateTimeout Instance { get; } = new StateTimeout(); +#pragma warning restore 618 + } /// - /// TBD + /// INTERNAL API /// - internal class TimeoutMarker + internal sealed class TimeoutMarker { /// /// TBD @@ -207,17 +287,18 @@ public TimeoutMarker(long generation) /// /// TBD /// - public long Generation { get; private set; } + public long Generation { get; } } /// - /// TBD + /// INTERNAL API /// - [DebuggerDisplay("Timer {Name,nq}, message: {Message")] + [DebuggerDisplay("Timer {Name,nq}, message: {Message}")] internal class Timer : INoSerializationVerificationNeeded { - private readonly ILoggingAdapter _debugLog; - + private ICancelable _ref; + private readonly IScheduler _scheduler; + /// /// TBD /// @@ -226,47 +307,41 @@ internal class Timer : INoSerializationVerificationNeeded /// TBD /// TBD /// TBD - /// TBD - public Timer(string name, object message, bool repeat, int generation, IActorContext context, ILoggingAdapter debugLog) + public Timer(string name, object message, bool repeat, int generation, IActorContext context) { - _debugLog = debugLog; Context = context; Generation = generation; Repeat = repeat; Message = message; Name = name; - var scheduler = context.System.Scheduler; - _scheduler = scheduler; - _ref = new Cancelable(scheduler); - } - private readonly IScheduler _scheduler; - private readonly ICancelable _ref; + _scheduler = context.System.Scheduler; + } /// /// TBD /// - public string Name { get; private set; } + public string Name { get; } /// /// TBD /// - public object Message { get; private set; } + public object Message { get; } /// /// TBD /// - public bool Repeat { get; private set; } + public bool Repeat { get; } /// /// TBD /// - public int Generation { get; private set; } + public int Generation { get; } /// /// TBD /// - public IActorContext Context { get; private set; } + public IActorContext Context { get; } /// /// TBD @@ -275,21 +350,9 @@ public Timer(string name, object message, bool repeat, int generation, IActorCon /// TBD public void Schedule(IActorRef actor, TimeSpan timeout) { - var name = Name; - var message = Message; - - Action send; - if(_debugLog != null) - send = () => - { - _debugLog.Debug("{0}Timer '{1}' went off. Sending {2} -> {3}",_ref.IsCancellationRequested ? "Cancelled " : "", name, message, actor); - actor.Tell(this, Context.Self); - }; - else - send = () => actor.Tell(this, Context.Self); - - if(Repeat) _scheduler.Advanced.ScheduleRepeatedly(timeout, timeout, send, _ref); - else _scheduler.Advanced.ScheduleOnce(timeout, send, _ref); + _ref = Repeat + ? _scheduler.ScheduleTellRepeatedlyCancelable(timeout, timeout, actor, this, Context.Self) + : _scheduler.ScheduleTellOnceCancelable(timeout, actor, this, Context.Self); } /// @@ -297,9 +360,10 @@ public void Schedule(IActorRef actor, TimeSpan timeout) /// public void Cancel() { - if (!_ref.IsCancellationRequested) + if (_ref != null) { _ref.Cancel(false); + _ref = null; } } } @@ -309,10 +373,10 @@ public void Cancel() /// /// The name of the state /// The data of the state - public class LogEntry + public sealed class LogEntry { /// - /// TBD + /// Initializes a new instance of the LogEntry /// /// TBD /// TBD @@ -327,17 +391,26 @@ public LogEntry(TS stateName, TD stateData, object fsmEvent) /// /// TBD /// - public TS StateName { get; private set; } + public TS StateName { get; } /// /// TBD /// - public TD StateData { get; private set; } + public TD StateData { get; } /// /// TBD /// - public object FsmEvent { get; private set; } + public object FsmEvent { get; } + + /// + /// TBD + /// + /// TBD + public override string ToString() + { + return $"StateName: <{StateName}>, StateData: <{StateData}>, FsmEvent: <{FsmEvent}>"; + } } /// @@ -350,46 +423,52 @@ public LogEntry(TS stateName, TD stateData, object fsmEvent) public class State : IEquatable> { /// - /// TBD + /// Initializes a new instance of the State /// /// TBD /// TBD /// TBD /// TBD /// TBD - public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopReason = null, List replies = null) + public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopReason = null, IReadOnlyList replies = null, bool notifies = true) { Replies = replies ?? new List(); StopReason = stopReason; Timeout = timeout; StateData = stateData; StateName = stateName; + Notifies = notifies; } /// /// TBD /// - public TS StateName { get; private set; } + public TS StateName { get; } + + /// + /// TBD + /// + public TD StateData { get; } /// /// TBD /// - public TD StateData { get; private set; } + public TimeSpan? Timeout { get; } /// /// TBD /// - public TimeSpan? Timeout { get; private set; } + public Reason StopReason { get; } /// /// TBD /// - public Reason StopReason { get; private set; } + public IReadOnlyList Replies { get; protected set; } /// /// TBD /// - public List Replies { get; protected set; } + internal bool Notifies { get; } /// /// TBD @@ -398,9 +477,9 @@ public State(TS stateName, TD stateData, TimeSpan? timeout = null, Reason stopRe /// TBD /// TBD /// TBD - public State Copy(TimeSpan? timeout, Reason stopReason = null, List replies = null) + internal State Copy(TimeSpan? timeout, Reason stopReason = null, IReadOnlyList replies = null) { - return new State(StateName, StateData, timeout, stopReason ?? StopReason, replies ?? Replies); + return new State(StateName, StateData, timeout, stopReason ?? StopReason, replies ?? Replies, Notifies); } /// @@ -412,7 +491,8 @@ public State Copy(TimeSpan? timeout, Reason stopReason = null, ListTBD public State ForMax(TimeSpan timeout) { - if (timeout <= TimeSpan.MaxValue) return Copy(timeout); + if (timeout <= TimeSpan.MaxValue) + return Copy(timeout); return Copy(timeout: null); } @@ -423,9 +503,10 @@ public State ForMax(TimeSpan timeout) /// TBD public State Replying(object replyValue) { - if (Replies == null) Replies = new List(); - var newReplies = Replies.ToArray().ToList(); + var newReplies = new List(Replies.Count + 1); newReplies.Add(replyValue); + newReplies.AddRange(Replies); + return Copy(Timeout, replies: newReplies); } @@ -437,7 +518,7 @@ public State Replying(object replyValue) /// TBD public State Using(TD nextStateData) { - return new State(StateName, nextStateData, Timeout, StopReason, Replies); + return new State(StateName, nextStateData, Timeout, StopReason, Replies, Notifies); } /// @@ -450,13 +531,21 @@ internal State WithStopReason(Reason reason) return Copy(Timeout, reason); } + /// + /// INTERNAL API + /// + internal State WithNotification(bool notifies) + { + return new State(StateName, StateData, Timeout, StopReason, Replies, notifies); + } + /// /// TBD /// /// TBD public override string ToString() { - return StateName + ", " + StateData; + return $"{StateName}, {StateData}"; } /// @@ -466,8 +555,8 @@ public override string ToString() /// TBD public bool Equals(State other) { - if(ReferenceEquals(null, other)) return false; - if(ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; return EqualityComparer.Default.Equals(StateName, other.StateName) && EqualityComparer.Default.Equals(StateData, other.StateData) && Timeout.Equals(other.Timeout) && Equals(StopReason, other.StopReason) && Equals(Replies, other.Replies); } @@ -478,9 +567,9 @@ public bool Equals(State other) /// TBD public override bool Equals(object obj) { - if(ReferenceEquals(null, obj)) return false; - if(ReferenceEquals(this, obj)) return true; - if(obj.GetType() != GetType()) return false; + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; return Equals((State)obj); } @@ -507,10 +596,10 @@ public override int GetHashCode() /// which allows pattern matching to extract both state and data. /// /// The state data for this event - public class Event : INoSerializationVerificationNeeded + public sealed class Event : INoSerializationVerificationNeeded { /// - /// TBD + /// Initializes a new instance of the Event /// /// TBD /// TBD @@ -523,12 +612,12 @@ public Event(object fsmEvent, TD stateData) /// /// TBD /// - public object FsmEvent { get; private set; } + public object FsmEvent { get; } /// /// TBD /// - public TD StateData { get; private set; } + public TD StateData { get; } /// /// TBD @@ -536,7 +625,7 @@ public Event(object fsmEvent, TD stateData) /// TBD public override string ToString() { - return "Event: <" + FsmEvent + ">, StateData: <" + StateData + ">"; + return $"Event: <{FsmEvent}>, StateData: <{StateData}>"; } } @@ -545,10 +634,10 @@ public override string ToString() /// /// TBD /// TBD - public class StopEvent : INoSerializationVerificationNeeded + public sealed class StopEvent : INoSerializationVerificationNeeded { /// - /// TBD + /// Initializes a new instance of the StopEvent /// /// TBD /// TBD @@ -563,17 +652,26 @@ public StopEvent(Reason reason, TS terminatedState, TD stateData) /// /// TBD /// - public Reason Reason { get; private set; } + public Reason Reason { get; } + + /// + /// TBD + /// + public TS TerminatedState { get; } /// /// TBD /// - public TS TerminatedState { get; private set; } + public TD StateData { get; } /// /// TBD /// - public TD StateData { get; private set; } + /// TBD + public override string ToString() + { + return $"Reason: <{Reason}>, TerminatedState: <{TerminatedState}>, StateData: <{StateData}>"; + } } #endregion @@ -589,26 +687,26 @@ public abstract class FSM : FSMBase, IListeners, IInternalSupport private readonly ILoggingAdapter _log = Context.GetLogger(); /// - /// TBD + /// Initializes a new instance of the FSM class. /// protected FSM() { - if(this is ILoggingFSM) + if (this is ILoggingFSM) DebugEvent = Context.System.Settings.FsmDebugEvent; } /// - /// TBD + /// Delegate describing this state's response to input /// /// TBD /// TBD public delegate State StateFunction(Event fsmEvent); /// - /// TBD + /// Handler which is called upon each state transition /// - /// TBD - /// TBD + /// State designator for the initial state + /// State designator for the next state public delegate void TransitionHandler(TState initialState, TState nextState); #region Finite State Machine Domain Specific Language (FSM DSL if you like acronyms) @@ -650,13 +748,7 @@ public State GoTo(TState nextStateName) return new State(nextStateName, _currentState.StateData); } - /// - /// Produce transition to other state. Return this from a state function - /// in order to effect the transition. - /// - /// State designator for the next state - /// Data for next state - /// State transition descriptor + [Obsolete("This method is obsoleted. Use GoTo(nextStateName).Using(newStateData) [1.2.0]")] public State GoTo(TState nextStateName, TData stateData) { return new State(nextStateName, stateData); @@ -669,34 +761,34 @@ public State GoTo(TState nextStateName, TData stateData) /// Descriptor for staying in the current state. public State Stay() { - return GoTo(_currentState.StateName); + return GoTo(_currentState.StateName).WithNotification(false); } /// /// Produce change descriptor to stop this FSM actor with /// - /// TBD + /// Descriptor for stopping in the current state. public State Stop() { - return Stop(new Normal()); + return Stop(Normal.Instance); } /// /// Produce change descriptor to stop this FSM actor with the specified . /// - /// TBD - /// TBD + /// Reason why this is shutting down. + /// Descriptor for stopping in the current state. public State Stop(Reason reason) { return Stop(reason, _currentState.StateData); } /// - /// TBD + /// Produce change descriptor to stop this FSM actor with the specified . /// - /// TBD - /// TBD - /// TBD + /// Reason why this is shutting down. + /// State data. + /// Descriptor for stopping in the current state. public State Stop(Reason reason, TData stateData) { return Stay().Using(stateData).WithStopReason(reason); @@ -719,7 +811,7 @@ public TransformHelper(StateFunction func) /// /// TBD /// - public StateFunction Func { get; private set; } + public StateFunction Func { get; } /// /// TBD @@ -733,6 +825,13 @@ public StateFunction Using(Func, State> andT } } + /// + /// TBD + /// + /// TBD + /// TBD + public TransformHelper Transform(StateFunction func) => new TransformHelper(func); + /// /// Schedule named timer to deliver message after given delay, possibly repeating. /// Any existing timer with the same name will automatically be canceled before adding @@ -744,28 +843,29 @@ public StateFunction Using(Func, State> andT /// send once if false, scheduleAtFixedRate if true public void SetTimer(string name, object msg, TimeSpan timeout, bool repeat = false) { - if(DebugEvent) - _log.Debug("setting " + (repeat ? "repeating" : "") + "timer '{0}' / {1}: {2}", name, timeout, msg); - if(_timers.ContainsKey(name)) + if (DebugEvent) + { + _log.Debug($"setting {(repeat ? "repeating" : "")} timer {name}/{timeout}: {msg}"); + } + if (_timers.ContainsKey(name)) + { _timers[name].Cancel(); - var timer = new Timer(name, msg, repeat, _timerGen.Next(), Context, DebugEvent ? _log : null); - timer.Schedule(Self, timeout); + } - if (!_timers.ContainsKey(name)) - _timers.Add(name, timer); - else - _timers[name] = timer; + var timer = new Timer(name, msg, repeat, _timerGen.Next(), Context); + timer.Schedule(Self, timeout); + _timers[name] = timer; } /// - /// Cancel a named , ensuring that the message is not subsequently delivered (no race.) + /// Cancel a named , ensuring that the message is not subsequently delivered (no race.) /// /// The name of the timer to cancel. public void CancelTimer(string name) { if (DebugEvent) { - _log.Debug("Cancelling timer {0}", name); + _log.Debug($"Cancelling timer {name}"); } if (_timers.ContainsKey(name)) @@ -795,24 +895,14 @@ public bool IsTimerActive(string name) /// TBD public void SetStateTimeout(TState state, TimeSpan? timeout) { - if(!_stateTimeouts.ContainsKey(state)) - _stateTimeouts.Add(state, timeout); - else - _stateTimeouts[state] = timeout; + _stateTimeouts[state] = timeout; } - //Internal API - bool IInternalSupportsTestFSMRef.IsStateTimerActive - { - get - { - return _timeoutFuture != null; - } - } + // Internal API + bool IInternalSupportsTestFSMRef.IsStateTimerActive => _timeoutFuture != null; /// - /// Set handler which is called upon each state transition, i.e. not when - /// staying in the same state. + /// Set handler which is called upon each state transition /// /// TBD public void OnTransition(TransitionHandler transitionHandler) @@ -856,6 +946,9 @@ public void Initialize() /// /// Current state name /// + /// + /// This exception is thrown if this property is accessed before was called. + /// public TState StateName { get @@ -869,6 +962,9 @@ public TState StateName /// /// Current state data /// + /// + /// This exception is thrown if this property is accessed before was called. + /// public TData StateData { get @@ -889,27 +985,20 @@ public TData NextStateData { get { - if(_nextState == null) throw new InvalidOperationException("NextStateData is only available during OnTransition"); - return _nextState.StateData; + if (_nextState != null) + return _nextState.StateData; + throw new InvalidOperationException("NextStateData is only available during OnTransition"); } } - /// - /// TBD - /// - /// TBD - /// TBD - public TransformHelper Transform(StateFunction func) { return new TransformHelper(func); } - #endregion #region Internal implementation details - private readonly ListenerSupport _listener = new ListenerSupport(); /// - /// TBD + /// Retrieves the support needed to interact with an actor's listeners. /// - public ListenerSupport Listeners { get { return _listener; } } + public ListenerSupport Listeners { get; } = new ListenerSupport(); /// /// Can be set to enable debugging on certain actions taken by the FSM @@ -1017,63 +1106,23 @@ private static StateFunction OrElse(StateFunction original, StateFunction fallba #region Actor methods - /// - /// Main actor receive method - /// - /// TBD - /// TBD + /// protected override bool Receive(object message) { - var match = PatternMatch.Match(message) - .With(marker => - { - if (_generation == marker.Generation) - { - ProcessMsg(new StateTimeout(), "state timeout"); - } - }) - .With(t => - { - if (_timers.ContainsKey(t.Name) && _timers[t.Name].Generation == t.Generation) - { - if (_timeoutFuture != null) - { - _timeoutFuture.Cancel(false); - _timeoutFuture = null; - } - _generation++; - if (!t.Repeat) - { - _timers.Remove(t.Name); - } - ProcessMsg(t.Message,t); - } - }) - .With(cb => - { - Context.Watch(cb.ActorRef); - Listeners.Add(cb.ActorRef); - //send the current state back as a reference point - cb.ActorRef.Tell(new CurrentState(Self, _currentState.StateName)); - }) - .With(l => - { - Context.Watch(l.Listener); - Listeners.Add(l.Listener); - l.Listener.Tell(new CurrentState(Self, _currentState.StateName)); - }) - .With(ucb => - { - Context.Unwatch(ucb.ActorRef); - Listeners.Remove(ucb.ActorRef); - }) - .With(d => + var timeoutMarker = message as TimeoutMarker; + if (timeoutMarker != null) + { + if (_generation == timeoutMarker.Generation) { - Context.Unwatch(d.Listener); - Listeners.Remove(d.Listener); - }) - .With(_=> { DebugEvent = true; }) - .Default(msg => + ProcessMsg(StateTimeout.Instance, "state timeout"); + } + return true; + } + + var timer = message as Timer; + if (timer != null) + { + if (_timers.ContainsKey(timer.Name) && _timers[timer.Name].Generation == timer.Generation) { if (_timeoutFuture != null) { @@ -1081,9 +1130,58 @@ protected override bool Receive(object message) _timeoutFuture = null; } _generation++; - ProcessMsg(msg, Sender); - }); - return match.WasHandled; + if (!timer.Repeat) + { + _timers.Remove(timer.Name); + } + ProcessMsg(timer.Message, timer); + } + return true; + } + + var subscribeTransitionCallBack = message as SubscribeTransitionCallBack; + if (subscribeTransitionCallBack != null) + { + Context.Watch(subscribeTransitionCallBack.ActorRef); + Listeners.Add(subscribeTransitionCallBack.ActorRef); + //send the current state back as a reference point + subscribeTransitionCallBack.ActorRef.Tell(new CurrentState(Self, _currentState.StateName)); + return true; + } + + var listen = message as Listen; + if (listen != null) + { + Context.Watch(listen.Listener); + Listeners.Add(listen.Listener); + listen.Listener.Tell(new CurrentState(Self, _currentState.StateName)); + return true; + } + + var unsubscribeTransitionCallBack = message as UnsubscribeTransitionCallBack; + if (unsubscribeTransitionCallBack != null) + { + Context.Unwatch(unsubscribeTransitionCallBack.ActorRef); + Listeners.Remove(unsubscribeTransitionCallBack.ActorRef); + return true; + } + + var deafen = message as Deafen; + if (deafen != null) + { + Context.Unwatch(deafen.Listener); + Listeners.Remove(deafen.Listener); + return true; + } + + if (_timeoutFuture != null) + { + _timeoutFuture.Cancel(false); + _timeoutFuture = null; + } + _generation++; + ProcessMsg(message, Sender); + return true; } private void ProcessMsg(object any, object source) @@ -1094,40 +1192,49 @@ private void ProcessMsg(object any, object source) private void ProcessEvent(Event fsmEvent, object source) { - if(DebugEvent) + if (DebugEvent) { var srcStr = GetSourceString(source); - _log.Debug("processing {0} from {1}", fsmEvent, srcStr); + _log.Debug("processing {0} from {1} in state {2}", fsmEvent, srcStr, StateName); } + var stateFunc = _stateFunctions[_currentState.StateName]; var oldState = _currentState; - State upcomingState = null; - if(stateFunc != null) + State nextState = null; + + if (stateFunc != null) { - upcomingState = stateFunc(fsmEvent); + nextState = stateFunc(fsmEvent); } - if(upcomingState == null) + if (nextState == null) { - upcomingState = HandleEvent(fsmEvent); + nextState = HandleEvent(fsmEvent); } - ApplyState(upcomingState); - if(DebugEvent && !Equals(oldState, upcomingState)) + ApplyState(nextState); + + if (DebugEvent && !Equals(oldState, nextState)) { - _log.Debug("transition {0} -> {1}", oldState, upcomingState); + _log.Debug("transition {0} -> {1}", oldState, nextState); } } private string GetSourceString(object source) { var s = source as string; - if(s != null) return s; + if (s != null) + return s; + var timer = source as Timer; - if(timer != null) return "timer '" + timer.Name + "'"; + if (timer != null) + return "timer '" + timer.Name + "'"; + var actorRef = source as IActorRef; - if(actorRef != null) return actorRef.ToString(); + if (actorRef != null) + return actorRef.ToString(); + return "unknown"; } @@ -1137,43 +1244,44 @@ void IInternalSupportsTestFSMRef.ApplyState(State ApplyState(upcomingState); } - private void ApplyState(State upcomingState) + private void ApplyState(State nextState) { - if (upcomingState.StopReason == null){ MakeTransition(upcomingState); - return; + if (nextState.StopReason == null) + { + MakeTransition(nextState); } - var replies = upcomingState.Replies; - replies.Reverse(); - foreach (var reply in replies) + else { - Sender.Tell(reply); + for (int i = nextState.Replies.Count - 1; i >= 0; i--) + { + Sender.Tell(nextState.Replies[i]); + } + Terminate(nextState); + Context.Stop(Self); } - Terminate(upcomingState); - Context.Stop(Self); } - private void MakeTransition(State upcomingState) + private void MakeTransition(State nextState) { - if (!_stateFunctions.ContainsKey(upcomingState.StateName)) + if (!_stateFunctions.ContainsKey(nextState.StateName)) { - Terminate( - Stay() - .WithStopReason( - new Failure(String.Format("Next state {0} does not exist", upcomingState.StateName)))); + Terminate(Stay().WithStopReason(new Failure($"Next state {nextState.StateName} does not exist"))); } else { - var replies = upcomingState.Replies; - replies.Reverse(); - foreach (var r in replies) { Sender.Tell(r); } - if (!_currentState.StateName.Equals(upcomingState.StateName)) + for (int i = nextState.Replies.Count - 1; i >= 0; i--) + { + Sender.Tell(nextState.Replies[i]); + } + if (!_currentState.StateName.Equals(nextState.StateName) || nextState.Notifies) { - _nextState = upcomingState; - HandleTransition(_currentState.StateName, _nextState.StateName); - Listeners.Gossip(new Transition(Self, _currentState.StateName, _nextState.StateName)); + _nextState = nextState; + HandleTransition(_currentState.StateName, nextState.StateName); + Listeners.Gossip(new Transition(Self, _currentState.StateName, nextState.StateName)); _nextState = null; } - _currentState = upcomingState; + _currentState = nextState; + var timeout = _currentState.Timeout ?? _stateTimeouts[_currentState.StateName]; if (timeout.HasValue) { @@ -1192,8 +1300,12 @@ private void Terminate(State upcomingState) { var reason = upcomingState.StopReason; LogTermination(reason); - foreach (var t in _timers) { t.Value.Cancel(); } + foreach (var t in _timers) + { + t.Value.Cancel(); + } _timers.Clear(); + _timeoutFuture?.Cancel(); _currentState = upcomingState; var stopEvent = new StopEvent(reason, _currentState.StateName, _currentState.StateData); @@ -1214,7 +1326,7 @@ protected override void PostStop() * Setting this instance's state to Terminated does no harm during restart, since * the new instance will initialize fresh using StartWith. */ - Terminate(Stay().WithStopReason(new Shutdown())); + Terminate(Stay().WithStopReason(Shutdown.Instance)); base.PostStop(); } @@ -1227,18 +1339,18 @@ protected override void PostStop() /// TBD protected virtual void LogTermination(Reason reason) { - PatternMatch.Match(reason) - .With(f => + var failure = reason as Failure; + if (failure != null) + { + if (failure.Cause is Exception) { - if (f.Cause is Exception) - { - _log.Error(f.Cause.AsInstanceOf(), "terminating due to Failure"); - } - else - { - _log.Error(f.Cause.ToString()); - } - }); + _log.Error(failure.Cause.AsInstanceOf(), "terminating due to Failure"); + } + else + { + _log.Error(failure.Cause.ToString()); + } + } } } diff --git a/src/core/Akka/Routing/Listeners.cs b/src/core/Akka/Routing/Listeners.cs index 6f7254abe9a..cdba06c6e79 100644 --- a/src/core/Akka/Routing/Listeners.cs +++ b/src/core/Akka/Routing/Listeners.cs @@ -40,7 +40,7 @@ public abstract class ListenerMessage {} /// The class represents a sent by an to another /// instructing the second actor to start listening for messages sent by the first actor. /// - public class Listen : ListenerMessage + public sealed class Listen : ListenerMessage { /// /// Initializes a new instance of the class. @@ -54,14 +54,14 @@ public Listen(IActorRef listener) /// /// The actor that receives the message. /// - public IActorRef Listener { get; private set; } + public IActorRef Listener { get; } } /// /// The class represents a sent by an to another /// instructing the second actor to stop listening for messages sent by the first actor. /// - public class Deafen : ListenerMessage + public sealed class Deafen : ListenerMessage { /// /// Initializes a new instance of the class. @@ -75,14 +75,14 @@ public Deafen(IActorRef listener) /// /// The actor that no longer receives the message. /// - public IActorRef Listener { get; private set; } + public IActorRef Listener { get; } } /// /// This class represents a instructing an /// to perform a supplied for all of its listeners. /// - public class WithListeners : ListenerMessage + public sealed class WithListeners : ListenerMessage { /// /// Initializes a new instance of the class. @@ -96,7 +96,7 @@ public WithListeners(Action listenerFunction) /// /// The action to perform for all of an actor's listeners. /// - public Action ListenerFunction { get; private set; } + public Action ListenerFunction { get; } } /// diff --git a/src/core/Akka/Util/Internal/Extensions.cs b/src/core/Akka/Util/Internal/Extensions.cs index 4705f4ff144..99e47fa2c8c 100644 --- a/src/core/Akka/Util/Internal/Extensions.cs +++ b/src/core/Akka/Util/Internal/Extensions.cs @@ -201,14 +201,14 @@ public static IEnumerable Concat(this IEnumerable enumerable, T item) } /// - /// TBD + /// Applies a delegate to all elements of this enumerable. /// - /// TBD - /// TBD - /// TBD - public static void ForEach(this IEnumerable enumerable, Action action) + /// The type of the elements of . + /// An to iterate. + /// The function that is applied for its side-effect to every element. The result of function is discarded. + public static void ForEach(this IEnumerable source, Action action) { - foreach (var item in enumerable) + foreach (var item in source) action(item); } From 601e03929372ab5ea0c1e787ad8277d3b6194198 Mon Sep 17 00:00:00 2001 From: ravengerUA Date: Wed, 22 Mar 2017 09:00:30 +0200 Subject: [PATCH 2/3] changed API to IReadOnlyList --- src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt | 4 ++-- .../CoreAPISpec.ApprovePersistence.approved.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index e9fc197e3fd..35ba76c3468 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -798,8 +798,8 @@ namespace Akka.Actor } public class State : System.IEquatable> { - public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, bool notifies = True) { } - public System.Collections.Generic.IReadOnlyCollection Replies { get; set; } + public State(TS stateName, TD stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyList replies = null, bool notifies = True) { } + public System.Collections.Generic.IReadOnlyList Replies { get; set; } public TD StateData { get; } public TS StateName { get; } public Akka.Actor.FSMBase.Reason StopReason { get; } diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index 6df5e8e301d..2630793e0c8 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -830,14 +830,14 @@ namespace Akka.Persistence.Fsm public void WhenUnhandled(Akka.Persistence.Fsm.PersistentFSMBase.StateFunction stateFunction) { } public class State : Akka.Actor.FSMBase.State { - public State(TState stateName, TData stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } + public State(TState stateName, TData stateData, System.Nullable timeout = null, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyList replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } public System.Action AfterTransitionHandler { get; } public Akka.Util.ILinearSeq DomainEvents { get; } public new bool Notifies { get; set; } public Akka.Persistence.Fsm.PersistentFSMBase.State AndThen(System.Action handler) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Applying(Akka.Util.ILinearSeq events) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Applying(TEvent e) { } - public Akka.Persistence.Fsm.PersistentFSMBase.State Copy(System.Nullable timeout, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyCollection replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } + public Akka.Persistence.Fsm.PersistentFSMBase.State Copy(System.Nullable timeout, Akka.Actor.FSMBase.Reason stopReason = null, System.Collections.Generic.IReadOnlyList replies = null, Akka.Util.ILinearSeq domainEvents = null, System.Action afterTransitionDo = null) { } public Akka.Persistence.Fsm.PersistentFSMBase.State ForMax(System.TimeSpan timeout) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Replying(object replyValue) { } public Akka.Persistence.Fsm.PersistentFSMBase.State Using(TData nextStateData) { } From 4ebd0a79e93995ebde523d47aaa35f79cc792296 Mon Sep 17 00:00:00 2001 From: "alexey.v" Date: Wed, 22 Mar 2017 10:36:08 +0200 Subject: [PATCH 3/3] the latest fix --- .../Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt | 2 +- src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt index 2630793e0c8..bcdfce8bdf4 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApprovePersistence.approved.txt @@ -854,7 +854,7 @@ namespace Akka.Persistence.Fsm public System.Action AfterTransitionHandler { get; } public Akka.Util.ILinearSeq DomainEvents { get; } public bool Notifies { get; set; } - public System.Collections.Generic.IReadOnlyCollection Replies { get; } + public System.Collections.Generic.IReadOnlyList Replies { get; } public TData StateData { get; } public TState StateName { get; } public Akka.Actor.FSMBase.Reason StopReason { get; } diff --git a/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs b/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs index f2c2928b983..1949f38bec5 100644 --- a/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs +++ b/src/core/Akka.Persistence/Fsm/PersistentFSMBase.cs @@ -1059,7 +1059,7 @@ public State ForMax(TimeSpan timeout) /// /// TBD /// - public IReadOnlyCollection Replies + public IReadOnlyList Replies { get {