How to find all children of an actor?

How to find all children of an actor?

This topic is based on a conversation between an anonymous user of Akka and our “Ask AI about Akka” bot in the documentation.

In Akka, you can retrieve the ActorRef of started child actors using the children method of the ActorContext. However, the type of the returned ActorRef is unknown, since different types can be used for different children. Therefore, this is not a useful way to lookup children when the purpose is to send messages to them.

Instead of finding children via the ActorContext, it is recommended to use an application specific collection for bookkeeping of children, such as a Map[String, ActorRef[Child.Command]]. Here is an example in Scala:

object Parent {
  sealed trait Command
  case class DelegateToChild(name: String, message: Child.Command) extends Command
  private case class ChildTerminated(name: String) extends Command

  def apply(): Behavior[Command] = {
    def updated(children: Map[String, ActorRef[Child.Command]]): Behavior[Command] = {
      Behaviors.receive { (context, command) =>
        command match {
          case DelegateToChild(name, childCommand) =>
            children.get(name) match {
              case Some(ref) =>
                ref ! childCommand
                Behaviors.same
              case None =>
                val ref = context.spawn(Child(), name)
                context.watchWith(ref, ChildTerminated(name))
                ref ! childCommand
                updated(children + (name -> ref))
            }

          case ChildTerminated(name) =>
            updated(children - name)
        }
      }
    }

    updated(Map.empty)
  }
}

And here is an example in Java:

public class Parent extends AbstractBehavior<Parent.Command> {

  public interface Command {}

  public static class DelegateToChild implements Command {
    public final String name;
    public final Child.Command message;

    public DelegateToChild(String name, Child.Command message) {
      this.name = name;
      this.message = message;
    }
  }

  // ... rest of the code ...

  private Behavior<Command> onDelegateToChild(DelegateToChild command) {
    ActorRef<Child.Command> ref = children.get(command.name);
    if (ref == null) {
      ref = getContext().spawn(Child.create(), command.name);
      getContext().watchWith(ref, new ChildTerminated(command.name));
      children.put(command.name, ref);
    }
    ref.tell(command.message);
    return this;
  }

  private Behavior<Command> onChildTerminated(ChildTerminated command) {
    children.remove(command.name);
    return this;
  }
}

Remember to remove entries from the Map when the children are terminated. For that purpose it’s convenient to use watchWith, as illustrated in the example above, because then you can include the key to the Map in the termination message. In that way the name of the actor doesn’t have to be the same as identifier used for bookkeeping.

Retrieving the children from the ActorContext can still be useful for some use cases, such as:

  • see if a child name is in use
  • stopping children
  • the type of the child is well known and unsafeUpcast of the ActorRef is considered “safe enough”

Sources:

How can I issue a PoisonPill event to all the children of an actor?

In Akka, you can send a PoisonPill message to an actor to stop it. If you want to stop all children of an actor, you can iterate over the children and send a PoisonPill to each one. However, please note that PoisonPill is not supported in Akka Typed. Instead, you should define a message that the actor understands and let it return Behaviors.stopped when receiving that message.

Here is an example of how you might do this in Akka Classic:

Scala:

context.children.foreach(_ ! PoisonPill)

Java:

for (ActorRef child : getContext().getChildren()) {
    child.tell(PoisonPill.getInstance(), getSelf());
}

If you are using Akka Typed, you would need to define a custom stop message:

Scala:

sealed trait Command
case object Stop extends Command

def apply(): Behavior[Command] = Behaviors.receive { (context, message) =>
  message match {
    case Stop => Behaviors.stopped
  }
}

// To stop all children
context.children.foreach(_ ! Stop)

Java:

public interface Command {}
public enum Stop implements Command { INSTANCE }

public Behavior<Command> create() {
    return Behaviors.receive(Command.class)
        .onMessage(Stop.class, msg -> Behaviors.stopped())
        .build();
}

// To stop all children
for (ActorRef<Command> child : getContext().getChildren()) {
    child.tell(Stop.INSTANCE);
}

Please note that the PoisonPill message will be enqueued as ordinary messages and will be handled after messages that were already queued in the mailbox. This means that the actor will continue processing its current messages before it stops.

Sources: