Akka Design Principals

2019-03-21 10:55发布

问题:

Whilst working on a fairly large Akka application, I have come across a very simple structure when working with normal methods and non Akka classes but which are actually quite difficult to nail when working with Akka which is why I have come here to ask what you recommend to be the best way to solve this issue.

So the issue is this, I have one parent actor, let's call him "Connector", Connector has behavior describing what it should do when it receives a ConnectCommand instance. First it submit a form using an HttpClient, then it goes to a couple of URLs to check for some session parameters and eventually sends the Sender (Referred to as the "Consumer") a connection message containing everything it needs to use the API.

Now, I'm a big fan of tell, not so much of pull / ask so implementing this is in my opinion a hard task to do. Let's go over it. All the responses returned by the HttpClientActor are a Response instance, so what first came to mind is having multiple behaviors defined in my actor and incrementally, after a certain step of the connection process has been completed, change behavior to the next step.

private final PartialFunction<Object, BoxedUnit> inital = ReceiveBuilder
    .match(ConnectCommand.class, c -> this.startConnection())
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage1 = ReceiveBuilder
    .match(Response.class, this::stage1)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage2 = ReceiveBuilder
    .match(Response.class, this::stage2)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage3 = ReceiveBuilder
    .match(Response.class, this::stage3)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage4 = ReceiveBuilder
    .match(Response.class, this::stage4)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage5 = ReceiveBuilder
    .match(Response.class, this::stage5)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage6 = ReceiveBuilder
    .match(Response.class, this::stage6)
    .matchAny(this::unhandled)
    .build();

private final PartialFunction<Object, BoxedUnit> stage7 = ReceiveBuilder
    .match(Response.class, this::stage7)
    .matchAny(this::unhandled)
    .build();

This has the advantage that it is using tell, not ask but has the major drawback that code becomes very unreadable.

Now I'm at the point I feel like this Actor needs some change in a good way but there are two alternatives in my opinion.

The first one involves splitting up every HttpRequest and Response into a separate Actor and aggregating the results in the Connector actor. This has the advantage of being very readable, using tell and shouldn't hurt performance because Akka is built to handle lots of actors. The only drawback to this is that I need to create a lot of container classes for these parts of state that need to be delivered from Stage5Actor to the Connector actor. This makes for a large memory overhead (correct me if I'm wrong).

The second approach is using the Ask pattern to wire the steps together. This would result in a single Connector actor and since Spray does so too for it's Http Client I think it may be a valid solution. Only drawback to this is, because everything rests on top of an external Http API, timeouts may become an issue. If this approach is recommend by the Akka team, how does one handle with all the timeouts which are completely unpredictable.

Please note that this implementation needs to be able to use supervision strategies since our whole current approach is based on top of this.

If you feel there is a far better solution than the ones I mentioned, please do tell! I'm really enjoying Akka a.t.m. and every piece of advice I get it, is a gain in experience and knowledge, not only for me, but for the whole community :D. Plus I think this is an issue more people run into every once in a while.

Thanks in advance and a big thanks to the Akka team for producing such an awesome lib!

PS. This question was first asked on the Akka github itself but I decided to post it here because this is just as much an Actor model related question as an Akka related question.

Link to the issue on github: https://github.com/akka/akka/issues/16080

回答1:

It seems to me like you don't necessarily need N stages to collect the responses. If you know how many responses you're waiting for you can collect them all under a single behavior.

Also, in any scenario where you are using ask, you can easily replace it with an intermediary Actor to hold this context and pass all messages through tell. That's actually what ask does anyway, but the main differences are that you wouldn't have to deal with specifying timeouts for every ask (well, you should still have a timeout for the overall request) and you can bundle all outstanding stages in a single Actor instead of an extra Actor for every ask.

Jamie Allen has really good description of this scenario as the Extra and Cameo Patterns in Effective Akka.

So with all this in mind, you might be able to follow something along the lines of:

  • When Consumer send the message to Connector, Connector can create a new Actor (Cameo) for this request context. You have to capture the sender Consumer in this Actor.
  • The Cameo Actor can kick off the subsequent requests through tells. At this point you can make either the Cameo or the Connector as the Supervisor so your Supervision Strategies still work as you want.
  • The Cameo in it's Receive block can wait for responses from Connections. This doesn't have to be an await on the ask. Just accept the message in the receive and then update your internal state.
  • When all the Connections are complete, you can respond to the original Consumer through tell.