FSM

    示例项目

    你可以查看「」,以了解实际应用中的情况。

    FSM(有限状态机)是一个抽象的基类,它实现了一个 Akka Actor,并在「Erlang设 计原则」中得到了最好的描述。

    FSM 可以描述为一组形式的关系:

    • State(S) x Event(E) -> Actions (A), State(S’)

    这些关系被解释为如下含义:

    • 如果我们处于状态S,并且事件E发生,那么我们应该执行操作A,并向状态S’过渡。

    一个简单的例子

    为了演示AbstractFSM类的大部分特性,考虑一个 Actor,该 Actor 在消息到达突发(burst)时接收和排队消息,并在突发结束或收到刷新(flush)请求后发送它们。

    首先,考虑使用以下所有导入语句:

    1. import akka.actor.AbstractFSM;
    2. import akka.actor.ActorRef;
    3. import akka.japi.pf.UnitMatch;
    4. import java.util.Arrays;
    5. import java.util.LinkedList;
    6. import java.util.List;
    7. import java.time.Duration;

    我们的“Buncher” Actor 的协议(contract)是接受或产生以下信息:

    1. static final class SetTarget {
    2. private final ActorRef ref;
    3. public SetTarget(ActorRef ref) {
    4. this.ref = ref;
    5. }
    6. public ActorRef getRef() {
    7. return ref;
    8. }
    9. @Override
    10. public String toString() {
    11. return "SetTarget{" + "ref=" + ref + '}';
    12. }
    13. }
    14. static final class Queue {
    15. private final Object obj;
    16. public Queue(Object obj) {
    17. this.obj = obj;
    18. }
    19. public Object getObj() {
    20. return obj;
    21. }
    22. @Override
    23. public String toString() {
    24. return "Queue{" + "obj=" + obj + '}';
    25. }
    26. }
    27. static final class Batch {
    28. private final List<Object> list;
    29. public Batch(List<Object> list) {
    30. this.list = list;
    31. }
    32. public List<Object> getList() {
    33. return list;
    34. }
    35. @Override
    36. public boolean equals(Object o) {
    37. if (this == o) return true;
    38. if (o == null || getClass() != o.getClass()) return false;
    39. Batch batch = (Batch) o;
    40. return list.equals(batch.list);
    41. }
    42. @Override
    43. public int hashCode() {
    44. return list.hashCode();
    45. }
    46. @Override
    47. public String toString() {
    48. final StringBuilder builder = new StringBuilder();
    49. builder.append("Batch{list=");
    50. list.stream()
    51. .forEachOrdered(
    52. e -> {
    53. builder.append(e);
    54. builder.append(",");
    55. });
    56. int len = builder.length();
    57. builder.replace(len, len, "}");
    58. return builder.toString();
    59. }
    60. }
    61. static enum Flush {
    62. Flush
    63. }

    启动它需要SetTarget,为要传递的Batches设置目标;Queue将添加到内部队列,而Flush将标记突发(burst)的结束。

    1. // states
    2. enum State {
    3. Idle,
    4. Active
    5. }
    6. // state data
    7. interface Data {}
    8. enum Uninitialized implements Data {
    9. Uninitialized
    10. }
    11. final class Todo implements Data {
    12. private final ActorRef target;
    13. private final List<Object> queue;
    14. public Todo(ActorRef target, List<Object> queue) {
    15. this.target = target;
    16. this.queue = queue;
    17. }
    18. public ActorRef getTarget() {
    19. return target;
    20. }
    21. public List<Object> getQueue() {
    22. return queue;
    23. }
    24. @Override
    25. return "Todo{" + "target=" + target + ", queue=" + queue + '}';
    26. }
    27. public Todo addElement(Object element) {
    28. List<Object> nQueue = new LinkedList<>(queue);
    29. nQueue.add(element);
    30. return new Todo(this.target, nQueue);
    31. }
    32. public Todo copy(List<Object> queue) {
    33. return new Todo(this.target, queue);
    34. }
    35. public Todo copy(ActorRef target) {
    36. return new Todo(target, this.queue);
    37. }
    38. }

    Actor 可以处于两种状态:没有消息排队(即Idle)或有消息排队(即Active)。它将保持Active状态,只要消息一直到达并且不请求刷新。Actor 的内部状态数据由发送的目标 Actor 引用和消息的实际队列组成。

    现在让我们来看看我们的FSM Actor 的结构(skeleton):

    1. public class Buncher extends AbstractFSM<State, Data> {
    2. {
    3. startWith(Idle, Uninitialized);
    4. when(
    5. Idle,
    6. matchEvent(
    7. SetTarget.class,
    8. Uninitialized.class,
    9. (setTarget, uninitialized) ->
    10. stay().using(new Todo(setTarget.getRef(), new LinkedList<>()))));
    11. onTransition(
    12. matchState(
    13. Active,
    14. Idle,
    15. () -> {
    16. // reuse this matcher
    17. UnitMatch.create(
    18. matchData(
    19. Todo.class,
    20. todo ->
    21. todo.getTarget().tell(new Batch(todo.getQueue()), getSelf())));
    22. m.match(stateData());
    23. })
    24. .state(
    25. Idle,
    26. Active,
    27. () -> {
    28. /* Do something here */
    29. }));
    30. when(
    31. Active,
    32. Duration.ofSeconds(1L),
    33. matchEvent(
    34. Arrays.asList(Flush.class, StateTimeout()),
    35. Todo.class,
    36. (event, todo) -> goTo(Idle).using(todo.copy(new LinkedList<>()))));
    37. whenUnhandled(
    38. matchEvent(
    39. Queue.class,
    40. Todo.class,
    41. (queue, todo) -> goTo(Active).using(todo.addElement(queue.getObj())))
    42. .anyEvent(
    43. (event, state) -> {
    44. log()
    45. .warning(
    46. "received unhandled request {} in state {}/{}",
    47. event,
    48. stateName(),
    49. state);
    50. return stay();
    51. }));
    52. initialize();
    53. }
    54. }

    基本策略是通过继承AbstractFSM类并将可能的状态和数据值指定为类型参数来声明 Actor。在 Actor 的主体中,DSL 用于声明状态机:

    • startWith定义初始状态和初始数据
    • when(<state>) { ... }是要处理的每个状态的声明(可能是多个状态,传递的PartialFunction将使用orElse连接)
    • 最后使用initialize启动它,它执行到初始状态的转换并设置定时器(如果需要)。

    在这种情况下,我们从Idle状态开始,使用Uninitialized数据,其中只处理SetTarget()消息;stay准备结束此事件的处理,以避免离开当前状态,而using修饰符使 FSM 用包含目标 Actor 引用的Todo()对象替换内部状态(此时Uninitialized在这个点)。Active状态已声明状态超时,这意味着如果在 1 秒内没有收到消息,将生成FSM.StateTimeout 消息。这与在这种情况下接收Flush命令的效果相同,即转换回Idle状态并将内部队列重置为空向量。但是消息是如何排队的呢?由于这两种状态下的工作方式相同,因此我们利用以下事实:未由when()块处理的任何事件都传递给whenUnhandled()块:

    1. whenUnhandled(
    2. matchEvent(
    3. Queue.class,
    4. Todo.class,
    5. (queue, todo) -> goTo(Active).using(todo.addElement(queue.getObj())))
    6. .anyEvent(
    7. (event, state) -> {
    8. log()
    9. .warning(
    10. "received unhandled request {} in state {}/{}",
    11. event,
    12. stateName(),
    13. state);
    14. return stay();
    15. }));

    这里处理的第一个案例是将Queue() 请求添加到内部队列并进入Active状态(如果已经存在的话,这显然会保持Active状态),但前提是在接收到Queue()事件时,FSM 数据没有Uninitialized。否则,在所有其他未处理的情况下,第二种情况只会记录一个警告,而不会更改内部状态。

    唯一缺少的部分是Batches实际发送到目标的位置,为此我们使用了onTransition机制:你可以声明多个这样的块,如果发生状态转换(即只有当状态实际更改时),所有这些块都将尝试匹配行为。

    1. onTransition(
    2. matchState(
    3. Active,
    4. Idle,
    5. () -> {
    6. // reuse this matcher
    7. final UnitMatch<Data> m =
    8. UnitMatch.create(
    9. matchData(
    10. Todo.class,
    11. todo ->
    12. todo.getTarget().tell(new Batch(todo.getQueue()), getSelf())));
    13. m.match(stateData());
    14. })
    15. .state(
    16. Idle,
    17. Active,
    18. () -> {
    19. /* Do something here */
    20. }));

    转换回调是由matchState构造的一个生成器,后跟零或多个state,它将当前state和下一个state作为一对状态的输入。在状态更改期间,旧的状态数据通过stateData()可用,如展示的这样,新的状态数据将作为nextStateData()可用。

    • 注释:可以使用goto(S)stay()实现相同的状态转换(当前处于状态S时)。不同之处在于,goto(S)会发出一个事件S->S,该事件可以由onTransition处理,而stay()则不会。

    为了验证这个Buncher是否真的有效,使用「TestKit」编写一个测试非常容易,这里使用 JUnit 作为示例:

    1. public class BuncherTest extends AbstractJavaTest {
    2. static ActorSystem system;
    3. @BeforeClass
    4. public static void setup() {
    5. system = ActorSystem.create("BuncherTest");
    6. }
    7. @AfterClass
    8. public static void tearDown() {
    9. TestKit.shutdownActorSystem(system);
    10. system = null;
    11. }
    12. @Test
    13. public void testBuncherActorBatchesCorrectly() {
    14. new TestKit(system) {
    15. {
    16. final ActorRef buncher = system.actorOf(Props.create(Buncher.class));
    17. final ActorRef probe = getRef();
    18. buncher.tell(new SetTarget(probe), probe);
    19. buncher.tell(new Queue(43), probe);
    20. LinkedList<Object> list1 = new LinkedList<>();
    21. list1.add(42);
    22. list1.add(43);
    23. expectMsgEquals(new Batch(list1));
    24. buncher.tell(new Queue(44), probe);
    25. buncher.tell(Flush, probe);
    26. buncher.tell(new Queue(45), probe);
    27. LinkedList<Object> list2 = new LinkedList<>();
    28. list2.add(44);
    29. expectMsgEquals(new Batch(list2));
    30. LinkedList<Object> list3 = new LinkedList<>();
    31. list3.add(45);
    32. expectMsgEquals(new Batch(list3));
    33. system.stop(buncher);
    34. }
    35. };
    36. }
    37. @Test
    38. public void testBuncherActorDoesntBatchUninitialized() {
    39. new TestKit(system) {
    40. {
    41. final ActorRef buncher = system.actorOf(Props.create(Buncher.class));
    42. final ActorRef probe = getRef();
    43. buncher.tell(new Queue(42), probe);
    44. expectNoMessage();
    45. system.stop(buncher);
    46. }
    47. };
    48. }
    49. }

    抽象类是用于实现 FSM 的基类。它实现了 Actor,因为创建了一个 Actor 来驱动 FSM。

    • 注释AbstractFSM类定义了一个receive方法,该方法处理内部消息,并将其他所有信息传递给 FSM 逻辑(根据当前状态)。当覆盖receive方法时,请记住,例如状态超时处理取决于通过 FSM 逻辑实际传递消息。

    AbstractFSM类采用两个类型参数:

    • 所有状态名的父类型,通常是枚举
    • AbstractFSM模块本身跟踪的状态数据的类型。

    定义状态

    状态由方法的一个或多个调用定义。

    • when(<name>[, stateTimeout = <timeout>])(stateFunction)

    给定的名称必须是与AbstractFSM类的第一个类型参数类型兼容的对象。此对象用作哈希键,因此必须确保它正确实现equalshashCode;尤其是它不能是可变的。最适合这些需求的是case对象。

    如果给定stateTimeout参数,那么默认情况下,所有转换到该状态(包括保持)的操作都将接收该超时。使用显式超时启动转换可用于重写此默认值,有关详细信息,请参阅「Initiating Transitions」。在使用setStateTimeout(state, duration)进行操作处理期间,可以更改任何状态的状态超时。这将启用运行时配置,例如通过外部消息。

    stateFunction参数是一个PartialFunction[Event, State],它使用状态函数生成器语法方便地给出,如下所示:

    1. when(
    2. Idle,
    3. matchEvent(
    4. SetTarget.class,
    5. Uninitialized.class,
    6. (setTarget, uninitialized) ->
    7. stay().using(new Todo(setTarget.getRef(), new LinkedList<>()))));
    • 警告:需要为每个可能的 FSM 状态定义处理程序,否则在尝试切换到未声明的状态时将出现故障。

    建议将状态声明为枚举,然后验证每个状态都有一个when子句。如果要使状态的处理“unhandled”(下面将详细介绍),则仍需要这样声明:

    1. when(SomeState, AbstractFSM.NullFunction());

    定义初始状态

    每个 FSM 都需要一个起点(starting point),该起点使用:

    1. startWith(state, data[, timeout])

    可选的给定超时参数重写为所需初始状态给定的任何规范。如果要取消默认超时,请使用Duration.Inf

    未处理的事件

    如果状态不处理接收到的事件,则会记录警告。如果要在这种情况下执行其他操作,可以使用whenUnhandled(stateFunction)指定:

    1. whenUnhandled(
    2. matchEvent(
    3. X.class,
    4. (x, data) -> {
    5. log().info("Received unhandled event: " + x);
    6. return stay();
    7. })
    8. .anyEvent(
    9. (event, data) -> {
    10. log().warning("Received unknown event: " + event);
    11. return goTo(Error);
    12. }));
    13. }

    在此处理程序中,可以使用stateName方法查询 FSM 的状态。

    • 重要的:此处理程序不是堆叠的,这意味着每次调用whenUnhandled都会替换先前安装的(installed)处理程序。

    任何stateFunction的结果都必须是下一个状态的定义,除非终止 FSM,如「」。状态定义可以是当前状态(如stay指令所述),也可以是goto(state)给出的不同状态。结果对象允许通过下面描述的修饰符进一步限定:

    • forMax(duration),此修饰符设置下一个状态的状态超时。这意味着计时器(timer)启动,到期时向 FSM 发送StateTimeout消息。此计时器在同时接收到任何其他消息时被取消;你可以依赖这样一个事实,即在干预消息之后将不会处理StateTimeout消息。此修饰符还可用于重写为目标状态指定的任何默认超时。如果要取消默认超时,请使用Duration.Inf
    • using(data),此修饰符将旧状态数据替换为给定的新数据。如果你遵循上面的建议,这是唯一一个修改内部状态数据的地方。
    • replying(msg),此修饰符向当前处理的消息发送答复,否则不会修改状态转换。

    所有修饰符都可以链接起来,以实现一个漂亮简洁的描述:

    1. when(
    2. SomeState,
    3. matchAnyEvent(
    4. (msg, data) -> {
    5. return goTo(Processing)
    6. .using(newData)
    7. .forMax(Duration.ofSeconds(5))
    8. .replying(WillDo);
    9. }));

    实际上并非所有情况下都需要括号,但它们在视觉上区分修饰符和它们的参数,因此使代码更易于阅读。

    • 注释:请注意,return语句不能在when块或类似块中使用;这是一个 Scala 限制。使用if () ... else ...或者将其移动到方法定义中。

    监视转换

    概念上,“状态之间”会发生转换,这意味着在将任何操作放入事件处理块之后,这是显而易见的,因为下一个状态仅由事件处理逻辑返回的值定义。你不必担心设置内部状态变量的确切顺序,因为 FSM Actor 中的所有内容都在以单线程运行。

    内部监控

    到目前为止,FSM DSL 一直以状态和事件为中心。双视图(dual view)将其描述为一系列转换。这是由方法启用的

    1. onTransition(handler)

    它将动作与转换相关联,而不是与状态和事件相关联。处理程序是一个以一对状态作为输入的部分函数;不需要结果状态,因为无法修改正在进行的转换。

    1. onTransition(
    2. matchState(Idle, Active, () -> setTimer("timeout", Tick, Duration.ofSeconds(1L), true))
    3. .state(Active, null, () -> cancelTimer("timeout"))
    4. .state(null, Idle, (f, t) -> log().info("entering Idle from " + f)));

    也可以将接受两种状态的函数对象传递给onTransition,以将转换处理逻辑实现为一种方法:

    使用此方法注册的处理程序是堆叠(stacked)的,因此你可以在适合你的设计块中散置intersperse块。但是,应该注意的是,要为每个转换(transition)调用所有处理程序,而不仅仅是第一个匹配的处理程序。这是专门设计的,这样你就可以将某个方面的所有转换处理放在一个地方,而不必担心前面的声明会影响后面的声明;不过,操作仍然是按声明顺序执行的。

    • 注释:这种内部监控可用于根据转换构造你的 FSM,例如,在添加新的目标状态时,不能忘记在离开某个状态时取消计时器。

    外部监控

    外部 Actor 可以通过发送消息SubscribeTransitionCallBack(actorRef)来注册以获得状态转换的通知。命名的 Actor 将立即发送一条CurrentState(self, stateName)消息,并在触发状态更改时接收Transition(actorRef, oldState, newState)消息。

    在不注销的情况下停止侦听器(listener)将不会从订阅列表中删除该侦听器;请在停止侦听器之前使用UnsubscribeTransitionCallback

    除了状态超时之外,FSM 还管理由String名称标识的定时器(timers)。你可以使用

    1. setTimer(name, msg, interval, repeat)

    其中msg是将在持续时间interval结束后发送的消息对象。如果repeattrue,则计时器按interval参数给定的固定速率调度。在添加新计时器之前,任何具有相同名称的现有计时器都将自动取消。

    计时器取消可以使用:

    1. cancelTimer(name)

    它保证立即工作,这意味着即使计时器已经启动并将其排队,也不会在调用后处理计划的消息。任何计时器的状态都可以通过以下方式获取:

    1. isTimerActive(name)

    这些命名的计时器补充状态超时,因为它们不受接收其他消息的影响。

    从内部终止

    通过将结果状态指定为以下方式来停止 FSM:

    1. stop([reason[, data]])

    原因必须是Normal(默认)、ShutdownFailure(reason)之一,并且可以给出第二个参数来更改终止处理期间可用的状态数据。

    • 注释:应该注意的是,停止不会中止动作,并立即停止 FSM。停止操作必须以与状态转换相同的方式从事件处理程序返回,但请注意,在when块中不能使用return语句。
    1. when(
    2. Error,
    3. matchEventEquals(
    4. "stop",
    5. (event, data) -> {
    6. // do cleanup ...
    7. return stop();
    8. }));

    可以使用onTermination(handler)指定在 FSM 停止时执行的自定义代码。处理程序是一个分部函数,它以StopEvent(reason, stateName, stateData) 作为参数:

    1. onTermination(
    2. matchStop(
    3. Normal(),
    4. (state, data) -> {
    5. /* Do something here */
    6. })
    7. .stop(
    8. Shutdown(),
    9. (state, data) -> {
    10. /* Do something here */
    11. })
    12. .stop(
    13. Failure.class,
    14. (reason, state, data) -> {
    15. /* Do something here */
    16. }));

    对于whenUnhandled案例,此处理程序不堆叠,因此每次调用onTermination都会替换先前安装的处理程序。

    从外部终止

    当使用stop()方法停止与 FSM 关联的ActorRef时,将执行其postStop钩子。AbstractFSM类的默认实现是在准备处理StopEvent(Shutdown, ...)时执行onTermination处理程序。

    • 警告:如果你重写postStop并希望调用onTermination处理程序,请不要忘记调用super.postStop

    有限状态机的测试和调试

    在开发和故障排除过程中,FSM 和其他 Actor 一样需要关注。如「TestFSMRef」和以下所述,有专门的工具可用。

    事件跟踪

    在「配置」中设置akka.actor.debug.fsm可以通过LoggingFSM实例记录事件跟踪:

    1. static class MyFSM extends AbstractLoggingFSM<StateType, Data> {
    2. @Override
    3. public int logDepth() {
    4. return 12;
    5. }
    6. {
    7. onTermination(
    8. matchStop(
    9. Failure.class,
    10. (reason, state, data) -> {
    11. String lastEvents = getLog().mkString("\n\t");
    12. log()
    13. .warning(
    14. "Failure in state "
    15. + state
    16. + " with data "
    17. + data
    18. + "\n"
    19. + "Events leading up to this point:\n\t"
    20. + lastEvents);
    21. }));
    22. // ...
    23. }
    24. }

    此 FSM 将在DEBUG级别记录日志:

    • 所有已处理的事件,包括StateTimeout和定时计时器消息
    • 每次设置和取消指定计时器
    • 所有状态转换

    生命周期更改和特殊消息可以按照对「」的描述进行记录。

    AbstractLoggingFSM类向 FSM 添加了另一个功能:滚动事件日志(rolling event log),可在调试期间(用于跟踪 FSM 如何进入特定故障状态)或其他创造性用途中使用:

    logDepth默认为零,这将关闭事件日志。

    • 警告:日志缓冲区是在 Actor 创建期间分配的,这就是使用虚拟方法调用完成配置的原因。如果要使用val进行重写,请确保其初始化发生在运行LoggingFSM的初始值设定项之前,并且不要在分配缓冲区后更改logDepth返回的值。

    事件日志的内容可使用getLog方法获取,该方法返回IndexedSeq[LogEntry],其中最早的条目位于索引零。


    英文原文链接FSM.