Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jul 19, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions akka-actor-tests/src/test/scala/akka/actor/FSMTransitionSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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()
}
}
}
Expand All @@ -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)
Expand All @@ -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"
Expand Down
37 changes: 27 additions & 10 deletions akka-actor/src/main/scala/akka/actor/FSM.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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".
Expand Down Expand Up @@ -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))
Expand Down
13 changes: 13 additions & 0 deletions akka-docs/rst/project/migration-guide-2.3.x-2.4.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========================

Expand Down
19 changes: 17 additions & 2 deletions akka-docs/rst/scala/fsm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion akka-testkit/src/main/scala/akka/testkit/TestFSMRef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)())
}

/**
Expand Down