akkaakka-actor

Akka: Can an actor of some class become an actor of a diferent class?


As a course project, I am trying to implement a (simulation) of the Raft protocol. In this post, I will not use Raft terminology at all; instead, I will use a simplified one.

The protocol is run by a number of servers (for example, 5) which can be in three different states (A, B, C). The servers inherit some state variables and behavior from a "base" kind, but they all also have many unique state variables and methods, and respond to different messages. At some point of the protocol, a server in some state (for example, A) is required to become the other state (for example, B). In other words, the server should:

  1. Lose the state variables and methods of state A, acquire those of state B, but maintain the variables of the "base" kind.
  2. Stop responding to messages destined for state A, start responding to messages destined for state B.

In Akka, Point 1 can be implemented using Receives and become().

Point 2 is needed because, for example, an actor of class B should not have access to state variables and methods of an actor of class A. This aims at separating concerns, and achieving a better code organization.

The issues I am facing in implementing these Point 2 are the following:

A way to circumvent the issue is that the newly spawned actor "advertises" itself by sending a message to the other actors, including its old ActorRef. However, again due to the protocol, the other servers may be temporarily not available (i.e., they are crashed), thus they might not receive and process the advertisement.

In the project, I must use extensions of AbstractActor, and not FSM (final state machines), and have to use Java.

Is there any Akka pattern or functionality that solves this use case? Thank you for any insight. Below is a simplified example.

public abstract class BaseActor extends AbstractActor {
    protected int x = 0;
    // some state variables and methods that make sense for both A and B

    @Override
    public Receive createReceive() {
        return new ReceiveBuilder()
                .matchEquals("x", msg -> {
                    System.out.println(x);
                    x++;
                })
                .build();
    }
}

public class A extends BaseActor {
    protected int a = 10;
    // many other state variables and methods that are own of A and do NOT make sense to B

    @Override
    public Receive createReceive() {
        return new ReceiveBuilder()
                .matchEquals("a", msg -> {
                    System.out.println(a);
                })
                .matchEquals("change", msg -> {
                    // here I want A to become B, but maintain value of x
                })
                .build()
                .orElse(super.createReceive());
    }
}

public class B extends BaseActor {
    protected int b = 20;
    // many other state variables and methods that are own of B and do NOT make sense to A

    @Override
    public AbstractActor.Receive createReceive() {
        return new ReceiveBuilder()
                .matchEquals("b", msg -> {
                    System.out.println(b);
                })
                .matchEquals("change", msg -> {
                    // here I want B to become A, but maintain value of x
                })
                .build()
                .orElse(super.createReceive());
    }
}

public class Example {
    public static void main(String[] args) {
        var system = ActorSystem.create("example");

        // actor has class A
        var actor = system.actorOf(Props.create(A.class));
        actor.tell("x", ActorRef.noSender()); // prints "0"
        actor.tell("a", ActorRef.noSender()); // prints "10"

        // here, the actor should become of class B,
        // preserving the value of x, a variable of the "base" kind
        actor.tell("change", ActorRef.noSender());

        // actor has class B
        actor.tell("x", ActorRef.noSender()); // should print "1"
        actor.tell("b", ActorRef.noSender()); // should print "20"
    }
}

Solution

  • This is a sketch implementation of how this could look like.

    1. You model each of the states a separate class:
    public class BaseState {
      //base state fields/getters/setters
    }
    
    public class StateA {
      BaseState baseState;
      //state A fields/getters/setters
      ..
    
      //factory methods
      public static StateA fromBase(BaseState baseState) {...}
    
      //if you need to go from StateB to StateA:
      public static StateA fromStateB(StateB stateB) {...}
    }
    
    public class StateB {
      BaseState baseState;
      //state B fields/getters/setters
    
      //factory methods
      public static StateB fromBase(BaseState baseState) {...}
    
      //if you need to go from StateA to StateB:
      public static StateB fromStateA(StateA stateA) {...}
    }
    
    1. Then in your Actor you can have receive functions defined for both A and B and initialize it to A or B depending which one is the initial one
    
    
    private static class MyActor extends AbstractActor
      {
        private AbstractActor.Receive receive4StateA(StateA stateA)
        {
          return new ReceiveBuilder()
            .matchEquals("a", msg -> stateA.setSomeProperty(msg))
            .matchEquals("changeToB", msg -> getContext().become(
              receive4StateB(StateB.fromStateA(stateA))))
            .build();
        }
    
        private AbstractActor.Receive receive4StateB(StateB stateB)
        {
          return new ReceiveBuilder()
            .matchEquals("b", msg -> stateB.setSomeProperty(msg))
            .matchEquals("changeToA", msg -> getContext().become(
              receive4StateA(StateA.fromStateB(stateB))))
            .build();
        }
    
        //assuming stateA is the initial state
        @Override
        public AbstractActor.Receive createReceive()
        {
          return receive4StateA(StateA.fromBase(new BaseState()));
        }
      }