include FSMDocSpec example in fsm.rst and fix a few outdated things
This commit is contained in:
parent
5e11b2df9d
commit
e087c6fd8c
2 changed files with 82 additions and 130 deletions
|
|
@ -3,13 +3,16 @@
|
||||||
*/
|
*/
|
||||||
package akka.docs.actor
|
package akka.docs.actor
|
||||||
|
|
||||||
|
//#test-code
|
||||||
import akka.testkit.AkkaSpec
|
import akka.testkit.AkkaSpec
|
||||||
|
import akka.actor.Props
|
||||||
|
|
||||||
class FSMDocSpec extends AkkaSpec {
|
class FSMDocSpec extends AkkaSpec {
|
||||||
|
|
||||||
"simple finite state machine" must {
|
"simple finite state machine" must {
|
||||||
|
//#fsm-code-elided
|
||||||
//#simple-imports
|
//#simple-imports
|
||||||
import akka.actor.{ Actor, ActorRef, FSM, Props }
|
import akka.actor.{ Actor, ActorRef, FSM }
|
||||||
import akka.util.duration._
|
import akka.util.duration._
|
||||||
//#simple-imports
|
//#simple-imports
|
||||||
//#simple-events
|
//#simple-events
|
||||||
|
|
@ -23,11 +26,11 @@ class FSMDocSpec extends AkkaSpec {
|
||||||
//#simple-events
|
//#simple-events
|
||||||
//#simple-state
|
//#simple-state
|
||||||
// states
|
// states
|
||||||
trait State
|
sealed trait State
|
||||||
case object Idle extends State
|
case object Idle extends State
|
||||||
case object Active extends State
|
case object Active extends State
|
||||||
|
|
||||||
trait Data
|
sealed trait Data
|
||||||
case object Uninitialized extends Data
|
case object Uninitialized extends Data
|
||||||
case class Todo(target: ActorRef, queue: Seq[Any]) extends Data
|
case class Todo(target: ActorRef, queue: Seq[Any]) extends Data
|
||||||
//#simple-state
|
//#simple-state
|
||||||
|
|
@ -40,20 +43,20 @@ class FSMDocSpec extends AkkaSpec {
|
||||||
case Event(SetTarget(ref), Uninitialized) ⇒ stay using Todo(ref, Vector.empty)
|
case Event(SetTarget(ref), Uninitialized) ⇒ stay using Todo(ref, Vector.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
//#simple-transition
|
//#transition-elided
|
||||||
onTransition {
|
onTransition {
|
||||||
case Active -> Idle ⇒
|
case Active -> Idle ⇒
|
||||||
stateData match {
|
stateData match {
|
||||||
case Todo(ref, queue) ⇒ ref ! Batch(queue)
|
case Todo(ref, queue) ⇒ ref ! Batch(queue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#simple-transition
|
//#transition-elided
|
||||||
|
|
||||||
when(Active, stateTimeout = 1 second) {
|
when(Active, stateTimeout = 1 second) {
|
||||||
case Event(Flush | FSM.StateTimeout, t: Todo) ⇒ goto(Idle) using t.copy(queue = Vector.empty)
|
case Event(Flush | FSM.StateTimeout, t: Todo) ⇒ goto(Idle) using t.copy(queue = Vector.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
//#simple-unhandled
|
//#unhandled-elided
|
||||||
whenUnhandled {
|
whenUnhandled {
|
||||||
// common code for both states
|
// common code for both states
|
||||||
case Event(Queue(obj), t @ Todo(_, v)) ⇒
|
case Event(Queue(obj), t @ Todo(_, v)) ⇒
|
||||||
|
|
@ -63,11 +66,12 @@ class FSMDocSpec extends AkkaSpec {
|
||||||
log.warning("received unhandled request {} in state {}/{}", e, stateName, s)
|
log.warning("received unhandled request {} in state {}/{}", e, stateName, s)
|
||||||
stay
|
stay
|
||||||
}
|
}
|
||||||
//#simple-unhandled
|
//#unhandled-elided
|
||||||
|
|
||||||
initialize
|
initialize
|
||||||
}
|
}
|
||||||
//#simple-fsm
|
//#simple-fsm
|
||||||
|
//#fsm-code-elided
|
||||||
|
|
||||||
"batch correctly" in {
|
"batch correctly" in {
|
||||||
val buncher = system.actorOf(Props(new Buncher))
|
val buncher = system.actorOf(Props(new Buncher))
|
||||||
|
|
@ -87,7 +91,6 @@ class FSMDocSpec extends AkkaSpec {
|
||||||
buncher ! Queue(42)
|
buncher ! Queue(42)
|
||||||
expectNoMsg
|
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
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue