rewrite FSM docs in reST
- also set add_function_parentheses to False (that is really annoying) - also set show_authors to True (that is a matter of taste)
This commit is contained in:
parent
34c16266ed
commit
e952569c7a
4 changed files with 436 additions and 220 deletions
|
|
@ -12,7 +12,7 @@ extensions = ['sphinx.ext.todo']
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
source_suffix = '.rst'
|
source_suffix = '.rst'
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
exclude_patterns = ['_build', 'pending']
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
project = u'Akka'
|
project = u'Akka'
|
||||||
copyright = u'2009-2011, Scalable Solutions AB'
|
copyright = u'2009-2011, Scalable Solutions AB'
|
||||||
|
|
@ -21,6 +21,8 @@ release = '1.1'
|
||||||
|
|
||||||
pygments_style = 'akka'
|
pygments_style = 'akka'
|
||||||
highlight_language = 'scala'
|
highlight_language = 'scala'
|
||||||
|
add_function_parentheses = False
|
||||||
|
show_authors = True
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ Contents
|
||||||
pending/fault-tolerance-java
|
pending/fault-tolerance-java
|
||||||
pending/fault-tolerance-scala
|
pending/fault-tolerance-scala
|
||||||
pending/Feature Stability Matrix
|
pending/Feature Stability Matrix
|
||||||
pending/fsm-scala
|
manual/fsm-scala
|
||||||
pending/futures-scala
|
pending/futures-scala
|
||||||
pending/getting-started
|
pending/getting-started
|
||||||
pending/guice-integration
|
pending/guice-integration
|
||||||
|
|
|
||||||
432
akka-docs/manual/fsm-scala.rst
Normal file
432
akka-docs/manual/fsm-scala.rst
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
FSM
|
||||||
|
===
|
||||||
|
|
||||||
|
.. sidebar:: Contents
|
||||||
|
|
||||||
|
.. contents:: :local:
|
||||||
|
|
||||||
|
.. module:: FSM
|
||||||
|
:platform: Scala
|
||||||
|
:synopsis: Finite State Machine DSL on top of Actors
|
||||||
|
.. moduleauthor:: Irmo Manie, Roland Kuhn
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
|
||||||
|
Module stability: **STABLE**
|
||||||
|
|
||||||
|
Overview
|
||||||
|
++++++++
|
||||||
|
|
||||||
|
The FSM (Finite State Machine) is available as a mixin for the akka Actor and
|
||||||
|
is best described in the `Erlang design principles
|
||||||
|
<http://www.erlang.org/documentation/doc-4.8.2/doc/design_principles/fsm.html>`_
|
||||||
|
|
||||||
|
A FSM can be described as a set of relations of the form:
|
||||||
|
|
||||||
|
**State(S) x Event(E) -> Actions (A), State(S')**
|
||||||
|
|
||||||
|
These relations are interpreted as meaning:
|
||||||
|
|
||||||
|
*If we are in state S and the event E occurs, we should perform the actions A and make a transition to the state S'.*
|
||||||
|
|
||||||
|
A Simple Example
|
||||||
|
++++++++++++++++
|
||||||
|
|
||||||
|
To demonstrate the usage of states we start with a simple FSM without state
|
||||||
|
data. The state can be of any type so for this example we create the states A,
|
||||||
|
B and C.
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
sealed trait ExampleState
|
||||||
|
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 behaviour.
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
import akka.actor.{Actor, FSM}
|
||||||
|
import akka.event.EventHandler
|
||||||
|
import FSM._
|
||||||
|
import akka.util.duration._
|
||||||
|
|
||||||
|
case object Move
|
||||||
|
|
||||||
|
class ABC extends Actor with FSM[ExampleState, Unit] {
|
||||||
|
|
||||||
|
startWith(A, Unit)
|
||||||
|
|
||||||
|
when(A) {
|
||||||
|
case Ev(Move) =>
|
||||||
|
EventHandler.info(this, "Go to B and move on after 5 seconds")
|
||||||
|
goto(B) forMax (5 seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
when(B) {
|
||||||
|
case Ev(StateTimeout) =>
|
||||||
|
EventHandler.info(this, "Moving to C")
|
||||||
|
goto(C)
|
||||||
|
}
|
||||||
|
|
||||||
|
when(C) {
|
||||||
|
case Ev(Move) =>
|
||||||
|
EventHandler.info(this, "Stopping")
|
||||||
|
stop
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize // this checks validity of the initial state and sets up timeout if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
Each state is described by one or more :func:`when(state)` blocks; if more than
|
||||||
|
one is given for the same state, they are tried in the order given until the
|
||||||
|
first is found which matches the incoming event. Events are matched using
|
||||||
|
either :func:`Ev(msg)` (if no state data are to be extracted) or
|
||||||
|
:func:`Event(msg, data)`, see below. The statements for each case are the
|
||||||
|
actions to be taken, where the final expression must describe the transition
|
||||||
|
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 FSM can also hold state data associated with the internal state of the
|
||||||
|
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
|
||||||
|
|
||||||
|
sealed trait LockState
|
||||||
|
case object Locked extends LockState
|
||||||
|
case object Open extends LockState
|
||||||
|
|
||||||
|
Now we can create a lock FSM that takes :class:`LockState` as a state and a
|
||||||
|
:class:`String` as state data:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
class Lock(code: String) extends Actor with FSM[LockState, String] {
|
||||||
|
|
||||||
|
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
|
||||||
|
+++++++++
|
||||||
|
|
||||||
|
This section describes the DSL in a more formal way, refer to `Examples`_ for more sample material.
|
||||||
|
|
||||||
|
The FSM Trait and Object
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
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
|
||||||
|
obvious that an actor is actually created. Importing all members of the
|
||||||
|
:obj:`FSM` object is recommended to receive useful implicits and directly
|
||||||
|
access the symbols like :obj:`StateTimeout`. This import is usually placed
|
||||||
|
inside the state machine definition:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
class MyFSM extends Actor with FSM[State, Data] {
|
||||||
|
import FSM._
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
The :class:`FSM` trait takes two type parameters:
|
||||||
|
|
||||||
|
#. the supertype of all state names, usually a sealed trait with case objects
|
||||||
|
extending it,
|
||||||
|
#. the type of the state data which are tracked by the :class:`FSM` module
|
||||||
|
itself.
|
||||||
|
|
||||||
|
.. _fsm-philosophy:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The state data together with the state name describe the internal state of
|
||||||
|
the state machine; if you stick to this scheme and do not add mutable fields
|
||||||
|
to the FSM class you have the advantage of making all changes of the
|
||||||
|
internal state explicit in a few well-known places.
|
||||||
|
|
||||||
|
Defining States
|
||||||
|
---------------
|
||||||
|
|
||||||
|
A state is defined by one or more invocations of the method
|
||||||
|
|
||||||
|
:func:`when(<name>[, stateTimeout = <timeout>])(stateFunction)`.
|
||||||
|
|
||||||
|
The given name must be an object which is type-compatible with the first type
|
||||||
|
parameter given to the :class:`FSM` trait. This object is used as a hash key,
|
||||||
|
so you must ensure that it properly implements :meth:`equals` and
|
||||||
|
:meth:`hashCode`; in particular it must not be mutable. The easiest fit for
|
||||||
|
these requirements are case objects.
|
||||||
|
|
||||||
|
If the :meth:`stateTimeout` parameter is given, then all transitions into this
|
||||||
|
state, including staying, receive this timeout by default. Initiating the
|
||||||
|
transition with an explicit timeout may be used to override this default, see
|
||||||
|
`Initiating Transitions`_ for more information.
|
||||||
|
|
||||||
|
The :meth:`stateFunction` argument is a :class:`PartialFunction[Event, State]`,
|
||||||
|
which is conveniently given using the partial function literal syntax as
|
||||||
|
demonstrated below:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
when(Idle) {
|
||||||
|
case Ev(Start(msg)) => // convenience extractor when state data not needed
|
||||||
|
goto(Timer) using (msg, self.channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
when(Timer, stateTimeout = 12 seconds) {
|
||||||
|
case Event(StateTimeout, (msg, channel)) =>
|
||||||
|
channel ! msg
|
||||||
|
goto(Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
The :class:`Event(msg, data)` case class may be used directly in the pattern as
|
||||||
|
shown in state Idle, or you may use the extractor :obj:`Ev(msg)` when the state
|
||||||
|
data are not needed.
|
||||||
|
|
||||||
|
Defining the Initial State
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Each FSM needs a starting point, which is declared using
|
||||||
|
|
||||||
|
:func:`startWith(state, data[, timeout])`
|
||||||
|
|
||||||
|
The optionally given timeout argument overrides any specification given for the
|
||||||
|
desired initial state. If you want to cancel a default timeout, use
|
||||||
|
:obj:`Duration.Inf`.
|
||||||
|
|
||||||
|
Unhandled Events
|
||||||
|
----------------
|
||||||
|
|
||||||
|
If a state doesn't handle a received event a warning is logged. If you want to
|
||||||
|
do something else in this case you can specify that with
|
||||||
|
:func:`whenUnhandled(stateFunction)`:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
whenUnhandled {
|
||||||
|
case Event(x : X, data) =>
|
||||||
|
EventHandler.info(this, "Received unhandled event: " + x)
|
||||||
|
stay
|
||||||
|
case Ev(msg) =>
|
||||||
|
EventHandler.warn(this, "Received unknown event: " + x)
|
||||||
|
goto(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
**IMPORTANT**: This handler is not stacked, meaning that each invocation of
|
||||||
|
:func:`whenUnhandled` replaces the previously installed handler.
|
||||||
|
|
||||||
|
Initiating Transitions
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The result of any :obj:`stateFunction` must be a definition of the next state
|
||||||
|
unless terminating the FSM, which is described in `Termination`_. The state
|
||||||
|
definition can either be the current state, as described by the :func:`stay`
|
||||||
|
directive, or it is a different state as given by :func:`goto(state)`. The
|
||||||
|
resulting object allows further qualification by way of the modifiers described
|
||||||
|
in the following:
|
||||||
|
|
||||||
|
:meth:`forMax(duration)`
|
||||||
|
This modifier sets a state timeout on the next state. This means that a timer
|
||||||
|
is started which upon expiry sends a :obj:`StateTimeout` message to the FSM.
|
||||||
|
This timer is canceled upon reception of any other message in the meantime;
|
||||||
|
you can rely on the fact that the :obj:`StateTimeout` message will not be
|
||||||
|
processed after an intervening message.
|
||||||
|
|
||||||
|
This modifier can also be used to override any default timeout which is
|
||||||
|
specified for the target state. If you want to cancel the default timeout,
|
||||||
|
use :obj:`Duration.Inf`.
|
||||||
|
|
||||||
|
:meth:`using(data)`
|
||||||
|
This modifier replaces the old state data with the new data given. If you
|
||||||
|
follow the advice :ref:`above <fsm-philosophy>`, this is the only place where
|
||||||
|
internal state data are ever modified.
|
||||||
|
|
||||||
|
:meth:`replying(msg)`
|
||||||
|
This modifier sends a reply to the currently processed message and otherwise
|
||||||
|
does not modify the state transition.
|
||||||
|
|
||||||
|
All modifier can be chained to achieve a nice and concise description:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
when(State) {
|
||||||
|
case Ev(msg) =>
|
||||||
|
goto(Processing) using (msg) forMax (5 seconds) replying (WillDo)
|
||||||
|
}
|
||||||
|
|
||||||
|
The parentheses are not actually needed in all cases, but they visually
|
||||||
|
distinguish between modifiers and their arguments and therefore make the code
|
||||||
|
even more pleasant to read for foreigners.
|
||||||
|
|
||||||
|
Monitoring Transitions
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Transitions occur "between states" conceptually, which means after any actions
|
||||||
|
you have put into the event handling block; this is obvious since the next
|
||||||
|
state is only defined by the value returned by the event handling logic. You do
|
||||||
|
not need to worry about the exact order with respect to setting the internal
|
||||||
|
state variable, as everything within the FSM actor is running single-threaded
|
||||||
|
anyway.
|
||||||
|
|
||||||
|
Internal Monitoring
|
||||||
|
*******************
|
||||||
|
|
||||||
|
Up to this point, the FSM DSL has been centered on states and events. The dual
|
||||||
|
view is to describe it as a series of transitions. This is enabled by the
|
||||||
|
method
|
||||||
|
|
||||||
|
:func:`onTransition(handler)`
|
||||||
|
|
||||||
|
which associates actions with a transition instead of with a state and event.
|
||||||
|
The handler is a partial function which takes a pair of states as input; no
|
||||||
|
resulting state is needed as it is not possible to modify the transition in
|
||||||
|
progress.
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
onTransition {
|
||||||
|
case Idle -> Active => setTimer("timeout")
|
||||||
|
case Active -> _ => cancelTimer("timeout")
|
||||||
|
case x -> Idle => EventHandler.info("entering Idle from "+x)
|
||||||
|
}
|
||||||
|
|
||||||
|
The convenience extractor :obj:`->` enables decomposition of the pair of states
|
||||||
|
with a clear visual reminder of the transition's direction. As usual in pattern
|
||||||
|
matches, an underscore may be used for irrelevant parts; alternatively you
|
||||||
|
could bind the unconstrained state to a variable, e.g. for logging as shown in
|
||||||
|
the last case.
|
||||||
|
|
||||||
|
It is also possible to pass a function object accepting two states to
|
||||||
|
:func:`onTransition`, in case your transition handling logic is implemented as
|
||||||
|
a method:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
onTransition(handler _)
|
||||||
|
|
||||||
|
private def handler(from: State, to: State) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
The handlers registered with this method are stacked, so you can intersperse
|
||||||
|
:func:`onTransition` blocks with :func:`when` blocks as suits your design. It
|
||||||
|
should be noted, however, that *all handlers will be invoked for each
|
||||||
|
transition*, not only the first matching one. This is designed specifically so
|
||||||
|
you can put all transition handling for a certain aspect into one place without
|
||||||
|
having to worry about earlier declarations shadowing later ones; the actions
|
||||||
|
are still executed in declaration order, though.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This kind of internal monitoring may be used to structure your FSM according
|
||||||
|
to transitions, so that for example the cancellation of a timer upon leaving
|
||||||
|
a certain state cannot be forgot when adding new target states.
|
||||||
|
|
||||||
|
External Monitoring
|
||||||
|
*******************
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Registering a not-running listener generates a warning and fails gracefully.
|
||||||
|
Stopping a listener without unregistering will remove the listener from the
|
||||||
|
subscription list upon the next transition.
|
||||||
|
|
||||||
|
Termination
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The FSM is stopped by specifying the result state as
|
||||||
|
|
||||||
|
:func:`stop([reason[, data]])`
|
||||||
|
|
||||||
|
The reason must be one of :obj:`Normal` (which is the default), :obj:`Shutdown`
|
||||||
|
or :obj:`Failure(reason)`, and the second argument may be given to change the
|
||||||
|
state data which is available during termination handling.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
It should be noted that :func:`stop` does not abort the actions and stop the
|
||||||
|
FSM immediately. The stop action must be returned from the event handler in
|
||||||
|
the same way as a state transition.
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
when(A) {
|
||||||
|
case Ev(Stop) =>
|
||||||
|
doCleanup()
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
You can use :func:`onTermination(handler)` to specify custom code that is
|
||||||
|
executed when the FSM is stopped. The handler is a partial function which takes
|
||||||
|
a :class:`StopEvent(reason, stateName, stateData)` as argument:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
onTermination {
|
||||||
|
case StopEvent(Normal, s, d) => ...
|
||||||
|
case StopEvent(Shutdown, _, _) => ...
|
||||||
|
case StopEvent(Failure(cause), s, d) => ...
|
||||||
|
}
|
||||||
|
|
||||||
|
As for the :func:`whenUnhandled` case, this handler is not stacked, so each
|
||||||
|
invocation of :func:`onTermination` replaces the previously installed handler.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
++++++++
|
||||||
|
|
||||||
|
A bigger FSM example can be found in the sources:
|
||||||
|
|
||||||
|
* `Dining Hakkers using FSM <https://github.com/jboner/akka/blob/master/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala#L1>`_
|
||||||
|
* `Dining Hakkers using become <https://github.com/jboner/akka/blob/master/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala#L1>`_
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
FSM
|
|
||||||
===
|
|
||||||
|
|
||||||
Module stability: **STABLE**
|
|
||||||
|
|
||||||
The FSM (Finite State Machine) is available as a mixin for the akka Actor and is best described in the `Erlang design principals <@http://www.erlang.org/documentation/doc-4.8.2/doc/design_principles/fsm.html>`_
|
|
||||||
|
|
||||||
A FSM can be described as a set of relations of the form:
|
|
||||||
> **State(S) x Event(E) -> Actions (A), State(S')**
|
|
||||||
|
|
||||||
These relations are interpreted as meaning:
|
|
||||||
> *If we are in state S and the event E occurs, we should perform the actions A and make a transition to the state S'.*
|
|
||||||
|
|
||||||
State Definitions
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
To demonstrate the usage of states we start with a simple state only FSM without state data. The state can be of any type so for this example we create the states A, B and C.
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
sealed trait ExampleState
|
|
||||||
case object A extends ExampleState
|
|
||||||
case object B extends ExampleState
|
|
||||||
case object C extends ExampleState
|
|
||||||
|
|
||||||
Now lets create an object to influence the FSM and define the states and their behaviour.
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
import akka.actor.{Actor, FSM}
|
|
||||||
import FSM._
|
|
||||||
import akka.util.duration._
|
|
||||||
|
|
||||||
case object Move
|
|
||||||
|
|
||||||
class ABC extends Actor with FSM[ExampleState,Unit] {
|
|
||||||
|
|
||||||
startWith(A, Unit)
|
|
||||||
|
|
||||||
when(A) {
|
|
||||||
case Event(Move, _) =>
|
|
||||||
log.info("Go to B and move on after 5 seconds")
|
|
||||||
goto(B) forMax (5 seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
when(B) {
|
|
||||||
case Event(StateTimeout, _) =>
|
|
||||||
log.info("Moving to C")
|
|
||||||
goto(C)
|
|
||||||
}
|
|
||||||
|
|
||||||
when(C) {
|
|
||||||
case Event(Move, _) =>
|
|
||||||
log.info("Stopping")
|
|
||||||
stop
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize // this checks validity of the initial state and sets up timeout if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
So we use 'when' to specify a state and define what needs to happen when we receive an event. We use 'goto' to go to another state. We use 'forMax' to tell for how long we maximum want to stay in that state before we receive a timeout notification. We use 'stop' to stop the FSM. And we use 'startWith' to specify which state to start with. The call to 'initialize' should be the last action done in the actor constructor.
|
|
||||||
|
|
||||||
If we want to stay in the current state we can use (I'm hoping you can guess this by now) 'stay'. That can also be combined with the 'forMax'
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
when(C) {
|
|
||||||
case Event(unknown, _) =>
|
|
||||||
stay forMax (2 seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
The timeout can also be associated with the state itself, the choice depends on whether most of the transitions to the state require the same value for the timeout:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
when(A) {
|
|
||||||
case Ev(Start(msg)) => // convenience extractor when state data not needed
|
|
||||||
goto(Timer) using msg
|
|
||||||
}
|
|
||||||
|
|
||||||
when(B, stateTimeout = 12 seconds) {
|
|
||||||
case Event(StateTimeout, msg) =>
|
|
||||||
target ! msg
|
|
||||||
case Ev(DifferentPause(dur : Duration)) =>
|
|
||||||
stay forMax dur // overrides default state timeout for this single transition
|
|
||||||
}
|
|
||||||
|
|
||||||
Unhandled Events
|
|
||||||
----------------
|
|
||||||
|
|
||||||
If a state doesn't handle a received event a warning is logged. If you want to do something with this events you can specify that with 'whenUnhandled'
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
whenUnhandled {
|
|
||||||
case Event(x, _) => log.info("Received unhandled event: " + x)
|
|
||||||
}
|
|
||||||
|
|
||||||
Termination
|
|
||||||
-----------
|
|
||||||
|
|
||||||
You can use 'onTermination' to specify custom code that is executed when the FSM is stopped. A reason is passed to tell how the FSM was stopped.
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
onTermination {
|
|
||||||
case Normal => log.info("Stopped normal")
|
|
||||||
case Shutdown => log.info("Stopped because of shutdown")
|
|
||||||
case Failure(cause) => log.error("Stopped because of failure: " + cause)
|
|
||||||
}
|
|
||||||
|
|
||||||
State Transitions
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
When state transitions to another state we might want to know about this and take action. To specify this we can use 'onTransition' to capture the transitions.
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
onTransition {
|
|
||||||
case A -> B => log.info("Moving from A to B")
|
|
||||||
case _ -> C => log.info("Moving from something to C")
|
|
||||||
}
|
|
||||||
|
|
||||||
Multiple onTransition blocks may be given and all will be execution while processing a transition. This enables you to associate your Actions either with the initial state of a processing step, or with the transition into the final state of a processing step.
|
|
||||||
|
|
||||||
Transitions occur "between states" conceptually, which means after any actions you have put into the event handling block; this is obvious since the next state is only defined by the value returned by the event handling logic. You do not need to worry about the exact order with respect to setting the internal state variable, as everything within the FSM actor is running single-threaded anyway.
|
|
||||||
|
|
||||||
It is also possible to pass a function object accepting two states to onTransition, in case your state handling logic is implemented as a method:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
onTransition(handler _)
|
|
||||||
|
|
||||||
private def handler(from: State, to: State) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
State Data
|
|
||||||
----------
|
|
||||||
|
|
||||||
The FSM can also hold state data that is attached to every event. The state data can be of any type but to demonstrate let's look at a lock with a String as state data holding the entered unlock code.
|
|
||||||
First we need two states for the lock:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
sealed trait LockState
|
|
||||||
case object Locked extends LockState
|
|
||||||
case object Open extends LockState
|
|
||||||
|
|
||||||
Now we can create a lock FSM that takes LockState as a state and a String as state data:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
import akka.actor.{FSM, Actor}
|
|
||||||
import FSM._
|
|
||||||
import akka.util.duration._
|
|
||||||
|
|
||||||
class Lock(code: String) extends Actor with FSM[LockState, String] {
|
|
||||||
|
|
||||||
val 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 {
|
|
||||||
// not enough digits yet so stay using the incomplete code as the new state data
|
|
||||||
case incomplete if incomplete.length < code.length =>
|
|
||||||
stay using incomplete
|
|
||||||
// code matched the one from the lock so go to Open state and reset the state data
|
|
||||||
case `code` =>
|
|
||||||
log.info("Unlocked")
|
|
||||||
goto(Open) using emptyCode forMax (1 seconds)
|
|
||||||
// wrong code, stay Locked and reset the state data
|
|
||||||
case wrong =>
|
|
||||||
log.error("Wrong code " + wrong)
|
|
||||||
stay using emptyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when(Open) {
|
|
||||||
// after the timeout, go back to Locked state
|
|
||||||
case Event(StateTimeout, _) => {
|
|
||||||
log.info("Locked")
|
|
||||||
goto(Locked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startWith(Locked, emptyCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
To use the Lock you can run a small program like this:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
|
||||||
|
|
||||||
object Lock {
|
|
||||||
|
|
||||||
def main(args: Array[String]) {
|
|
||||||
|
|
||||||
val lock = Actor.actorOf(new Lock("1234")).start()
|
|
||||||
|
|
||||||
lock ! '1'
|
|
||||||
lock ! '2'
|
|
||||||
lock ! '3'
|
|
||||||
lock ! '4'
|
|
||||||
|
|
||||||
Actor.registry.shutdownAll()
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Dining Hakkers
|
|
||||||
--------------
|
|
||||||
|
|
||||||
A bigger FSM example can be found in the sources.
|
|
||||||
`Dining Hakkers using FSM <@https://github.com/jboner/akka/blob/master/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala#L1>`_
|
|
||||||
`Dining Hakkers using become <@https://github.com/jboner/akka/blob/master/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnBecome.scala#L1>`_
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue