=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] {
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 file

@ -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 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``
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 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
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 file

@ -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)())
}
/**