diff --git a/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala index 1475e87599..3f7dda1837 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala +++ b/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" diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 32b0a3dfb3..9907fafdac 100644 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/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: * *
- *   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))
diff --git a/akka-docs/rst/project/migration-guide-2.3.x-2.4.x.rst b/akka-docs/rst/project/migration-guide-2.3.x-2.4.x.rst
index 7ea70ce474..764422fe6e 100644
--- a/akka-docs/rst/project/migration-guide-2.3.x-2.4.x.rst
+++ b/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
 ===========================
 
diff --git a/akka-docs/rst/scala/fsm.rst b/akka-docs/rst/scala/fsm.rst
index 2dec8c3d6f..4a00697000 100644
--- a/akka-docs/rst/scala/fsm.rst
+++ b/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
diff --git a/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala b/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala
index 4a1f601b43..b5d8db0926 100644
--- a/akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala
+++ b/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)())
   }
 
   /**