📜 ⬆️ ⬇️

Kill all humans with a cat, or state machines on Akka.FSM

As I wrote in my first article , not so long ago I switched from C ++ to Scala. And at the same time I began to study the model of actors performed by Akka. The most vivid impression on me was the easy implementation and testing of finite state machines (finite-state machines, FSM), which this library provides. I don’t know why this happened, given the abundance of other beautiful and useful things in Akka. But now in my first Scala project I use finite automata for each drop-down opportunity supported by expediency (as I sincerely hope). And so I decided that I was ready to share with the community the knowledge about Akka.FSM, as well as some tricks and personal insights that I had time to accumulate. I did not find a similar topic on the Habré (and indeed with articles about Scala and Akka here somehow was not thick), and decided, without delay, to rectify the situation and speak out, until someone said everything before me. And so that it is not boring - I propose to realize together the behavior of the real electronic cat. I would like to believe that some kind of lonely romantic soul, inspired by my article, will modify the functionality offered in it to a full-fledged “Tamakotchi”, as homework. The main thing is that such a soul does not forget to share its results with the community in the comments. Ideally, it would be possible to create a project on a shared access github so that everyone could contribute their personal contribution to the development of transhumanism ideas. And now - in the direction of jokes and fantasies, roll up our sleeves. We will start from scratch, and I, for the hell of 7D and the effect of presence, I will do every step with you. TDD is attached: with untested robokotom certainly will not be a joke.

The information in the article is intended for those who are already at least a little bit with Scala, and have at least a superficial idea of ​​the model of actors. For those who would like to meet, but do not know where to start, as a bonus, I wrote a small starting instruction and hid it under the spoiler so that the rest did not interfere. It tells how to create a clean Scala project with all the necessary libraries without any extra effort.


So, as you already understood, first we need a clean project with fresh versions of akka-actor, akka-testkit and scalatest libraries (at the time of writing this article is akka 2.3.4 and scalatest 2.1.6.
')
'' Eeee ... And what's all this garbage? '', Or for those who are not in the subject
Warning number 1: if you never felt Scala with your bare hands at all, and didn’t even spy on it through the keyhole, then you most likely will not understand any particular part of everything written later in this article. But for the most stubborn (I approve, he himself), I will explain exactly how you can create a new project on Scala with no hardship using the trendy and brilliant such Typesafe Activator bun.

Warning number 2: the following actions in the command line are valid for OS Linux and Mac OS X. The actions required for Windows are similar to those described, but differ from them (at least, the lack of a tilde before the name of the Projects directory, backslash slash, the word "folder" instead of the words “directory” or “directory”, and the presence in the archive of a special file activator.bat, designed for Windows).

Create a project


So let's go. The easiest way for me to create a new project is to download the mentioned typesafe activator from the official site. Library versions announced on the site at the time of writing this article: Activator 1.2.10, Akka 2.3.4, Scala 2.11.1. All is downloaded in the form of a ZIP archive. While it is being downloaded, we need to preheat the oven to 230 degrees Celsius. For now you think: “Why do we need an oven? o_0 "- 352MB archive has already been downloaded. Unpack all this stuff somewhere on the disk. I will do all the manipulations in the ~ / Projects directory. So:

$ mkdir ~/Projects $ cd ~/Projects $ unzip ~/Downloads/typesafe-activator-1.2.10.zip 

After the archive is unpacked, do not forget to lubricate the pan with oil. Everything, I promise, then everything will be extremely serious. Now we have two ways to create a project: through a graphical interface and through the command line. As a labor Jedi, we, of course, choose the path of strength (especially since the terminal is already open - not to close it because of some UI):

 $ activator-1.2.10/activator new kote hello-akka 

With this straightforward string, we tell the activator to create ( new ) a kote project in the current folder (and we, as we remember, stayed in ~ / Projects) from a template called hello-akka . This template already includes the build.sbt file configured for the necessary libraries. The possibilities of the dark side, as always, are lighter and more tempting, so if someone does not work on the command line, you can type ./activator ui (or just ui , if you are already in the activator console), and do everything in the opened browser . Everything is very beautiful there, look at least just for the sake of interest - I promise you will like it. After the project is created - go to its directory:

 $ cd kote 

IDE or not IDE


Then every Jedi decides for himself what his strength is: use ed, vi, vim, emacs, Sublime, TextMate, Atom, something else, or a full IDE. Personally, with the transition to Scala, I started using IntelliJ IDEA, so I will immediately generate project files for this environment. For everything to work out, add the line addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2") to the project / plugins.sbt file:

 $ echo "addSbtPlugin(\"com.github.mpeltonen\" % \"sbt-idea\" % \"1.5.2\")" > project/plugins.sbt 

Then we launch the activator, and then it will do everything that is necessary, according to our team:

 $ $ ./activator > gen-idea sbt-classifiers 

Now you can open a project in IDEA.

Or is it not an IDE?


If you think that IDE is the dark side of power (or vice versa), and it is not worthy of a Jedi - this is your complete right. In this case, you can stay in the command line of the activator, and edit the files in any convenient way. And then only two teams of activator will decide the whole fate of our cat:
  1. compile - compile the project.
  2. test - run all tests. Call compile if necessary, so I lied, you can get by with this command alone.

I will not run kote in production in this article, but a potential developer of the final version of Tamagotchi will be able to do this with the help of the run command.

We clean the place for kote


All cats, as you know, are pedantic cleaners. Therefore, we will start with the preparation of a clean and neat home for our future pet. That is, we remove all the extra files that come with the newly created project in the framework of the hello-akka template. Personally, I personally consider unnecessary directories src / main / java, src / test / java with all the content, as well as all the .scala files, we will not need them either: src / main / scala / HelloAkkaScala.scala and src / test / scala /HelloAkkaSpec.scala. Well, now we are ready to proceed.


First step


In the beginning was the test. And the test did not compile. It is this statement, as is known, that is the fundamental postulate of TDD, of which I am a supporter at the moment. Therefore, I’ll start my description not from the automaton itself, but from the creation of the first test for it, in order to demonstrate the testing capabilities provided by the Akka TestKit library. Included with the activator, which I use, already has a framework for testing - the scalatest. He suits me perfectly, and I see no reason not to use it in our project. In general, Akka TestKit can be used with spec2 or something else, as it is framework independent . In order not to bother with the names of test packages, I will put the file directly in src / test / scala / KoteSpec.scala

 import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestFSMRef, TestKit} import org.scalatest.{BeforeAndAfterAll, FreeSpecLike, Matchers} class KoteSpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender with Matchers with FreeSpecLike with BeforeAndAfterAll { def this() = this(ActorSystem("KoteSpec")) import kote.Kote._ override def afterAll(): Unit = { system.shutdown() system.awaitTermination(10.seconds) } "A Kote actor" - { // All future tests go here } } 

Further it is assumed that all the tests I will add to the body of this class, immediately under the comment. I use FreeSpecLike, and not, say, FlatSpecLike, because it is much more convenient for me to visually structure a lot of tests for various states and automaton transitions on it. Since we are ready to start creating our first test, I propose to start with the fact that cats love to do more than anything else - sleep. So, adopting the principles of TDD, we will create a test that will check that the newly “born” cat initially sleeps:

 "should sleep at birth" in { val kote = TestFSMRef(new Kote) kote.stateName should be(State.Sleeping) kote.stateData should be(Data.Empty) } 


Now try to sort things out in order. TestFSMRef is a class that offers us the Akka TestKit framework to simplify the testing of finite automata implemented using the FSM class. To be more precise, TestFSMRef is a class with an auxiliary object (companion object), the apply method of which we call. And this method returns us an instance of the class TestFSMRef, which is the successor of the most ordinary ActorRef, that is, we can send messages to our automaton as a simple actor. However, the functionality of TestFSMRef is somewhat extended compared to the simple ActorRef, and these extensions are designed specifically for testing. One of these extensions are the two functions we used: stateName and stateData, which provide access to the current state of our test kitten. Why are there two functions, one state? After all, in the usual sense, the state is a combination of the current values ​​of the internal parameters of the machine. Where are the two variables here, and why two? The fact is that in order to describe the current state of the Akka.FSM automaton (based on the principles of the design of automata in Erlang), it separates the concepts of state “names” and “data” associated with it. In addition, Akka recommends avoiding the use of mutable properties (var) in the class of the automaton, justifying this with the advantage that in this way the state of the automaton in the program code can be changed only in a few predetermined and well-known places and avoid unobvious and implicit changes . Moreover, there is no direct access from inside our future class to these two variables: they are declared as private in the base class FSM. However, TestFSMRef provides access to them for testing. And how to reach them from the class of the machine itself - it will become clear further.

So, our sleep state I called Sleeping. And I put it in the auxiliary State object, which will now store all the names of our states for clarity of the code and to avoid confusion. As for the data, at this stage we do not yet know what they will be. But something "feed" the machine as data still have to, otherwise it will not work. Therefore, I decided to call the variable name Empty, this is my personal choice, and does not oblige you to anything. You can call it another way: Nothing, Undefined. As for me, Empty is rather short and informative. I also used to store data in a specially selected object, which I called Data. In my “combat” automata of various types of data, sometimes it is not less, or even more, than the names of the states, so I always keep them in a dedicated place: cutlets separately, flies separately.

Well, compile? It is clear that the compilation will not work, in the absence of those types and variables that we refer to in the test. This means that we are ready to proceed to the next stage of the TDD cycle.

In order to declare the class of our automaton, we need two basic types from which all classes and objects will be inherited, describing the names of the states and their data. In order not to litter the environment, we will create an auxiliary object (companion object) that will store all the definitions necessary for the kitten’s life. This is the generally accepted standard of behavior in the world of Scala, and no one will blame us for this. If for tests with the name of the package we did not bother, then for the project itself, I still create it. Let's call it kote. And we will put the implementation file of our pet, respectively, in src / main / scala / kote / Kote.scala. So, let's begin:

 package kote import akka.actor.FSM import scala.concurrent.duration._ /** Kote companion object */ object Kote { sealed trait State sealed trait Data } 

These definitions are enough to declare a kitty class:

 /** Kote Tamakotchi mimimi njawka! */ class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ } 

Inside the class, I added an import of everything that will be further declared in the auxiliary object, to simplify further access. We can only declare the value of the name and data for our original "sleepy" state:

 /** Kote companion object */ object Kote { sealed trait State sealed trait Data object State { case object Sleeping extends State } object Data { case object Empty extends Data } } 

Before compiling the test, the last step remains. Since from the tests we refer (and want to refer henceforth) to the insides of the Kote object as easily and simply as from the class itself, it will be convenient for us to add import into the body of the KoteSpec class. It is possible immediately after the alternative constructor declaration:

 ... def this() = this(ActorSystem("KoteSpec")) import Kote._ ... 

Don't forget to add import kote.Kote to the import section in the KoteSpec.scala file. Now the project has successfully compiled, and you can run the test. What? Red? NullPointerException? And you thought - just create a new kitten? The nature of millions of years of evolution on it is portable! Well, okay, do not panic. Probably, the problem is that we did not tell our animal what to do immediately after birth. This is very easy to do:

 class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ startWith(State.Sleeping, Data.Empty) } 

We start the test, and - voa! Green as I love! The kitten seems to have come to life, but something is kind of boring: stupidly sleeps for itself - that's all. This is not fun. Let's wake him up.

"Sleep, my joy!", Or how to implement the behavior in the initial state


How would we do this? Do not slow down the monitor while the test works? Let's be constructive and think: if our kitten is an actor, then the only way to communicate with him is to send messages. Such an important kote-bureaucrat, all that remained was a sissy secretary to hire him to sort out the correspondence. What message should he send to wake him up? We could write to him simply: kote! “Prosnis'! Wake up! ". But personally, I consider sending messages as strings as a mauve, because you can always make a mistake in some kind of character, and the compiler won't even notice it, and it will be very difficult to debug it later. Yes, and our newborn kote, if to fantasize, should not yet understand human language. I propose to develop a special cat language commands, which he seemed to begin to learn from birth. Well, instinctively, or something. And we will contribute to the development of his instincts. The first team we will teach him will be called WakeUp. And we pop it into our helper object, in the Commands sub-object:

 object Kote { ... object Commands { case object WakeUp } } 

Now proceed to the test:

 "should wake up on command" in { val kote = TestFSMRef(new Kote) kote ! Commands.WakeUp kote.stateName should be (State.Awake) } 

Of course, the test will not compile. We forgot to announce the name of our state:

  case object Awake extends State 

Now the test has been compiled, but, as it seems, we were destined to crash with another exception: NoSuchElementException: key not found: Sleeping . What do all these barbaric letters mean? Only one thing: we told our young lover of quantum experiments that he should sleep, and he really sleeps obediently, but at the same time what sleep is and how to do it - he still does not know. And we, in addition, are trying to send him a message in this state of uncertainty. Let's not be encouraged by the well-known torturers and poisoners of the cats and keep the poor animal in desperate ignorance, and just describe its behavior:

 when(State.Sleeping, Data.Empty) { FSM.NullFunction } 

Not bad for a start. when is the most common scala function with two pairs of parentheses. That is, when () (). In the first we specify the name of the state for which we want to describe the behavior, and secondly (here the second brackets are not visible, as scala allows them not to be specified in this case) - a partial function (partial function), which characterizes the behavior of our animal in this state. So let's call it - behavior. And the behavior is in response to various external stimuli. Simply - on incoming messages. Normal reaction can be of three types - either the machine remains in the current state (stay), or goes into the new (goto), or stops the work (stop). The fourth option, the “abnormal” reaction, is when the machine cannot cope with the problem that has come over and throws an exception (and then, as is the case with a regular actor, his supervisor decides what to do with it, in accordance with the current supervision strategy ). I will touch on the topic of exceptions a little later.

FSM.NullFunction is helpfully provided by the Akka library function, which tells us that the cat in this state does absolutely nothing and does not react to anything, and skips all incoming messages past the ears. We could write {case _ =>} , but it would not be exactly the same, and I’ll also mention this later. It is convenient to use NullFunction as a “gag” to describe future states, the details of the implementation of which are not yet important at this stage, but the transition to which we already need to be tested.

“Wake up, lazy brute!”, Or how to respond to an event by switching to a new state


So, let's run the test now - and now the cause of the fall is completely different: Sleeping was not equal to Awake. Of course, because our cat learned to sleep, but we have not yet taught him to respond to the WakeUp command. Let's try to stir it up a bit:

 when(State.Sleeping) { case Event(Commands.WakeUp, Data.Empty) => goto(State.Awake) } 

As I said, we do not have direct access to variables with the state name and data. We get access to them only when our machine receives a message. The FSM wraps this message in the case class Event, and adds the current state data to the same place. Now we can apply pattern matching and isolate from the "flown" event all that we need. In this case, we are convinced that being in the state called Sleeping, we received the WakeUp command, and our data were Data.Empty. And we react to all this vinaigrette transition to a new state: Awake. This approach to the description of behavior allows us to handle various options for combining the names of the state with the current data to it. That is, to find in the same state, we can react differently to the same message depending on the current data.

Now I would like to note the features of the mentioned state transition functions: goto and stay. By themselves, these are “pure” functions that do not have any side effects. Which means that the very fact of calling them to change the current state does not lead. They only return the state value we need (specified by the user in the case of goto, or current in the case of a stay), reduced to the type understood by the FSM. In order for a change to occur, it must be returned from our function of behavior.

With this sorted out. Now we run the test - but again the failure: Next state Awake does not exist. I intentionally wanted to show what happens if the next state is not declared using when: the transition simply does not occur, and the machine remains in the same state. Exceptions, as it happened with our starting state, are also not discarded. Often in the impulse of development I forgot about it, and spent time trying to figure out why the transition did not occur and the test drops. The message “Next state Awake does not not exist” in non-trivial tests in the log can simply not be noticed among others. But over time you begin to get used to this feature.

So, we declare the zero function as our next state, and the test will turn green:

  when(State.Awake)(FSM.NullFunction) 


“Stroke the cat!”, Or how to react to the event, while maintaining steadfastness


Well, now you can pet our kitten, taking advantage of the fact that he woke up. I hope what and where to add - already figured out?

Team:
  case object Stroke 

Test:
 "should purr on stroke" in { val kote = TestFSMRef(new Kote) kote ! Commands.WakeUp kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

Kote:
 when(State.Awake) { case Event(Commands.Stroke, Data.Empty) => sender() ! "purrr" stay() } 


The same can be written more succinctly:
 when(State.Awake) { case Event(Commands.Stroke, Data.Empty) => stay() replying "purrr" } 

"Do not wake the cat twice!", Or how to test without repeating without repeating


Stop stop! Well, it turns out, to stroke the cat in the dough, we wake him up first, and then stroke it? Great, that is, if we have another 10-15 intermediate (and if 100-150?) Up to the tested state, then we will have to pass through everything correctly, not allowing a single error to get to the right one? What if all the same mistake, and we were not where we think? Or did something change over time in transitions between intermediate states? In this case, the TestFSMRef provides us with the ability to set the required state and data with the help of the setState function, without having to go through all intermediate steps. So, we change our test:

 "should purr on stroke" in { val kote = TestFSMRef(new Kote) kote.setState(State.Awake, Data.Empty) kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

Well, for tests of the same state for several different stimuli, I personally invented for myself this method of getting rid of duplicate code:

 class TestedKote { val kote = TestFSMRef(new Kote) } 

And now I can replace all our tests with:

 "should sleep at birth" in new TestedKote { kote.stateName should be (State.Sleeping) kote.stateData should be (Data.Empty) } "should wake up on command" in new TestedKote { kote ! Commands.WakeUp kote.stateName should be (State.Awake) } "should purr on stroke" in new TestedKote { kote.setState(State.Awake, Data.Empty) kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be (State.Awake) } 

As for testing the same non-starting state several times, I brought out the following simple technique for myself:

 "while in Awake state" - { trait AwakeKoteState extends TestedKote { kote.setState(State.Awake, Data.Empty) } "should purr on stroke" in new AwakeKoteState { kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be(State.Awake) } } 

As you can see, I created a framing with the subtitle “while in Awake state” for all the “awake” tests, and put trait AwakeKoteState (it can be a class, not the essence) that, when initialized, immediately puts the cat in a waking state without unnecessary gestures. Now all the tests in this state, I will announce with it.

"Breathe more life", or how to add meaningful data to the state


, . , — . , , ! ! , . ! ? , , , , , . : . , . — , , . , , — / . , Empty , . , , . , , . , hunger: Int, , 100 — , 0 — ( ). , , 60 — , , . case class VitalSigns, case object Empty . - Data. So:

 ... object Data { case class VitalSigns(hunger: Int) extends Data } ... 

Naturally, now in the whole project you need to change Data.Empty to Data.VitalSigns. Starting with the startWith line:

  startWith(State.Sleeping, Data.VitalSigns(hunger = 60)) 

In fact, in the existing behavior of a kitten in the states already described, we (he, of course) does not care about his vital signs, so we can safely replace the Data.Empty here with an underscore, and not with VitalSigns:

 when(State.Sleeping) { case Event(Commands.WakeUp, _) => goto(State.Awake) } when(State.Awake) { case Event(Commands.Stroke, _) => stay() replying "purrr" } 

Now our kitten has evolved even more, and can complicate its behavior, and rumble when stroking only if it is full enough:

 when(State.Awake) { case Event(Commands.Stroke, Data.VitalSigns(hunger)) if hunger < 30 => stay() replying "purrr" case Event(Commands.Stroke, Data.VitalSigns(hunger)) => stay() replying "miaw!!11" } 


And tests:

 "while in Awake state" - { trait AwakeKoteState extends TestedKote { def initialHunger: Int kote.setState(State.Awake, Data.VitalSigns(initialHunger)) } trait FullUp { def initialHunger: Int = 15 } trait Hungry { def initialHunger: Int = 75 } "should purr on stroke if not hungry" in new AwakeKoteState with FullUp { kote ! Commands.Stroke expectMsg("purrr") kote.stateName should be(State.Awake) } "should miaw on stroke if hungry" in new AwakeKoteState with Hungry { kote ! Commands.Stroke expectMsg("miaw!!11") kote.stateName should be(State.Awake) } } 

"The animal is starving!", Or how to plan events


The kitten must “gain” the level of hunger over time (What? “To pry”? There is no such word in the Russian language!) To do this, we will plan a GrowHungry message for every 5 minutes immediately after the “birth” of the cat, and it will stay with him until his death. Cruel? That's life!

Message:
  case class GrowHungry(by: Int) 

Kote:
 class Kote extends FSM[Kote.State, Kote.Data] { import Kote._ import context.dispatcher startWith(State.Sleeping, Data.VitalSigns(hunger = 60)) val hungerControl = context.system.scheduler.schedule(5.minutes, 5.minutes, self, Commands.GrowHungry(3)) override def postStop(): Unit = { hungerControl.cancel() } ... 

I made the level of “recruited” feeling of hunger variable, because along with the natural process of “starvation” (adding a kitten +3 to hunger every 5 minutes), an animal can engage in mobile activity, in which case its appetite will grow much faster. hungerControl is a Cancellable instance, and before stopping a kitten's heart, it must be canceled in a postStop to avoid leaks, since the dispatcher does not follow the stopping of actors , and will send further messages to the dead kitten directly to the next world, and the dead, even if they are kittens, disturb worthless Well, one more thing: to call the scheduler, you need to specify implicit ExecutionContext, so the import context.dispatcher line appeared.

"And let's just kill him!", Or how to handle events common to all states


In order not to delay the article, I immediately realize the death of a cat from starvation (hunger> = 100), and the transition to a particularly hungry state (hunger> 85), where the kitten should be busy only by regularly meowing and begging for food. Let's believe that Akka has tested her planners, and the message will arrive on time, and let's write how the cat will react to it. It is worth noting that “natural fat burning” will occur in all states: whether the cat sleeps, wakes up, asks for food, eats, or plays with the mouse. How to be in this case? Describe the same behavior for all possible states? Together with the tests? And if at some point we forget to write a test, and the cat, having found such a ball, will hang in one state and enjoy eternal satiety? Yes, to hell with him, and let him be enjoyed, do not mind,but this bastard mustached, according to the old habit, will eat regular, and fats will not be burned - and in the end he will die from an overdose of food in the body! In this case, the FSM, sincerely caring for your kitten, suggests using the whenUnhandled function, which works in any state for messages that do not coincide with any of the options in the function and behavior for the current state. Remember, I wrote thatFSM.NullFunction { _ => } ? , , . , , whenUnhandled, — «» , whenUnhandled .

 whenUnhandled { case Event(Commands.GrowHungry(by), Data.VitalSigns(hunger)) => val newHunger = hunger + by if (newHunger < 85) stay() using Data.VitalSigns(newHunger) else if (newHunger < 100) goto(State.VeryHungry) using Data.VitalSigns(newHunger) else throw new RuntimeException("They killed the kitty! Bastards!") } 

— using. , , , stay, goto. , using , , . using , , — .

“Vivifying Pathology”, or how to test exceptions


I will not describe all the tests, to save time and space. They are not much different from previous ones. From the interesting, I consider it necessary to mention the method of testing the thrown out exception. Actually, for FSM it is no different from the method applicable to ordinary actors:

 "should die of hunger" in new AwakeKoteState with Hungry { intercept[RuntimeException] { kote.receive(Commands.GrowHungry(1000)) // headshot } } 

Instead of sending a message (which would have caused the delivery of the exection not to the test, but to the supervisor actor, which in this case is a user guardian, and the test would simply not pass), we use the actor's receive method call directly. This is necessary in order to make sure that the automaton throws out the correct exception in a particular situation. After all, it will depend on this further, whether the almighty supervisor will reanimate our poor animal, finding the appropriate mark for his strategy in the ejected exception. It was for the demonstration of this test that I used the exception as the cause of the cat's death. And it would be possible to simply return stop () - but so normal cats die from old age, and not from hunger.

"Stop sleeping already, sonya!", Or how to limit the length of time in the state


So that we ourselves no longer fall asleep, I will tell you about the last feature that I would not like to bypass. This is the ability to set the maximum timeout for a state. It is set simply: it is indicated after the comma in the when function, immediately after the state name. For example, after 3 hours of sleep, the cat wakes up on its own, and you do not need to wake it up:

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp, _) => goto(State.Awake) } 

What do you think will wake up? NahBy itself, the timeout neither the state nor the data does not change. This can only be done by the cat itself, by its own volitional decision (well, still Neo, but I heard he will not return again, and old Chuck is no longer a cake). And in the only place - in the function of behavior (this does not apply to Neo, he could anywhere, just like Chuck in his youth). But now, after a specified period of time, if no one is awakened by a cat, he will receive a StateTimeout message. And how to react to it is already for him to decide:

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp | StateTimeout, _) => goto(State.Awake) } 

Now he can wake up for two reasons: if he slept long enough and slept, or if he was woken up forcibly. You can separate these two events, and respond to them in different ways: in one case, be active and playful, and in the other - be an evil stinker, constantly meow and annoy and upset everyone. In any case, if the cat is out of sleep, then the timeout will automatically be canceled (unlike the stupid scheduler, which you need to cancel yourself) and nothing supernatural will happen. By the way, if the cat gets a timeout, but continues to sleep (returning stay ()), he will receive it again after 3 hours, as expected. That is, the time-out, without being explicitly canceled or reassigned (using stay (). ForMax (20.hours), what's next), but being caught in the behavioral function and accompanied by the stay () response,"Shoot" again after a specified period of time.

, state timeout when ( ), goto stay forMax (, stay().forMax(1.minute) , goto(State.Sleeping).using(Data.Something).forMax(1.minute) ), ( when, ):

 when(State.Sleeping, 3.hours) { case Event(Commands.WakeUp, _) => goto(State.Awake).forMax(3.hours) case Event(StateTimeout, _) => goto(State.Awake).forMax(5.hours) } 

, , 3 , — 5 . , , StateTimeout Awake.

«?! ?!!»,


And finally, the very last thing, I promise. There is another useful feature in Akka FSM: the onTransition method. It allows you to set some actions in the transition from state to state. Use it like this:

 onTransition { case State.Sleeping -> State.Awake => log.warning("Meow!") case _ -> State.Sleeping => log.info("Zzzzz...") } 

It seems to be obvious, but just in case I will explain: at the moment of transition from a sleeping state to a waking state, the kitten meows in a special way exactly once. When going from any state to a state of sleep, a single snore publishes (this is how it sounds in English. From this we conclude that our kote is British).

These actions work even when you set a state in the test using the FSMActorRef.setState function(if the transition from the current to the target state coincides with one of those described in onTransition, of course). Thus, respectively, they can be tested. Well, remember that here the use of the goto function will be meaningless. I am telling you this as a person who once stubbornly tried to change the data during a change of states, and for a long time could not understand why it did not work. One more thing that I discovered: the transition trigger will work even if you remain in the current state by returning stay () from the behavioral function. This was promised to be fixed in the next versions of Akka, but for the time being it would mean that if you return stay () when reacting to any of the Sleeping states, then onTransition will work and your kitten will snore.

the end


, . : , when ? whenUnhandled. Thank you all for your attention.

PS , , , .

Source: https://habr.com/ru/post/237657/


All Articles