=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:
parent
466c20e3cd
commit
2c88bb1169
5 changed files with 95 additions and 18 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue