Effect of returning a supervised behavior from within a supervised behavior

I am studying Akka typed.
I have imagined a scenario that I don’t find covered in the documentation, and I am having some troubles understanding how it is supposed to work.
Suppose that I have a receive behavior external wrapped in a supervisor.
Suppose that, as part of some message processing logic, external returns a new receive behavior internal wrapped in a new supervisor.

  • Do the two supervisors and the failure handling logic have some kind of relationship?
  • If failures happen, what behavior are the PreRestart or PostStop signals passed to?

Thanks. :slightly_smiling_face:

To help myself in reasoning, I have made a code with the following characteristics:

  • When the external behavior receives the Change message, it becomes internal.
  • When any behavior receives a Crash1 message, it throws an Exception1; when it receives a Crash2 message, it throws an Exception2.
  • Both behaviors are wrapped in a supervisor with restart strategy.
  • The external supervisor supervises Exception1 exceptions.
  • The internal supervisor supervises Exception2 exceptions.
  • Both behaviors handle the PreRestart and PostStop signals.
public class Question {
    public interface Command {}
    public static final class Change implements Command {}
    public static final class Crash1 implements Command {}
    public static final class Crash2 implements Command {}
    public static final class Msg implements Command {}
    public static final class Exception1 extends Exception {}
    public static final class Exception2 extends Exception {}

    public static Behavior<Command> external() {
        return Behaviors.supervise(Behaviors.receive(Command.class)
            .onMessage(Msg.class, msg -> {
                System.out.println("External: received Msg, return same");
                return Behaviors.same();
            })
            .onMessage(Crash1.class, msg -> {
                System.out.println("External: received Crash1, throw Exception1");
                throw new Exception1();
            })
            .onMessage(Crash2.class, msg -> {
                System.out.println("External: received Crash2, throw Exception2");
                throw new Exception2();
            })
            .onMessage(Change.class, msg -> {
                System.out.println("External: received Change, return internal supervised behavior");

                return Behaviors.supervise(internal()).onFailure(Exception2.class, SupervisorStrategy.restart());
            })
            .onSignal(PreRestart.class, signal -> {
                System.out.println("External: PreRestart");
                return Behaviors.same();
            })
            .onSignal(PostStop.class, signal -> {
                System.out.println("External: PostStop");
                return Behaviors.same();
            })
            .build())
        onFailure(Exception1.class, SupervisorStrategy.restart());
    }

    public static Behavior<Command> internal() {
        return Behaviors.receive(Command.class)
            .onMessage(Msg.class, msg2 -> {
                System.out.println("Internal: received Msg, return same");
                return Behaviors.same();
            })
            .onMessage(Crash1.class, msg -> {
                System.out.println("Internal: received Crash1, throw Exception1");
                throw new Exception1();
            })
            .onMessage(Crash2.class, msg -> {
                System.out.println("Internal: received Crash2, throw Exception2");
                throw new Exception2();
            })
            .onSignal(PreRestart.class, signal -> {
                System.out.println("Internal: PreRestart");
                return Behaviors.same();
            })
            .onSignal(PostStop.class, signal -> {
                System.out.println("Internal: PostStop");
                return Behaviors.same();
            })
        .build();
    }
}

The main looks as follows:

final ActorSystem<Command> system = ActorSystem.create(external(), "helloakka");
system.tell(new Msg()); // External: received Msg, return same
system.tell(new Crash1()); // External: received Crash1, throw Exception1
// External: PreRestart
system.tell(new Change()); // External: received Change, return internal supervised behavior
system.tell(new Msg()); // Internal: received Msg, return same

Let’s continue the main in two different ways:

V1

system.tell(new Crash1()); // Internal: received Crash1, throw Exception1
// Internal: PreRestart
system.tell(new Msg()); // External: received Msg, return same

V2

system.tell(new Crash2()); // Internal: received Crash2, throw Exception2
// Internal: PreRestart
system.tell(new Msg()); // Internal: received Msg, return same

Let’s analyze the output produced by the two different scenarios

In V1:

  • Reception of Crash1 makes internal throw Exception1.
  • The PreRestart handler of internal is called.
  • Reception of Msg is handled by external.

In V2:

  • Reception of Crash2 makes internal throw Exception2.
  • The PreRestart handler of internal is called (same as before).
  • Reception of Msg is handled by internal (as opposite to external).

I can make the following conclusions:

  1. First, the PreRestart signal is passed to the behavior that produced a failure (which is, necessarily, the newest behavior). I conclude this because both output have // Internal: PreRestart. Additionally, this is what the documentation says.
  2. Second, the behavior is restarted. The restarted behavior is not necessarily the newest one, but the latest behavior whose supervisor actually matched failure. I conclude this because, in V1, the latest reception of Msg is done by external, whereas in V2 is done by internal. This is made possible by the fact that, in V1, Exception1 could be only matched by the external supervisor; whereas, in V2, Exception2 could be only matched by the internal supervisor.

The questions are: Have I deduced the correct conclusions? Are there other relationships that I am not aware of?

But, most importantly: Why is is like so? Is this just an implementation detail, or is there a rationale for doing it this way?

For example, why isn’t the PreRestart signal passed to the behavior that is about to be restarted (i.e., the one whose supervisor matched the failure? instead of the newest?

Edit

Edit: the documentation on fault-tolerance seems to partially confirm my conclusions:

Each returned behavior will be re-wrapped automatically with the supervisor.

It makes sense then that supervisors will be used in a LIFO order.

Other questions that came to my mind are the following:
The code I have provided show that a behavior can have many supervisors, that are used in a LIFO order. When a failure occurs, the most recently added supervisor capable of handling the failure is used.

Why is this LIFO approach used, as opposed to an approach with a single supervisor where a new supervisor replaces the previous one? Wouldn’t this be more intuitive? Is there perhaps a historical rationale for doing so?

As I mentioned before, the documentation on fault-tolerance seems to partially confirm my conclusions:

Each returned behavior will be re-wrapped automatically with the supervisor.

Using Git, I have been able to find the commit where this line was introduced, the GitHub issue and the GItHub fix.

@johanandren @patriknw I see that you have worked on this. Could you please clarify the semantics of returning a new supervised behavior? What would the use case be?
Perhaps there is no use case at all.
Perhaps, this is possible because there is no reasonable way to prevent it using the type system.

Typically it is used in a way that the supervisors are defined once, and then the inner behaviors are switched. It would be very inconvenient to have to re-define supervisor for each beavior switch.

Another aspect is that the supervisor can be defined by the parent behavior before spawing the child behavior. Then the child behavior shouldn’t even be aware of how errors are handled, but it should still be able to switch behavior of itself.

One use of preRestart is to cleanup or close resources, and those belong tho the behavior that was used, not the one that will be used.

The main reason for defining more than one Behaviors.supervise, wrapping each other, is to have different strategy for different exceptions. They are evaluated in order inner-to-outer

Thanks for your patience and your answer, Patrik.

Typically it is used in a way that the supervisors are defined once, and then the inner behaviors are switched. It would be very inconvenient to have to re-define supervisor for each beavior switch.

Agreed.

One use of preRestart is to cleanup or close resources, and those belong tho the behavior that was used, not the one that will be used.

Makes sense.

The main reason for defining more than one Behaviors.supervise, wrapping each other, is to have different strategy for different exceptions. They are evaluated in order inner-to-outer

Yes, I am aware that, to handle different exceptions with different strategies, calls to supervise can be nested (mentioned in the documentation).

However, the source of confusion is something a bit different. Take a look at the following example (which is a shortened version of the code I originally posted). In the example, a supervised Receive behavior returns a new supervised Receive behavior.

The example does not implement anything useful: I wrote it just to test what would happen; which you have now clarified: upon failure, all supervisors installed so far are evaluated, from the innermost to the outermost (i.e., from the supervisor decorating the behavior that was most recently returned, to the least recently returned).

I am wondering if there is a more real-world scenario where returning a supervised Receive behavior from a supervised Receive behavior makes sense.

The only scenarios I can come up with are:

  • When the newly returned behavior fails in a new way (i.e., throws an new type of Exception that the previous behavior could not), and we want to add a new supervisor for that new type of Exception.
  • When the newly returned behavior fails in the same way of the previous (i.e., throws the same types of Exceptions), but we want to override an outer supervisor (e.g., the outer supervisor restarts the behavior upon IllegalArgumentException, and we now want to stop it instead).

What do you think?

Cross posted on StackOverflow with better explanation