Merge branch 'wip-1485-fsm-docs-∂π'
This commit is contained in:
commit
ee5ae1068b
8 changed files with 215 additions and 191 deletions
|
|
@ -49,21 +49,6 @@ object FSM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* This extractor is just convenience for matching a (S, S) pair, including a
|
|
||||||
* reminder what the new state is.
|
|
||||||
*/
|
|
||||||
object -> {
|
|
||||||
def unapply[S](in: (S, S)) = Some(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* With these implicits in scope, you can write "5 seconds" anywhere a
|
|
||||||
* Duration or Option[Duration] is expected. This is conveniently true
|
|
||||||
* for derived classes.
|
|
||||||
*/
|
|
||||||
implicit def d2od(d: Duration): Option[Duration] = Some(d)
|
|
||||||
|
|
||||||
case class LogEntry[S, D](stateName: S, stateData: D, event: Any)
|
case class LogEntry[S, D](stateName: S, stateData: D, event: Any)
|
||||||
|
|
||||||
case class State[S, D](stateName: S, stateData: D, timeout: Option[Duration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) {
|
case class State[S, D](stateName: S, stateData: D, timeout: Option[Duration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) {
|
||||||
|
|
@ -208,9 +193,12 @@ trait FSM[S, D] extends Listeners {
|
||||||
* @param stateTimeout default state timeout for this state
|
* @param stateTimeout default state timeout for this state
|
||||||
* @param stateFunction partial function describing response to input
|
* @param stateFunction partial function describing response to input
|
||||||
*/
|
*/
|
||||||
protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = {
|
protected final def when(stateName: S, stateTimeout: Duration = null)(stateFunction: StateFunction): Unit =
|
||||||
|
register(stateName, stateFunction, Option(stateTimeout))
|
||||||
|
|
||||||
|
@deprecated("use the more import-friendly variant taking a Duration", "2.0")
|
||||||
|
protected final def when(stateName: S, stateTimeout: Timeout)(stateFunction: StateFunction): Unit =
|
||||||
register(stateName, stateFunction, stateTimeout)
|
register(stateName, stateFunction, stateTimeout)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set initial state. Call this method from the constructor before the #initialize method.
|
* Set initial state. Call this method from the constructor before the #initialize method.
|
||||||
|
|
@ -221,9 +209,8 @@ trait FSM[S, D] extends Listeners {
|
||||||
*/
|
*/
|
||||||
protected final def startWith(stateName: S,
|
protected final def startWith(stateName: S,
|
||||||
stateData: D,
|
stateData: D,
|
||||||
timeout: Timeout = None) = {
|
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. Return this from a state function in
|
||||||
|
|
@ -232,9 +219,7 @@ trait FSM[S, D] extends Listeners {
|
||||||
* @param nextStateName state designator for the next state
|
* @param nextStateName state designator for the next state
|
||||||
* @return state transition descriptor
|
* @return state transition descriptor
|
||||||
*/
|
*/
|
||||||
protected final def goto(nextStateName: S): State = {
|
protected final def goto(nextStateName: S): State = FSM.State(nextStateName, currentState.stateData)
|
||||||
FSM.State(nextStateName, currentState.stateData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce "empty" transition descriptor. Return this from a state function
|
* Produce "empty" transition descriptor. Return this from a state function
|
||||||
|
|
@ -242,31 +227,22 @@ trait FSM[S, D] extends Listeners {
|
||||||
*
|
*
|
||||||
* @return descriptor for staying in current state
|
* @return descriptor for staying in current state
|
||||||
*/
|
*/
|
||||||
protected final def stay(): State = {
|
protected final def stay(): State = goto(currentState.stateName) // cannot directly use currentState because of the timeout field
|
||||||
// cannot directly use currentState because of the timeout field
|
|
||||||
goto(currentState.stateName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce change descriptor to stop this FSM actor with reason "Normal".
|
* Produce change descriptor to stop this FSM actor with reason "Normal".
|
||||||
*/
|
*/
|
||||||
protected final def stop(): State = {
|
protected final def stop(): State = stop(Normal)
|
||||||
stop(Normal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce change descriptor to stop this FSM actor including specified reason.
|
* Produce change descriptor to stop this FSM actor including specified reason.
|
||||||
*/
|
*/
|
||||||
protected final def stop(reason: Reason): State = {
|
protected final def stop(reason: Reason): State = stop(reason, currentState.stateData)
|
||||||
stop(reason, currentState.stateData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce change descriptor to stop this FSM actor including specified reason.
|
* Produce change descriptor to stop this FSM actor including specified reason.
|
||||||
*/
|
*/
|
||||||
protected final def stop(reason: Reason, stateData: D): State = {
|
protected final def stop(reason: Reason, stateData: D): State = stay using stateData withStopReason (reason)
|
||||||
stay using stateData withStopReason (reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule named timer to deliver message after given delay, possibly repeating.
|
* Schedule named timer to deliver message after given delay, possibly repeating.
|
||||||
|
|
@ -290,12 +266,11 @@ trait FSM[S, D] extends Listeners {
|
||||||
* Cancel named timer, ensuring that the message is not subsequently delivered (no race).
|
* Cancel named timer, ensuring that the message is not subsequently delivered (no race).
|
||||||
* @param name of the timer to cancel
|
* @param name of the timer to cancel
|
||||||
*/
|
*/
|
||||||
protected[akka] def cancelTimer(name: String) = {
|
protected[akka] def cancelTimer(name: String): Unit =
|
||||||
if (timers contains name) {
|
if (timers contains name) {
|
||||||
timers(name).cancel
|
timers(name).cancel
|
||||||
timers -= name
|
timers -= name
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inquire whether the named timer is still active. Returns true unless the
|
* Inquire whether the named timer is still active. Returns true unless the
|
||||||
|
|
@ -308,8 +283,14 @@ trait FSM[S, D] extends Listeners {
|
||||||
* Set state timeout explicitly. This method can safely be used from within a
|
* Set state timeout explicitly. This method can safely be used from within a
|
||||||
* state handler.
|
* state handler.
|
||||||
*/
|
*/
|
||||||
protected final def setStateTimeout(state: S, timeout: Timeout) {
|
protected final def setStateTimeout(state: S, timeout: Timeout): Unit = stateTimeouts(state) = timeout
|
||||||
stateTimeouts(state) = timeout
|
|
||||||
|
/**
|
||||||
|
* This extractor is just convenience for matching a (S, S) pair, including a
|
||||||
|
* reminder what the new state is.
|
||||||
|
*/
|
||||||
|
object -> {
|
||||||
|
def unapply[S](in: (S, S)) = Some(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -337,9 +318,7 @@ trait FSM[S, D] extends Listeners {
|
||||||
* <b>Multiple handlers may be installed, and every one of them will be
|
* <b>Multiple handlers may be installed, and every one of them will be
|
||||||
* called, not only the first one matching.</b>
|
* called, not only the first one matching.</b>
|
||||||
*/
|
*/
|
||||||
protected final def onTransition(transitionHandler: TransitionHandler) {
|
protected final def onTransition(transitionHandler: TransitionHandler): Unit = transitionEvent :+= transitionHandler
|
||||||
transitionEvent :+= transitionHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience wrapper for using a total function instead of a partial
|
* Convenience wrapper for using a total function instead of a partial
|
||||||
|
|
@ -354,24 +333,20 @@ trait FSM[S, D] extends Listeners {
|
||||||
/**
|
/**
|
||||||
* Set handler which is called upon termination of this FSM actor.
|
* Set handler which is called upon termination of this FSM actor.
|
||||||
*/
|
*/
|
||||||
protected final def onTermination(terminationHandler: PartialFunction[StopEvent[S, D], Unit]) = {
|
protected final def onTermination(terminationHandler: PartialFunction[StopEvent[S, D], Unit]): Unit =
|
||||||
terminateEvent = terminationHandler
|
terminateEvent = terminationHandler
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set handler which is called upon reception of unhandled messages.
|
* Set handler which is called upon reception of unhandled messages.
|
||||||
*/
|
*/
|
||||||
protected final def whenUnhandled(stateFunction: StateFunction) = {
|
protected final def whenUnhandled(stateFunction: StateFunction): Unit =
|
||||||
handleEvent = stateFunction orElse handleEventDefault
|
handleEvent = stateFunction orElse handleEventDefault
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify existence of initial state and setup timers. This should be the
|
* Verify existence of initial state and setup timers. This should be the
|
||||||
* last call within the constructor.
|
* last call within the constructor.
|
||||||
*/
|
*/
|
||||||
protected final def initialize {
|
protected final def initialize: Unit = makeTransition(currentState)
|
||||||
makeTransition(currentState)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return current state name (i.e. object of type S)
|
* Return current state name (i.e. object of type S)
|
||||||
|
|
@ -414,7 +389,7 @@ trait FSM[S, D] extends Listeners {
|
||||||
private val stateFunctions = mutable.Map[S, StateFunction]()
|
private val stateFunctions = mutable.Map[S, StateFunction]()
|
||||||
private val stateTimeouts = mutable.Map[S, Timeout]()
|
private val stateTimeouts = mutable.Map[S, Timeout]()
|
||||||
|
|
||||||
private def register(name: S, function: StateFunction, timeout: Timeout) {
|
private def register(name: S, function: StateFunction, timeout: Timeout): Unit = {
|
||||||
if (stateFunctions contains name) {
|
if (stateFunctions contains name) {
|
||||||
stateFunctions(name) = stateFunctions(name) orElse function
|
stateFunctions(name) = stateFunctions(name) orElse function
|
||||||
stateTimeouts(name) = timeout orElse stateTimeouts(name)
|
stateTimeouts(name) = timeout orElse stateTimeouts(name)
|
||||||
|
|
@ -494,12 +469,12 @@ trait FSM[S, D] extends Listeners {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def processMsg(value: Any, source: AnyRef) {
|
private def processMsg(value: Any, source: AnyRef): Unit = {
|
||||||
val event = Event(value, currentState.stateData)
|
val event = Event(value, currentState.stateData)
|
||||||
processEvent(event, source)
|
processEvent(event, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[akka] def processEvent(event: Event, source: AnyRef) {
|
private[akka] def processEvent(event: Event, source: AnyRef): Unit = {
|
||||||
val stateFunc = stateFunctions(currentState.stateName)
|
val stateFunc = stateFunctions(currentState.stateName)
|
||||||
val nextState = if (stateFunc isDefinedAt event) {
|
val nextState = if (stateFunc isDefinedAt event) {
|
||||||
stateFunc(event)
|
stateFunc(event)
|
||||||
|
|
@ -510,7 +485,7 @@ trait FSM[S, D] extends Listeners {
|
||||||
applyState(nextState)
|
applyState(nextState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[akka] def applyState(nextState: State) {
|
private[akka] def applyState(nextState: State): Unit = {
|
||||||
nextState.stopReason match {
|
nextState.stopReason match {
|
||||||
case None ⇒ makeTransition(nextState)
|
case None ⇒ makeTransition(nextState)
|
||||||
case _ ⇒
|
case _ ⇒
|
||||||
|
|
@ -520,7 +495,7 @@ trait FSM[S, D] extends Listeners {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private[akka] def makeTransition(nextState: State) {
|
private[akka] def makeTransition(nextState: State): Unit = {
|
||||||
if (!stateFunctions.contains(nextState.stateName)) {
|
if (!stateFunctions.contains(nextState.stateName)) {
|
||||||
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 {
|
||||||
|
|
@ -541,9 +516,9 @@ trait FSM[S, D] extends Listeners {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def postStop() { terminate(stay withStopReason Shutdown) }
|
override def postStop(): Unit = { terminate(stay withStopReason Shutdown) }
|
||||||
|
|
||||||
private def terminate(nextState: State) {
|
private def terminate(nextState: State): Unit = {
|
||||||
if (!currentState.stopReason.isDefined) {
|
if (!currentState.stopReason.isDefined) {
|
||||||
val reason = nextState.stopReason.get
|
val reason = nextState.stopReason.get
|
||||||
reason match {
|
reason match {
|
||||||
|
|
@ -600,13 +575,13 @@ trait LoggingFSM[S, D] extends FSM[S, D] { this: Actor ⇒
|
||||||
super.setTimer(name, msg, timeout, repeat)
|
super.setTimer(name, msg, timeout, repeat)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected[akka] abstract override def cancelTimer(name: String) = {
|
protected[akka] abstract override def cancelTimer(name: String): Unit = {
|
||||||
if (debugEvent)
|
if (debugEvent)
|
||||||
log.debug("canceling timer '" + name + "'")
|
log.debug("canceling timer '" + name + "'")
|
||||||
super.cancelTimer(name)
|
super.cancelTimer(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private[akka] abstract override def processEvent(event: Event, source: AnyRef) {
|
private[akka] abstract override def processEvent(event: Event, source: AnyRef): Unit = {
|
||||||
if (debugEvent) {
|
if (debugEvent) {
|
||||||
val srcstr = source match {
|
val srcstr = source match {
|
||||||
case s: String ⇒ s
|
case s: String ⇒ s
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,17 @@ depending on the configuration of the actor system:
|
||||||
- There are several special types of actor references which behave like local
|
- There are several special types of actor references which behave like local
|
||||||
actor references for all practical purposes:
|
actor references for all practical purposes:
|
||||||
|
|
||||||
- :class:`AskActorRef` is the special representation of a :meth:`Promise` for
|
- :class:`PromiseActorRef` is the special representation of a :meth:`Promise` for
|
||||||
the purpose of being completed by the response from an actor; it is created
|
the purpose of being completed by the response from an actor; it is created
|
||||||
by the :meth:`ActorRef.ask` invocation.
|
by the :meth:`ActorRef.ask` invocation.
|
||||||
- :class:`DeadLetterActorRef` is the default implementation of the dead
|
- :class:`DeadLetterActorRef` is the default implementation of the dead
|
||||||
letters service, where all messages are re-routed whose routees are shut
|
letters service, where all messages are re-routed whose routees are shut
|
||||||
down or non-existent.
|
down or non-existent.
|
||||||
|
- :class:`EmptyLocalActorRef` is what is returned when looking up a
|
||||||
|
non-existing local actor path: it is equivalent to a
|
||||||
|
:class:`DeadLetterActorRef`, but it retains its path so that it can be sent
|
||||||
|
over the network and compared to other existing actor refs for that path,
|
||||||
|
some of which might have been obtained before the actor stopped existing.
|
||||||
|
|
||||||
- And then there are some one-off internal implementations which you should
|
- And then there are some one-off internal implementations which you should
|
||||||
never really see:
|
never really see:
|
||||||
|
|
@ -309,12 +314,3 @@ other actors are found. The next level consists of the following:
|
||||||
- ``"/remote"`` is an artificial path below which all actors reside whose
|
- ``"/remote"`` is an artificial path below which all actors reside whose
|
||||||
supervisors are remote actor references
|
supervisors are remote actor references
|
||||||
|
|
||||||
Future extensions:
|
|
||||||
|
|
||||||
- ``"/service"`` is an artificial path below which actors can be presented by
|
|
||||||
means of configuration, i.e. deployed at system start-up or just-in-time
|
|
||||||
(triggered by look-up)
|
|
||||||
- ``"/alias"`` is an artificial path below which other actors may be “mounted”
|
|
||||||
(as in the Unix file-system sense) by path—local or remote—to give them
|
|
||||||
logical names.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,10 @@ currently traversed actor, otherwise it will step “down” to the named child.
|
||||||
It should be noted that the ``..`` in actor paths here always means the logical
|
It should be noted that the ``..`` in actor paths here always means the logical
|
||||||
structure, i.e. the supervisor.
|
structure, i.e. the supervisor.
|
||||||
|
|
||||||
|
If the path being looked up does not exist, a special actor reference is
|
||||||
|
returned which behaves like the actor system’s dead letter queue but retains
|
||||||
|
its identity (i.e. the path which was looked up).
|
||||||
|
|
||||||
Remote actor addresses may also be looked up, if remoting is enabled::
|
Remote actor addresses may also be looked up, if remoting is enabled::
|
||||||
|
|
||||||
getContext().actorFor("akka://app@otherhost:1234/user/serviceB")
|
getContext().actorFor("akka://app@otherhost:1234/user/serviceB")
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,10 @@ currently traversed actor, otherwise it will step “down” to the named child.
|
||||||
It should be noted that the ``..`` in actor paths here always means the logical
|
It should be noted that the ``..`` in actor paths here always means the logical
|
||||||
structure, i.e. the supervisor.
|
structure, i.e. the supervisor.
|
||||||
|
|
||||||
|
If the path being looked up does not exist, a special actor reference is
|
||||||
|
returned which behaves like the actor system’s dead letter queue but retains
|
||||||
|
its identity (i.e. the path which was looked up).
|
||||||
|
|
||||||
Remote actor addresses may also be looked up, if remoting is enabled::
|
Remote actor addresses may also be looked up, if remoting is enabled::
|
||||||
|
|
||||||
context.actorFor("akka://app@otherhost:1234/user/serviceB")
|
context.actorFor("akka://app@otherhost:1234/user/serviceB")
|
||||||
|
|
|
||||||
96
akka-docs/scala/code/akka/docs/actor/FSMDocSpec.scala
Normal file
96
akka-docs/scala/code/akka/docs/actor/FSMDocSpec.scala
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2012 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.docs.actor
|
||||||
|
|
||||||
|
//#test-code
|
||||||
|
import akka.testkit.AkkaSpec
|
||||||
|
import akka.actor.Props
|
||||||
|
|
||||||
|
class FSMDocSpec extends AkkaSpec {
|
||||||
|
|
||||||
|
"simple finite state machine" must {
|
||||||
|
//#fsm-code-elided
|
||||||
|
//#simple-imports
|
||||||
|
import akka.actor.{ Actor, ActorRef, FSM }
|
||||||
|
import akka.util.duration._
|
||||||
|
//#simple-imports
|
||||||
|
//#simple-events
|
||||||
|
// received events
|
||||||
|
case class SetTarget(ref: ActorRef)
|
||||||
|
case class Queue(obj: Any)
|
||||||
|
case object Flush
|
||||||
|
|
||||||
|
// sent events
|
||||||
|
case class Batch(obj: Seq[Any])
|
||||||
|
//#simple-events
|
||||||
|
//#simple-state
|
||||||
|
// states
|
||||||
|
sealed trait State
|
||||||
|
case object Idle extends State
|
||||||
|
case object Active extends State
|
||||||
|
|
||||||
|
sealed trait Data
|
||||||
|
case object Uninitialized extends Data
|
||||||
|
case class Todo(target: ActorRef, queue: Seq[Any]) extends Data
|
||||||
|
//#simple-state
|
||||||
|
//#simple-fsm
|
||||||
|
class Buncher extends Actor with FSM[State, Data] {
|
||||||
|
|
||||||
|
startWith(Idle, Uninitialized)
|
||||||
|
|
||||||
|
when(Idle) {
|
||||||
|
case Event(SetTarget(ref), Uninitialized) ⇒ stay using Todo(ref, Vector.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#transition-elided
|
||||||
|
onTransition {
|
||||||
|
case Active -> Idle ⇒
|
||||||
|
stateData match {
|
||||||
|
case Todo(ref, queue) ⇒ ref ! Batch(queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#transition-elided
|
||||||
|
|
||||||
|
when(Active, stateTimeout = 1 second) {
|
||||||
|
case Event(Flush | FSM.StateTimeout, t: Todo) ⇒ goto(Idle) using t.copy(queue = Vector.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#unhandled-elided
|
||||||
|
whenUnhandled {
|
||||||
|
// common code for both states
|
||||||
|
case Event(Queue(obj), t @ Todo(_, v)) ⇒
|
||||||
|
goto(Active) using t.copy(queue = v :+ obj)
|
||||||
|
|
||||||
|
case Event(e, s) ⇒
|
||||||
|
log.warning("received unhandled request {} in state {}/{}", e, stateName, s)
|
||||||
|
stay
|
||||||
|
}
|
||||||
|
//#unhandled-elided
|
||||||
|
|
||||||
|
initialize
|
||||||
|
}
|
||||||
|
//#simple-fsm
|
||||||
|
//#fsm-code-elided
|
||||||
|
|
||||||
|
"batch correctly" in {
|
||||||
|
val buncher = system.actorOf(Props(new Buncher))
|
||||||
|
buncher ! SetTarget(testActor)
|
||||||
|
buncher ! Queue(42)
|
||||||
|
buncher ! Queue(43)
|
||||||
|
expectMsg(Batch(Seq(42, 43)))
|
||||||
|
buncher ! Queue(44)
|
||||||
|
buncher ! Flush
|
||||||
|
buncher ! Queue(45)
|
||||||
|
expectMsg(Batch(Seq(44)))
|
||||||
|
expectMsg(Batch(Seq(45)))
|
||||||
|
}
|
||||||
|
|
||||||
|
"batch not if uninitialized" in {
|
||||||
|
val buncher = system.actorOf(Props(new Buncher))
|
||||||
|
buncher ! Queue(42)
|
||||||
|
expectNoMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#test-code
|
||||||
|
|
@ -26,146 +26,104 @@ These relations are interpreted as meaning:
|
||||||
A Simple Example
|
A Simple Example
|
||||||
================
|
================
|
||||||
|
|
||||||
To demonstrate the usage of states we start with a simple FSM without state
|
To demonstrate most of the features of the :class:`FSM` trait, consider an
|
||||||
data. The state can be of any type so for this example we create the states A,
|
actor which shall receive and queue messages while they arrive in a burst and
|
||||||
B and C.
|
send them on after the burst ended or a flush request is received.
|
||||||
|
|
||||||
.. code-block:: scala
|
First, consider all of the below to use these import statements:
|
||||||
|
|
||||||
sealed trait ExampleState
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala#simple-imports
|
||||||
case object A extends ExampleState
|
|
||||||
case object B extends ExampleState
|
|
||||||
case object C extends ExampleState
|
|
||||||
|
|
||||||
Now lets create an object representing the FSM and defining the behavior.
|
The contract of our “Buncher” actor is that is accepts or produces the following messages:
|
||||||
|
|
||||||
.. code-block:: scala
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala#simple-events
|
||||||
|
|
||||||
import akka.actor.{Actor, FSM}
|
``SetTarget`` is needed for starting it up, setting the destination for the
|
||||||
import akka.util.duration._
|
``Batches`` to be passed on; ``Queue`` will add to the internal queue while
|
||||||
|
``Flush`` will mark the end of a burst.
|
||||||
|
|
||||||
case object Move
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala#simple-state
|
||||||
|
|
||||||
class ABC extends Actor with FSM[ExampleState, Unit] {
|
The actor can be in two states: no message queued (aka ``Idle``) or some
|
||||||
|
message queued (aka ``Active``). It will stay in the active state as long as
|
||||||
|
messages keep arriving and no flush is requested. The internal state data of
|
||||||
|
the actor is made up of the target actor reference to send the batches to and
|
||||||
|
the actual queue of messages.
|
||||||
|
|
||||||
import FSM._
|
Now let’s take a look at the skeleton for our FSM actor:
|
||||||
|
|
||||||
startWith(A, Unit)
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala
|
||||||
|
:include: simple-fsm
|
||||||
|
:exclude: transition-elided,unhandled-elided
|
||||||
|
|
||||||
when(A) {
|
The basic strategy is to declare the actor, mixing in the :class:`FSM` trait
|
||||||
case Ev(Move) =>
|
and specifying the possible states and data values as type paramters. Within
|
||||||
log.info(this, "Go to B and move on after 5 seconds")
|
the body of the actor a DSL is used for declaring the state machine:
|
||||||
goto(B) forMax (5 seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
when(B) {
|
* :meth:`startsWith` defines the initial state and initial data
|
||||||
case Ev(StateTimeout) =>
|
* then there is one :meth:`when(<state>) { ... }` declaration per state to be
|
||||||
log.info(this, "Moving to C")
|
handled (could potentially be multiple ones, the passed
|
||||||
goto(C)
|
:class:`PartialFunction` will be concatenated using :meth:`orElse`)
|
||||||
}
|
* finally starting it up using :meth:`initialize`, which performs the
|
||||||
|
transition into the initial state and sets up timers (if required).
|
||||||
|
|
||||||
when(C) {
|
In this case, we start out in the ``Idle`` and ``Uninitialized`` state, where
|
||||||
case Ev(Move) =>
|
only the ``SetTarget()`` message is handled; ``stay`` prepares to end this
|
||||||
log.info(this, "Stopping")
|
event’s processing for not leaving the current state, while the ``using``
|
||||||
stop
|
modifier makes the FSM replace the internal state (which is ``Uninitialized``
|
||||||
}
|
at this point) with a fresh ``Todo()`` object containing the target actor
|
||||||
|
reference. The ``Active`` state has a state timeout declared, which means that
|
||||||
|
if no message is received for 1 second, a ``FSM.StateTimeout`` message will be
|
||||||
|
generated. This has the same effect as receiving the ``Flush`` command in this
|
||||||
|
case, namely to transition back into the ``Idle`` state and resetting the
|
||||||
|
internal queue to the empty vector. But how do messages get queued? Since this
|
||||||
|
shall work identically in both states, we make use of the fact that any event
|
||||||
|
which is not handled by the ``when()`` block is passed to the
|
||||||
|
``whenUnhandled()`` block:
|
||||||
|
|
||||||
initialize // this checks validity of the initial state and sets up timeout if needed
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala#unhandled-elided
|
||||||
}
|
|
||||||
|
|
||||||
Each state is described by one or more :func:`when(state)` blocks; if more than
|
The first case handled here is adding ``Queue()`` requests to the internal
|
||||||
one is given for the same state, they are tried in the order given until the
|
queue and going to the ``Active`` state (this does the obvious thing of staying
|
||||||
first is found which matches the incoming event. Events are matched using
|
in the ``Active`` state if already there), but only if the FSM data are not
|
||||||
either :func:`Ev(msg)` (if no state data are to be extracted) or
|
``Uninitialized`` when the ``Queue()`` event is received. Otherwise—and in all
|
||||||
:func:`Event(msg, data)`, see below. The statements for each case are the
|
other non-handled cases—the second case just logs a warning and does not change
|
||||||
actions to be taken, where the final expression must describe the transition
|
the internal state.
|
||||||
into the next state. This can either be :func:`stay` when no transition is
|
|
||||||
needed or :func:`goto(target)` for changing into the target state. The
|
|
||||||
transition may be annotated with additional properties, where this example
|
|
||||||
includes a state timeout of 5 seconds after the transition into state B:
|
|
||||||
:func:`forMax(duration)` arranges for a :obj:`StateTimeout` message to be
|
|
||||||
scheduled, unless some other message is received first. The construction of the
|
|
||||||
FSM is finished by calling the :func:`initialize` method as last part of the
|
|
||||||
ABC constructor.
|
|
||||||
|
|
||||||
State Data
|
The only missing piece is where the ``Batches`` are actually sent to the
|
||||||
==========
|
target, for which we use the ``onTransition`` mechanism: you can declare
|
||||||
|
multiple such blocks and all of them will be tried for matching behavior in
|
||||||
|
case a state transition occurs (i.e. only when the state actually changes).
|
||||||
|
|
||||||
The FSM can also hold state data associated with the internal state of the
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala#transition-elided
|
||||||
state machine. The state data can be of any type but to demonstrate let's look
|
|
||||||
at a lock with a :class:`String` as state data holding the entered unlock code.
|
|
||||||
First we need two states for the lock:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
The transition callback is a partial function which takes as input a pair of
|
||||||
|
states—the current and the next state. The FSM trait includes a convenience
|
||||||
|
extractor for these in form of an arrow operator, which conveniently reminds
|
||||||
|
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``.
|
||||||
|
|
||||||
sealed trait LockState
|
To verify that this buncher actually works, it is quite easy to write a test
|
||||||
case object Locked extends LockState
|
using the :ref:`akka-testkit`, which is conveniently bundled with ScalaTest traits
|
||||||
case object Open extends LockState
|
into ``AkkaSpec``:
|
||||||
|
|
||||||
Now we can create a lock FSM that takes :class:`LockState` as a state and a
|
.. includecode:: code/akka/docs/actor/FSMDocSpec.scala
|
||||||
:class:`String` as state data:
|
:include: test-code
|
||||||
|
:exclude: fsm-code-elided
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
import akka.actor.{Actor, FSM}
|
|
||||||
|
|
||||||
class Lock(code: String) extends Actor with FSM[LockState, String] {
|
|
||||||
|
|
||||||
import FSM._
|
|
||||||
|
|
||||||
val emptyCode = ""
|
|
||||||
|
|
||||||
startWith(Locked, emptyCode)
|
|
||||||
|
|
||||||
when(Locked) {
|
|
||||||
// receive a digit and the code that we have so far
|
|
||||||
case Event(digit: Char, soFar) => {
|
|
||||||
// add the digit to what we have
|
|
||||||
soFar + digit match {
|
|
||||||
case incomplete if incomplete.length < code.length =>
|
|
||||||
// not enough digits yet so stay using the
|
|
||||||
// incomplete code as the new state data
|
|
||||||
stay using incomplete
|
|
||||||
case `code` =>
|
|
||||||
// code matched the one from the lock
|
|
||||||
// so go to Open state and reset the state data
|
|
||||||
goto(Open) using emptyCode forMax (1 seconds)
|
|
||||||
case wrong =>
|
|
||||||
// wrong code, stay Locked and reset the state data
|
|
||||||
stay using emptyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when(Open) {
|
|
||||||
case Ev(StateTimeout, _) => {
|
|
||||||
// after the timeout, go back to Locked state
|
|
||||||
goto(Locked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize
|
|
||||||
}
|
|
||||||
|
|
||||||
This very simple example shows how the complete state of the FSM is encoded in
|
|
||||||
the :obj:`(State, Data)` pair and only explicitly updated during transitions.
|
|
||||||
This encapsulation is what makes state machines a powerful abstraction, e.g.
|
|
||||||
for handling socket states in a network server application.
|
|
||||||
|
|
||||||
Reference
|
Reference
|
||||||
=========
|
=========
|
||||||
|
|
||||||
This section describes the DSL in a more formal way, refer to `Examples`_ for more sample material.
|
|
||||||
|
|
||||||
The FSM Trait and Object
|
The FSM Trait and Object
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
The :class:`FSM` trait may only be mixed into an :class:`Actor`. Instead of
|
The :class:`FSM` trait may only be mixed into an :class:`Actor`. Instead of
|
||||||
extending :class:`Actor`, the self type approach was chosen in order to make it
|
extending :class:`Actor`, the self type approach was chosen in order to make it
|
||||||
obvious that an actor is actually created. Importing all members of the
|
obvious that an actor is actually created. Importing all members of the
|
||||||
:obj:`FSM` object is recommended to receive useful implicits and directly
|
:obj:`FSM` object is recommended if you want to directly access the symbols
|
||||||
access the symbols like :obj:`StateTimeout`. This import is usually placed
|
like :obj:`StateTimeout`. This import is usually placed inside the state
|
||||||
inside the state machine definition:
|
machine definition:
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
|
|
@ -192,15 +150,6 @@ The :class:`FSM` trait takes two type parameters:
|
||||||
to the FSM class you have the advantage of making all changes of the
|
to the FSM class you have the advantage of making all changes of the
|
||||||
internal state explicit in a few well-known places.
|
internal state explicit in a few well-known places.
|
||||||
|
|
||||||
Defining Timeouts
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The :class:`FSM` module uses :ref:`Duration` for all timing configuration.
|
|
||||||
Several methods, like :func:`when()` and :func:`startWith()` take a
|
|
||||||
:class:`FSM.Timeout`, which is an alias for :class:`Option[Duration]`. There is
|
|
||||||
an implicit conversion available in the :obj:`FSM` object which makes this
|
|
||||||
transparent, just import it into your FSM body.
|
|
||||||
|
|
||||||
Defining States
|
Defining States
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ abstract class GenericBuncher[A: Manifest, B](val singleTimeout: Duration, val m
|
||||||
case Event(Stop, _) ⇒ stop
|
case Event(Stop, _) ⇒ stop
|
||||||
}
|
}
|
||||||
|
|
||||||
when(Active, stateTimeout = Some(singleTimeout)) {
|
when(Active, stateTimeout = singleTimeout) {
|
||||||
case Event(Msg(m), acc) ⇒
|
case Event(Msg(m), acc) ⇒
|
||||||
stay using merge(acc, m)
|
stay using merge(acc, m)
|
||||||
case Event(StateTimeout, acc) ⇒
|
case Event(StateTimeout, acc) ⇒
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ class TestFSMRef[S, D, T <: Actor](
|
||||||
* corresponding transition initiated from within the FSM, including timeout
|
* corresponding transition initiated from within the FSM, including timeout
|
||||||
* and stop handling.
|
* and stop handling.
|
||||||
*/
|
*/
|
||||||
def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: Option[Duration] = None, stopReason: Option[FSM.Reason] = None) {
|
def setState(stateName: S = fsm.stateName, stateData: D = fsm.stateData, timeout: Duration = null, stopReason: Option[FSM.Reason] = None) {
|
||||||
fsm.applyState(FSM.State(stateName, stateData, timeout, stopReason))
|
fsm.applyState(FSM.State(stateName, stateData, Option(timeout), stopReason))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue