Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

=act #13970 Send transistion on goto(CurrentState) in FSM

When in `A`:
* `goto(A)` will trigger `onTransition(A -> A)`
* `stay()` will NOT trigger `onTransition`

Includes:
* migration guide
* docs updates
* test
  • Loading branch information...
commit 2c88bb116903b42decb9d8063dc410325a9b9d29 1 parent 466c20e
@bambuchaAdm bambuchaAdm authored ktoso committed
View
42 akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala
@@ -32,13 +32,15 @@ object FSMTransitionSpec {
class OtherFSM(target: ActorRef) extends Actor with FSM[Int, Int] {
startWith(0, 0)
when(0) {
- case Event("tick", _) goto(1) using (1)
+ case Event("tick", _) goto(1) using 1
+ case Event("stay", _) stay()
}
when(1) {
- case _ stay
+ case _ goto(1)
}
onTransition {
case 0 -> 1 target ! ((stateData, nextStateData))
+ case 1 -> 1 target ! ((stateData, nextStateData))
}
}
@@ -78,7 +80,7 @@ class FSMTransitionSpec extends AkkaSpec with ImplicitSender {
expectMsg(FSM.CurrentState(fsm, 0))
akka.pattern.gracefulStop(forward, 5 seconds)
fsm ! "tick"
- expectNoMsg
+ expectNoMsg()
}
}
}
@@ -93,6 +95,36 @@ class FSMTransitionSpec extends AkkaSpec with ImplicitSender {
}
}
+ "trigger transition event when goto() the same state" in {
+ import FSM.Transition
+ val forward = system.actorOf(Props(new Forwarder(testActor)))
+ val fsm = system.actorOf(Props(new OtherFSM(testActor)))
+
+ within(1 second) {
+ fsm ! FSM.SubscribeTransitionCallBack(forward)
+ expectMsg(FSM.CurrentState(fsm, 0))
+ fsm ! "tick"
+ expectMsg((0, 1))
+ expectMsg(Transition(fsm, 0, 1))
+ fsm ! "tick"
+ expectMsg((1, 1))
+ expectMsg(Transition(fsm, 1, 1))
+ }
+ }
+
+ "not trigger transition event on stay()" in {
+ import FSM.Transition
+ val forward = system.actorOf(Props(new Forwarder(testActor)))
+ val fsm = system.actorOf(Props(new OtherFSM(testActor)))
+
+ within(1 second) {
+ fsm ! FSM.SubscribeTransitionCallBack(forward)
+ expectMsg(FSM.CurrentState(fsm, 0))
+ fsm ! "stay"
+ expectNoMsg()
+ }
+ }
+
"not leak memory in nextState" in {
val fsmref = system.actorOf(Props(new Actor with FSM[Int, ActorRef] {
startWith(0, null)
@@ -105,11 +137,11 @@ class FSMTransitionSpec extends AkkaSpec with ImplicitSender {
when(1) {
case Event("test", _)
try {
- sender() ! s"failed: ${nextStateData}"
+ sender() ! s"failed: $nextStateData"
} catch {
case _: IllegalStateException sender() ! "ok"
}
- stay
+ stay()
}
}))
fsmref ! "switch"
View
37 akka-actor/src/main/scala/akka/actor/FSM.scala
@@ -123,7 +123,14 @@ object FSM {
* name, the state data, possibly custom timeout, stop reason and replies
* accumulated while processing the last message.
*/
- final case class State[S, D](stateName: S, stateData: D, timeout: Option[FiniteDuration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) {
+ final case class State[S, D](stateName: S, stateData: D, timeout: Option[FiniteDuration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil)(private[akka] val notifies: Boolean = true) {
+
+ /**
+ * Copy object and update values if needed.
+ */
+ private[akka] def copy(stateName: S = stateName, stateData: D = stateData, timeout: Option[FiniteDuration] = timeout, stopReason: Option[Reason] = stopReason, replies: List[Any] = replies, notifies: Boolean = notifies): State[S, D] = {
+ State(stateName, stateData, timeout, stopReason, replies)(notifies)
+ }
/**
* Modify state transition descriptor to include a state timeout for the
@@ -160,7 +167,12 @@ object FSM {
private[akka] def withStopReason(reason: Reason): State[S, D] = {
copy(stopReason = Some(reason))
}
+
+ private[akka] def withNotification(notifies: Boolean): State[S, D] = {
+ copy(notifies = notifies)
+ }
}
+
/**
* All messages sent to the [[akka.actor.FSM]] will be wrapped inside an
* `Event`, which allows pattern matching to extract both state and data.
@@ -179,7 +191,6 @@ object FSM {
* Finite State Machine actor trait. Use as follows:
*
* <pre>
- * object A {
* trait State
* case class One extends State
* case class Two extends State
@@ -312,24 +323,30 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging {
* @param timeout state timeout for the initial state, overriding the default timeout for that state
*/
final def startWith(stateName: S, stateData: D, timeout: Timeout = None): Unit =
- currentState = FSM.State(stateName, stateData, timeout)
+ currentState = FSM.State(stateName, stateData, timeout)()
/**
- * Produce transition to other state. Return this from a state function in
- * order to effect the transition.
+ * Produce transition to other state.
+ * Return this from a state function in order to effect the transition.
+ *
+ * This method always triggers transition events, even for `A -> A` transitions.
+ * If you want to stay in the same state without triggering an state transition event use [[#stay]] instead.
*
* @param nextStateName state designator for the next state
* @return state transition descriptor
*/
- final def goto(nextStateName: S): State = FSM.State(nextStateName, currentState.stateData)
+ final def goto(nextStateName: S): State = FSM.State(nextStateName, currentState.stateData)()
/**
- * Produce "empty" transition descriptor. Return this from a state function
- * when no state change is to be effected.
+ * Produce "empty" transition descriptor.
+ * Return this from a state function when no state change is to be effected.
+ *
+ * No transition event will be triggered by [[#stay]].
+ * If you want to trigger an event like `S -> S` for [[#onTransition]] to handle use [[#goto]] instead.
*
* @return descriptor for staying in current state
*/
- final def stay(): State = goto(currentState.stateName) // cannot directly use currentState because of the timeout field
+ final def stay(): State = goto(currentState.stateName).withNotification(false) // cannot directly use currentState because of the timeout field
/**
* Produce change descriptor to stop this FSM actor with reason "Normal".
@@ -624,7 +641,7 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging {
terminate(stay withStopReason Failure("Next state %s does not exist".format(nextState.stateName)))
} else {
nextState.replies.reverse foreach { r sender() ! r }
- if (currentState.stateName != nextState.stateName) {
+ if (currentState.stateName != nextState.stateName || nextState.notifies) {
this.nextState = nextState
handleTransition(currentState.stateName, nextState.stateName)
gossip(Transition(self, currentState.stateName, nextState.stateName))
View
13 akka-docs/rst/project/migration-guide-2.3.x-2.4.x.rst
@@ -38,6 +38,19 @@ If you have been creating EventStreams manually, you now have to provide an acto
Please note that this change affects you only if you have implemented your own busses, Akka's own ``context.eventStream``
is still there and does not require any attention from you concerning this change.
+FSM notifies on same state transitions
+======================================
+When changing states in an Finite-State-Machine Actor (``FSM``), state transition events are emitted and can be handled by the user
+either by registering ``onTransition`` handlers or by subscribing to these events by sending it an ``SubscribeTransitionCallBack`` message.
+
+Previously in ``2.3.x`` when an ``FSM`` was in state ``A`` and performed an ``goto(A)`` transition, no state transition notification would be sent.
+This is because it would effectively stay in the same state, and was deemed to be semantically equivalent to calling ``stay()``.
+
+In ``2.4.x`` when an ``FSM`` performs a any ``goto(X)`` transition, it will always trigger state transition events.
+Which turns out to be useful in many systems where same-state transitions actually should have an effect.
+
+In case you do *not* want to trigger a state transition event when effectively performing an ``X->X`` transition, use ``stay()`` instead.
+
Removed Deprecated Features
===========================
View
19 akka-docs/rst/scala/fsm.rst
@@ -102,6 +102,13 @@ you of the direction of the state change which is being matched. During the
state change, the old state data is available via ``stateData`` as shown, and
the new state data would be available as ``nextStateData``.
+.. note::
+ Same-state transitions can be implemented (when currently in state ``S``) using
+ ``goto(S)`` or ``stay()``. The difference between those being that ``goto(S)`` will
+ emit an event ``S->S`` event that can be handled by ``onTransition``,
+ whereas ``stay()`` will *not*.
+
+
To verify that this buncher actually works, it is quite easy to write a test
using the :ref:`akka-testkit`, which is conveniently bundled with ScalaTest traits
into ``AkkaSpec``:
@@ -327,8 +334,16 @@ External actors may be registered to be notified of state transitions by
sending a message :class:`SubscribeTransitionCallBack(actorRef)`. The named
actor will be sent a :class:`CurrentState(self, stateName)` message immediately
and will receive :class:`Transition(actorRef, oldState, newState)` messages
-whenever a new state is reached. External monitors may be unregistered by
-sending :class:`UnsubscribeTransitionCallBack(actorRef)` to the FSM actor.
+whenever a state change is triggered.
+
+Please note that a state change includes the action of performing an ``goto(S)``, while
+already being state ``S``. In that case the monitoring actor will be notified with an
+``Transition(ref,S,S)`` message. This may be useful if your ``FSM`` should
+react on all (also same-state) transitions. In case you'd rather not emit events for same-state
+transitions use ``stay()`` instead of ``goto(S)``.
+
+External monitors may be unregistered by sending
+:class:`UnsubscribeTransitionCallBack(actorRef)` to the ``FSM`` actor.
Stopping a listener without unregistering will not remove the listener from the
subscription list; use :class:`UnsubscribeTransitionCallback` before stopping
View
2  akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala
@@ -62,7 +62,7 @@ class TestFSMRef[S, D, T <: Actor](
* and stop handling.
*/
def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: FiniteDuration = null, stopReason: Option[FSM.Reason] = None) {
- fsm.applyState(FSM.State(stateName, stateData, Option(timeout), stopReason))
+ fsm.applyState(FSM.State(stateName, stateData, Option(timeout), stopReason)())
}
/**
Please sign in to comment.
Something went wrong with that request. Please try again.