Limor Stotland
Limor Stotland

Reputation: 88

Gracefully shutdown different supervisor actors without duplicating code

I have an API that creates actor A (at runtime). Then, A creates Actor B (at runtime as well).

I have another API that creates Actor C (different from actor A, No command code between them) and C creates Actor D.

I want to gracefully shutdown A and C once B and D has finished processing their messages (A and C not necessarily run together, They are unrelated).

Sending poison pill to A/C is not good enough because the children (B/D) will still get context stop, and will not be able to finish their tasks.

I understand I need to implement a new type of message. I didn't understand how to create an infrastructure so both A and C will know how to respond to this message without having same duplicate receive method in both.

The solution I found was to create a new trait that extends Actor and override the unhandled method. The code looks like this:

object SuicideActor {
  case class PleaseKillYourself()
  case class IKilledMyself()
}

trait SuicideActor extends Actor {
 override def unhandled(message: Any): Unit = message match {
    case PleaseKillYourself =>
      Logger.debug(s"Actor ${self.path} received PleaseKillYourself - stopping children and aborting...")
      val livingChildren = context.children.size
      if (livingChildren == 0) {
        endLife()
      } else {
        context.children.foreach(_ ! PleaseKillYourself)
        context become waitForChildren(livingChildren)
      }
    case _ => super.unhandled(message)
  }

  protected[crystalball] def waitForChildren(livingChildren: Int): Receive = {
    case IKilledMyself =>
      val remaining = livingChildren - 1
      if (remaining == 0) { endLife() }
      else { context become waitForChildren(remaining) }
  }

  private def endLife(): Unit = {
    context.parent ! IKilledMyself
    context stop self
  }
}

But this sound a bit hacky.... Is there a better (non hacky) solution ?

Upvotes: 1

Views: 60

Answers (2)

Limor Stotland
Limor Stotland

Reputation: 88

So it took me a bit but I find my answer. I implemented The Reaper Pattern

The SuicideActor create a dedicated Reaper actor when it finished its block. The Reaper watch all of the SuicideActor children and once they all Terminated it send a PoisonPill to the SuicideActor and to itself

The SuicideActor code is :

trait SuicideActor extends  Actor  {

  def killSwitch(block: => Unit): Unit = {
    block
    Logger.info(s"Actor ${self.path.name} is commencing suicide sequence...")
    context become PartialFunction.empty
    val children = context.children
    val reaper = context.system.actorOf(ReaperActor.props(self), s"ReaperFor${self.path.name}")
    reaper ! Reap(children.toSeq)
  }

  override def postStop(): Unit = Logger.debug(s"Actor ${self.path.name} is dead.")

}

And the Reaper is: object ReaperActor {

  case class Reap(underWatch: Seq[ActorRef])

  def props(supervisor: ActorRef): Props = {
    Props(new ReaperActor(supervisor))
  }
}

class ReaperActor(supervisor: ActorRef) extends Actor {

  override def preStart(): Unit = Logger.info(s"Reaper for ${supervisor.path.name} started")
  override def postStop(): Unit = Logger.info(s"Reaper for ${supervisor.path.name} ended")

  override def receive: Receive = {
    case Reap(underWatch) =>
      if (underWatch.isEmpty) {
        killLeftOvers
      } else {
        underWatch.foreach(context.watch)
        context become reapRemaining(underWatch.size)
        underWatch.foreach(_ ! PoisonPill)
      }
  }

  def reapRemaining(livingActorsNumber: Int): Receive = {
    case Terminated(_) =>
      val remainingActorsNumber = livingActorsNumber - 1
      if (remainingActorsNumber == 0) {
        killLeftOvers
      } else {
        context become reapRemaining(remainingActorsNumber)
      }
  }

  private def killLeftOvers = {
    Logger.debug(s"All children of ${supervisor.path.name} are dead killing supervisor")
    supervisor ! PoisonPill
    self ! PoisonPill
  }
}

Upvotes: 0

Ivan Stanislavciuc
Ivan Stanislavciuc

Reputation: 7275

I think designing your own termination procedure is not necessary and potentially can cause headache for future maintainers of you code as this is nonstandard akka behaviour that needs to be yet again understood.

If A and C unrelated and can terminate independently, the following options are possible. To avoid any confusion, I'll use just actor A and B in my explanations.

Option 1.

Actor A uses context.watch on newly created actor B and reacts on Terminated message in its receive method. Actor B calls context.stop(context.self) when it's done with its task and this will generate Terminated event that will be handled by actor A that can clean up its state if needed and terminate too.

Check these docs for more details.

Option 2.

Actor B calls context.stop(context.parent) to terminate the parent directly when it's done with its own task. This option does not allow parent to react and perform additional clean up tasks if needed.

Finally, sharing this logic between actors A and C can be done with a trait in the way you did but the logic is very small and having duplicated code is not all the time a bad thing.

Upvotes: 2

Related Questions