Actor Systems

Actor Systems #

We now look at how actors can be started outside of tests. All actors started in a program form a hierarchy. The root of this hierarchy is a behavior started by an actor system. Typically, there is only one actor system per application.

Echoing messages #

As a simple example of an application with multiple actors, we write a program where one actor echos messages back to another. The messages contain lines read from standard input, and the sender prints messages that bounced back from the other actor.

Here is an example interaction with our program. Every second line is input, the rest is output.

Type 'quit' to exit, something else to send
hi
received: hi
ok
received: ok
quit

Here is the first part of the implementation with the main method creating the actor system.

public class EchoLoop extends AbstractBehavior<Echo> {
    private static final BufferedReader STDIN = 
        new BufferedReader(new InputStreamReader(System.in));

    public static void main(String[] args) {
        ActorSystem<Echo> echoLoop = //
            ActorSystem.create(EchoLoop.create(), "echo-loop");
        
        System.out.println("Type 'quit' to exit, something else to send");
        try(Stream<String> lines = STDIN.lines()) {
            lines
                .takeWhile(Predicate.not("quit"::equals))
                .forEach(line -> echoLoop.tell(new Echo(line)));
        } finally {
            echoLoop.terminate();
        }
    }
    // to be continued
}

The program uses ActorSystem.create with an EchoLoop behavior (continued below) to start all used actors. Every line read from standard input is sent to the root actor wrapped inside an Echo message. When the user types quit, we shut down the actor system using terminate. Otherwise, the program would continue to run because the actors would still run.

The Echo message, defined as a nested class, wraps text.

public static class Echo {
    public final String text;
    
    public Echo(String text) {
        this.text = text;
    }
}

The EchoLoop behavior stores references to other actors.

public static Behavior<Echo> create() {
    return Behaviors.setup(EchoLoop::new);
}

private ActorRef<EchoServer.Request> server;
private ActorRef<EchoClient.Request> client;

private EchoLoop(ActorContext<Echo> ctx) {
    super(ctx);
    server = ctx.spawn(EchoServer.create(), "echo-server");
    client = ctx.spawn(EchoClient.create(), "echo-client");
}

In the constructor we use the provided actor context to spawn the other actors. When receiving an Echo message, the wrapped text is forwarded to the client using its Send message. Apart from the text, the Send message also contains a reference to the actor the text should be sent to.

@Override
public Receive<Echo> createReceive() {
    return newReceiveBuilder()
        .onAnyMessage(this::echo)
        .build();
}

private EchoLoop echo(Echo msg) {
    client.tell(new EchoClient.Send(msg.text, server));
    return this;
}

Implementing the echo client #

The definition of the echo client starts with the messages it can receive.

public class EchoClient extends AbstractBehavior<Request> {

    public interface Request {}

    public static class Send implements Request {
        public final String text;
        public final ActorRef<EchoServer.Request> server;

        public Send(String text, ActorRef<EchoServer.Request> server) {
            this.text = text;
            this.server = server;
        }
    }

    public static class Log implements Request {
        public final EchoServer.Response response;

        public Log(EchoServer.Response response) {
            this.response = response;
        }
    }
    // to be continued
}

All possible message implement the empty interface Request which we use as type parameter for the message type. We have already seen that the Send message wraps text and a reference to a server actor. The Log message wraps a server response.

As message handlers we use method references to an overloaded receive method. The builder method onMessage is used to restrict the handler to specific message types.

@Override
public Receive<Request> createReceive() {
    return newReceiveBuilder()
        .onMessage(Send.class, this::receive)
        .onMessage(Log.class, this::receive)
        .build();
}

When clients receive Log messages they print the wrapped text.

private EchoClient receive(Log msg) {
    System.out.printf("received: %s%n", msg.response.text);
    return this;
}

The text wrapped in a Send message is forwarded to the wrapped server actor using its Request message. Like Send, the EchoServer.Request message also wraps an actor reference in addition to the text. Both messages are equivalent, but we use different types for demonstration purposes.

private EchoClient receive(Send msg) {
    final ActorRef<EchoServer.Response> logAdapter =
        getContext().messageAdapter(EchoServer.Response.class, Log::new);
    msg.server.tell(new EchoServer.Request(msg.text, logAdapter));
    return this;
}

The echo client does not handle server requests directly, so it cannot pass a reference to itself inside the request to the server. Server requests need to be wrapped inside the Log message for the client to be able to handle them. The messageAdapter method on the actor context handles the wrapping and returns an actor reference of appropriate type.

The following test demonstrates the behavior of echo clients sending a message.

@Test
public void testThatEchoClientSendsGivenMessage() {
    ActorRef<EchoClient.Request> client = KIT.spawn(EchoClient.create());
    
    String text = "hello world";
    TestProbe<EchoServer.Request> server = KIT.createTestProbe();
    client.tell(new EchoClient.Send(text, server.getRef()));

    EchoServer.Request request = server.receiveMessage();
    assertEquals(text, request.text);
}

Task: Implement the echo server #

The class EchoServer defines Request and Response types for actors that echo sent text back to their sender. Implement the corresponding behavior receiving Request messages and sending back a Response with the same text before waiting for new messages.

Add a test to the EchoTests class demonstrating that the server behaves as intended.