Home > Net >  When many Akka Actors send messages to one actor, how to cleanly handle inheritance of inner Command
When many Akka Actors send messages to one actor, how to cleanly handle inheritance of inner Command

Time:09-03

In akka-typed, the convention is to create Behavior classes with static inner classes that represent the messages that they receive. Heres a simple example

public class HTTPCaller extends AbstractBehavior<HTTPCaller.MakeRequest> {

  public interface Command {}

  // this is the message the HTTPCaller receives
  public static final class MakeRequest implements Command { 
    public final String query;
    public final ActorRef<Response> replyTo;

    public MakeRequest(String query, ActorRef<Response> replyTo) {
      this.query = query;
      this.replyTo = replyTo;
    }
  }
  
  // this is the response message
  public static final class Response implement Command {
    public final String result;

    public Response(String result) {
      this.result = result;
    }
  }

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

  private HTTPCaller(ActorContext<Command> context) {
    super(context);
  }

  @Override
  public Receive<Command> createReceive() {
    return newReceiveBuilder()
               .onMessage(MakeRequest.class, this::onMakeRequest).build();
  }

  private Behavior<MakeRequest> onMakeRequest(MakeRequest message) {
    String result = // make HTTP request here using message.query
    message.replyTo.tell(new Response(result));
    return Behaviors.same();
  }
}

Let's say that 20 other actors send MakeRequest messages to the single HTTPCaller actor. Now, each of these other actors have inner classes that implement their own Command. Since MakeRequest is being used by all 20 classes it must be a subtype of all 20 of those actors' Command inner interface.

This is not ideal. I'm wondering what the Akka way of getting around this is.

CodePudding user response:

There's no requirement that a message (e.g. a command) which an actor sends (except for messages to itself...) have to conform to that actor's incoming message type. The commands sent to the HTTPCaller actor only have to (and in this case only do) extend HTTPCaller.Command.

So imagine that we have something like

public class SomeOtherActor extends AbstractBehavior<SomeOtherActor.Command> {
  public interface Command;

  // yada yada yada
    ActorRef<HTTPCaller.Command> httpCallerActor = ...
    httpCallerActor.tell(new HTTPCaller.MakeRequest("someQuery", getContext().getSystem().ignoreRef());
}

In general, when defining messages which are sent in reply, those are not going to extend the message type of the sending actor. In HTTPCaller, for instance, Response probably shouldn't implements Command: it can be a standalone class (alternatively, if it is something that might be received by the HTTPCaller actor, it should be handled in the receive builder).

My code above does bring up one question: if Response is to be received by SomeOtherActor, how can it extend SomeOtherActor.Command?

The solution there is message adaptation: a function to convert a Response to a SomeOtherActorCommand. For example

// in SomeOtherActor

// the simplest possible adaptation:
public static final class ResponseFromHTTPCaller implements Command {
  public final String result;

  public ResponseFromHTTPCaller(HTTPCaller.Response response) {
    result = response.result;
  }

// at some point before telling the httpCallerActor...
// apologies if the Java lambda syntax is messed up...
ActorRef<HTTPCaller.Response> httpCallerResponseRef =
  getContext().messageAdapter(
    HTTPCaller.Response.class,
    (response) -> { new ResponseFromHTTPCaller(response) }
  );

httpCallerActor.tell(new HTTPCaller.MakeRequest("someQuery", httpCallerResponseRef);

There is also the ask pattern, which is more useful for one-shot interactions between actors where there's a timeout.

  • Related