=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
This commit is contained in:
Łukasz Dubiel 2014-06-04 20:25:50 +02:00 committed by Konrad 'ktoso' Malawski
parent 466c20e3cd
commit 2c88bb1169
No known key found for this signature in database
GPG key ID: 9EDE9520298851A7
5 changed files with 95 additions and 18 deletions

View file

@ -32,13 +32,15 @@ object FSMTransitionSpec {
class OtherFSM(target: ActorRef) extends Actor with FSM[Int, Int] { class OtherFSM(target: ActorRef) extends Actor with FSM[Int, Int] {
startWith(0, 0) startWith(0, 0)
when(0) { when(0) {
case Event("tick", _) goto(1) using (1) case Event("tick", _) goto(1) using 1
case Event("stay", _) stay()
} }
when(1) { when(1) {
case _ stay case _ goto(1)
} }
onTransition { onTransition {
case 0 -> 1 target ! ((stateData, nextStateData)) 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)) expectMsg(FSM.CurrentState(fsm, 0))
akka.pattern.gracefulStop(forward, 5 seconds) akka.pattern.gracefulStop(forward, 5 seconds)
fsm ! "tick" 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 { "not leak memory in nextState" in {
val fsmref = system.actorOf(Props(new Actor with FSM[Int, ActorRef] { val fsmref = system.actorOf(Props(new Actor with FSM[Int, ActorRef] {
startWith(0, null) startWith(0, null)
@ -105,11 +137,11 @@ class FSMTransitionSpec extends AkkaSpec with ImplicitSender {
when(1) { when(1) {
case Event("test", _) case Event("test", _)
try { try {
sender() ! s"failed: ${nextStateData}" sender() ! s"failed: $nextStateData"
} catch { } catch {
case _: IllegalStateException sender() ! "ok" case _: IllegalStateException sender() ! "ok"
} }
stay stay()
} }
})) }))
fsmref ! "switch" fsmref ! "switch"

View file

@ -123,7 +123,14 @@ object FSM {
* name, the state data, possibly custom timeout, stop reason and replies * name, the state data, possibly custom timeout, stop reason and replies
* accumulated while processing the last message. * 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 * 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] = { private[akka] def withStopReason(reason: Reason): State[S, D] = {
copy(stopReason = Some(reason)) 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 * All messages sent to the [[akka.actor.FSM]] will be wrapped inside an
* `Event`, which allows pattern matching to extract both state and data. * `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: * Finite State Machine actor trait. Use as follows:
* *
* <pre> * <pre>
* object A {
* trait State * trait State
* case class One extends State * case class One extends State
* case class Two 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 * @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 = 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 * Produce transition to other state.
* order to effect the transition. * 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 * @param nextStateName state designator for the next state
* @return state transition descriptor * @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 * Produce "empty" transition descriptor.
* when no state change is to be effected. * 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 * @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". * 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))) terminate(stay withStopReason Failure("Next state %s does not exist".format(nextState.stateName)))
} else { } else {
nextState.replies.reverse foreach { r sender() ! r } nextState.replies.reverse foreach { r sender() ! r }
if (currentState.stateName != nextState.stateName) { if (currentState.stateName != nextState.stateName || nextState.notifies) {
this.nextState = nextState this.nextState = nextState
handleTransition(currentState.stateName, nextState.stateName) handleTransition(currentState.stateName, nextState.stateName)
gossip(Transition(self, currentState.stateName, nextState.stateName)) gossip(Transition(self, currentState.stateName, nextState.stateName))

View file

@ -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`` 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. 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 Removed Deprecated Features
=========================== ===========================

View file

@ -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 state change, the old state data is available via ``stateData`` as shown, and
the new state data would be available as ``nextStateData``. 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 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 using the :ref:`akka-testkit`, which is conveniently bundled with ScalaTest traits
into ``AkkaSpec``: 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 sending a message :class:`SubscribeTransitionCallBack(actorRef)`. The named
actor will be sent a :class:`CurrentState(self, stateName)` message immediately actor will be sent a :class:`CurrentState(self, stateName)` message immediately
and will receive :class:`Transition(actorRef, oldState, newState)` messages and will receive :class:`Transition(actorRef, oldState, newState)` messages
whenever a new state is reached. External monitors may be unregistered by whenever a state change is triggered.
sending :class:`UnsubscribeTransitionCallBack(actorRef)` to the FSM actor.
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 Stopping a listener without unregistering will not remove the listener from the
subscription list; use :class:`UnsubscribeTransitionCallback` before stopping subscription list; use :class:`UnsubscribeTransitionCallback` before stopping

View file

@ -62,7 +62,7 @@ class TestFSMRef[S, D, T <: Actor](
* and stop handling. * and stop handling.
*/ */
def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: FiniteDuration = null, stopReason: Option[FSM.Reason] = None) { 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)())
} }
/** /**