javaakkaactorfsm

Akka Java FSM by Example


Please note: I am a Java developer with no working knowledge of Scala (sadly). I would ask that any code examples provided in the answer would be using Akka's Java API.

I am trying to use the Akka FSM API to model the following super-simple state machine. In reality, my machine is much more complicated, but the answer to this question will allow me to extrapolate to my actual FSM.

enter image description here

And so I have 2 states: Off and On. You can go fro Off -> On by powering the machine on by calling SomeObject#powerOn(<someArguments>). You can go from On -> Off by powering the machine off by calling SomeObject#powerOff(<someArguments>).

I'm wondering what actors and supporting classes I'll need in order to implement this FSM. I believe the actor representing the FSM has to extend AbstractFSM. But what classes represent the 2 states? What code exposes and implements the powerOn(...) and powerOff(...) state transitions? A working Java example, or even just Java pseudo-code, would go a long way for me here.


Solution

  • I think we can do a bit better than copypasta from the FSM docs (http://doc.akka.io/docs/akka/snapshot/java/lambda-fsm.html). First, let's explore your use case a bit.

    You have two triggers (or events, or signals) -- powerOn and powerOff. You would like send these signals to an Actor and have it change state, of which the two meaningful states are On and Off.

    Now, strictly speaking an FSM needs one additional component: an action you wish to take on transition.

    FSM:
    State (S) x Event (E) -> Action (A), State (S')
    
    Read: "When in state S, if signal E is received, produce action A and advance to state S'"
    

    You don't NEED an action, but an Actor cannot be directly inspected, nor directly modified. All mutation and acknowledgement occurs through asynchronous message passing.

    In your example, which provides no action to perform on transition, you basically have a state machine that's a no-op. Actions occur, state transitions without side effect and that state is invisible, so a working machine is identical to a broken one. And since this all occurs asynchronously, you don't even know when the broken thing has finished.

    So allow me to expand your contract a little bit, and include the following actions in your FSM definitions:

     When in Off, if powerOn is received, advance state to On and respond to the caller with the new state
     When in On, if powerOff is received, advance state to Off and respond to the caller with the new state
    

    Now we might be able to build an FSM that is actually testable.

    Let's define a pair of classes for your two signals. (the AbstractFSM DSL expects to match on class):

    public static class PowerOn {}
    public static class PowerOff {}
    

    Let's define a pair of enums for your two states:

     enum LightswitchState { on, off }
    

    Let's define an AbstractFSM Actor (http://doc.akka.io/japi/akka/2.3.8/akka/actor/AbstractFSM.html). Extending AbstractFSM allows us to define an actor using a chain of FSM definitions similar to those above rather than defining message behavior directly in an onReceive() method. It provides a nice little DSL for these definitions, and (somewhat bizarrely) expects that the definitions be set up in a static initializer.

    A quick detour, though: AbstractFSM has two generics defined which are used to provide compile time type checking.

    S is the base of State types we wish to use, and D is the base of Data types. If you're building an FSM that will hold and modify data (maybe a power meter for your light switch?), you would build a separate class to hold this data rather than trying to add new members to your subclass of AbstractFSM. Since we have no data, let's define a dummy class just so you can see how it gets passed around:

    public static class NoDataItsJustALightswitch {}
    

    And so, with this out of the way, we can build our actor class.

    public class Lightswitch extends AbstractFSM<LightswitchState, NoDataItsJustALightswitch> {
        {  //static initializer
            startWith(off, new NoDataItsJustALightswitch()); //okay, we're saying that when a new Lightswitch is born, it'll be in the off state and have a new NoDataItsJustALightswitch() object as data
    
            //our first FSM definition
            when(off,                                //when in off,
                matchEvent(PowerOn.class,            //if we receive a PowerOn message,
                    NoDataItsJustALightswitch.class, //and have data of this type,
                    (powerOn, noData) ->             //we'll handle it using this function:
                        goTo(on)                     //go to the on state,
                            .replying(on);           //and reply to the sender that we went to the on state
                )
            );
    
            //our second FSM definition
            when(on, 
                matchEvent(PowerOff.class, 
                    NoDataItsJustALightswitch.class, 
                    (powerOn, noData) -> {
                        goTo(off)
                            .replying(off);
                        //here you could use multiline functions,
                        //and use the contents of the event (powerOn) or data (noData) to make decisions, alter content of the state, etc.
                    }
                )
            );
    
            initialize(); //boilerplate
        }
    }
    

    I'm sure you're wondering: how do I use this?! So let's make you a test harness using straight JUnit and the Akka Testkit for java:

    public class LightswitchTest { 
        @Test public void testLightswitch() {
            ActorSystem system = ActorSystem.create("lightswitchtest");//should make this static if you're going to test a lot of things, actor systems are a bit expensive
            new JavaTestKit(system) {{ //there's that static initializer again
                ActorRef lightswitch = system.actorOf(Props.create(Lightswitch.class)); //here is our lightswitch. It's an actor ref, a reference to an actor that will be created on 
                                                                                        //our behalf of type Lightswitch. We can't, as mentioned earlier, actually touch the instance 
                                                                                        //of Lightswitch, but we can send messages to it via this reference.
    
                lightswitch.tell(    //using the reference to our actor, tell it
                    new PowerOn(),   //to "Power On," using our message type
                    getRef());       //and giving it an actor to call back (in this case, the JavaTestKit itself)
    
                //because it is asynchronous, the tell will return immediately. Somewhere off in the distance, on another thread, our lightbulb is receiving its message
    
                expectMsgEquals(LightswitchState.on);   //we block until the lightbulb sends us back a message with its current state ("on.")
                                                         //If our actor is broken, this call will timeout and fail.
    
                lightswitch.tell(new PowerOff(), getRef());
    
                expectMsgEquals(LightswitchState.off);   
    
                system.stop(lightswitch); //switch works, kill the instance, leave the system up for further use
            }};
        }
    }
    

    And there you are: an FSM lightswitch. Honestly though, an example this trivial doesn't really show the power of FSMs, as a data-free example can be performed as a set of "become/unbecome" behaviors in like half as many LoC with no generics or lambdas. Much more readable IMO.

    PS consider learning Scala, if only to be able to read other peoples' code! The first half of the book Atomic Scala is available free online.

    PPS if all you really want is a composable state machine, I maintain Pulleys, a state machine engine based on statecharts in pure java. It's getting on in years (lot of XML and old patterns, no DI integration) but if you really want to decouple the implementation of a state machine from inputs and outputs there may be some inspiration there.