Akka typed - actor lifecycle management

Akka typed actors are different from akka classic in terms of life cycle management, such as creation, enabling, state transition, deactivation and monitoring. In this article, we will introduce akka typed actor lifecycle management.

Each kind of actor forms a template by defining its behavior attribute behavior, and then generates an actor instance by using the spawn method for the parent actor of the upper layer. The generated actor instance is added to the top-down tree structure of a system, directly under the parent of spawn. The guardian actor of akka typed, that is, the root root actor, is specified and generated when the actor system is defined. As follows:

    val config = ConfigFactory.load("application.conf")
    val man: ActorSystem[GreetStarter.Command] = ActorSystem(GreetStarter(), "greetDemo",config)
    man ! GreetStarter.RepeatedGreeting("Tiger",1.seconds)

In a sense, the actor system instance man represents root actor. We can send messages to man, and then green starter's behavior will use its own ActorContext to spread, stop, watch and dispatch computing tasks. In fact, it is a program hub:

  object GreetStarter {
    import Messages._
    def apply(): Behavior[SayHi] = {
      Behaviors.setup { ctx =>
        val props = DispatcherSelector.fromConfig("akka.actor.default-blocking-io-dispatcher")
        val helloActor = ctx.spawn(HelloActor(), "hello-actor",props)
        val greeter = ctx.spawn(Greeter(helloActor), "greeter")
        ctx.watch(greeter)
        ctx.watchWith(helloActor,StopWorker("something happend"))
        Behaviors.receiveMessage { who =>
          if (who.name == "stop") {
            ctx.stop(helloActor)
            ctx.stop(greeter)
            Behaviors.stopped
          } else {
            greeter ! who
            Behaviors.same
          }
        }
      }
    }
  }

However, sometimes we need to make and use actors outside of the root actor's actor context. The following example on the official document is a good example:

import akka.actor.typed.Behavior
import akka.actor.typed.SpawnProtocol
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.LoggerOps

object HelloWorldMain {
  def apply(): Behavior[SpawnProtocol.Command] =
    Behaviors.setup { context =>
      // Start initial tasks
      // context.spawn(...)

      SpawnProtocol()
    }
}

object Main extends App {
implicit val system: ActorSystem[SpawnProtocol.Command] =
  ActorSystem(HelloWorldMain(), "hello")

// needed in implicit scope for ask (?)
import akka.actor.typed.scaladsl.AskPattern._
implicit val ec: ExecutionContext = system.executionContext
implicit val timeout: Timeout = Timeout(3.seconds)

val greeter: Future[ActorRef[HelloWorld.Greet]] =
  system.ask(SpawnProtocol.Spawn(behavior = HelloWorld(), name = "greeter", props = Props.empty, _))

val greetedBehavior = Behaviors.receive[HelloWorld.Greeted] { (context, message) =>
  context.log.info2("Greeting for {} from {}", message.whom, message.from)
  Behaviors.stopped
}

val greetedReplyTo: Future[ActorRef[HelloWorld.Greeted]] =
  system.ask(SpawnProtocol.Spawn(greetedBehavior, name = "", props = Props.empty, _))

for (greeterRef <- greeter; replyToRef <- greetedReplyTo) {
  greeterRef ! HelloWorld.Greet("Akka", replyToRef)
}
...
}

You can see that all operations are performed outside the actor framework. The SpawnProtocol itself is an actor, as follows:

object SpawnProtocol {

...
  final case class Spawn[T](behavior: Behavior[T], name: String, props: Props, replyTo: ActorRef[ActorRef[T]])
      extends Command
...
  def apply(): Behavior[Command] =
    Behaviors.receive { (ctx, msg) =>
      msg match {
        case Spawn(bhvr, name, props, replyTo) =>
          val ref =
            if (name == null || name.equals(""))
              ctx.spawnAnonymous(bhvr, props)
            else {

              @tailrec def spawnWithUniqueName(c: Int): ActorRef[Any] = {
                val nameSuggestion = if (c == 0) name else s"$name-$c"
                ctx.child(nameSuggestion) match {
                  case Some(_) => spawnWithUniqueName(c + 1) // already taken, try next
                  case None    => ctx.spawn(bhvr, nameSuggestion, props)
                }
              }

              spawnWithUniqueName(0)
            }
          replyTo ! ref
          Behaviors.same
      }
    }

}

By sending a Spawn message, the outside world specifies to generate a new actor.

The state switch of an actor is to change from one behavior to another. We can customize Behaviors or use out of the box Behaviors???. If only internal variable changes are involved, the current behavior with variables can be generated directly, as follows:

object HelloWorldBot {

  def apply(max: Int): Behavior[HelloWorld.Greeted] = {
    bot(0, max)
  }

  private def bot(greetingCounter: Int, max: Int): Behavior[HelloWorld.Greeted] =
    Behaviors.receive { (context, message) =>
      val n = greetingCounter + 1
      context.log.info2("Greeting {} for {}", n, message.whom)
      if (n == max) {
        Behaviors.stopped
      } else {
        message.from ! HelloWorld.Greet(message.whom, context.self)
        bot(n, max)
      }
    }
}

The deactivation of an actor can be performed by ActorContext.stop Or its own Behaviors.stopped To achieve. Behaviors.stopped You can bring in a cleanup function. Do some cleanup before the actor stops completely:

object MasterControlProgram {
  sealed trait Command
  final case class SpawnJob(name: String) extends Command
  case object GracefulShutdown extends Command

  // Predefined cleanup operation
  def cleanup(log: Logger): Unit = log.info("Cleaning up!")

  def apply(): Behavior[Command] = {
    Behaviors
      .receive[Command] { (context, message) =>
        message match {
          case SpawnJob(jobName) =>
            context.log.info("Spawning job {}!", jobName)
            context.spawn(Job(jobName), name = jobName)
            Behaviors.same
          case GracefulShutdown =>
            context.log.info("Initiating graceful shutdown...")
            // perform graceful stop, executing cleanup before final system termination
            // behavior executing cleanup is passed as a parameter to Actor.stopped
            Behaviors.stopped { () =>
              cleanup(context.system.log)
            }
        }
      }
      .receiveSignal {
        case (context, PostStop) =>
          context.log.info("Master Control Program stopped")
          Behaviors.same
      }
  }
}

In fact, when an actor turns into the disabled stop state, it can be obtained from the receiveSignal of another monitor actor, as follows:

  object GreetStarter {
    import Messages._
    def apply(): Behavior[SayHi] = {
      Behaviors.setup { ctx =>
        val props = DispatcherSelector.fromConfig("akka.actor.default-blocking-io-dispatcher")
        val helloActor = ctx.spawn(HelloActor(), "hello-actor",props)
        val greeter = ctx.spawn(Greeter(helloActor), "greeter")
        ctx.watch(greeter)
        ctx.watchWith(helloActor,StopWorker("something happend"))
        Behaviors.receiveMessage { who =>
          if (who.name == "stop") {
            ctx.stop(helloActor)
            ctx.stop(greeter)
            Behaviors.stopped
          } else {
            greeter ! who
            Behaviors.same
          }
        }.receiveSignal {
            case (context, Terminated(ref)) =>
              context.log.info("{} stopped!", ref.path.name)
              Behaviors.same
          }
      }
    }
  }

Here is the. receiveSignal function and its captured Signal message:

  trait Receive[T] extends Behavior[T] {
    def receiveSignal(onSignal: PartialFunction[(ActorContext[T], Signal), Behavior[T]]): Behavior[T]
  }



trait Signal

/**
 * Lifecycle signal that is fired upon restart of the Actor before replacing
 * the behavior with the fresh one (i.e. this signal is received within the
 * behavior that failed).
 */
sealed abstract class PreRestart extends Signal
case object PreRestart extends PreRestart {
  def instance: PreRestart = this
}

/**
 * Lifecycle signal that is fired after this actor and all its child actors
 * (transitively) have terminated. The [[Terminated]] signal is only sent to
 * registered watchers after this signal has been processed.
 */
sealed abstract class PostStop extends Signal
// comment copied onto object for better hints in IDEs
/**
 * Lifecycle signal that is fired after this actor and all its child actors
 * (transitively) have terminated. The [[Terminated]] signal is only sent to
 * registered watchers after this signal has been processed.
 */
case object PostStop extends PostStop {
  def instance: PostStop = this
}

object Terminated {
  def apply(ref: ActorRef[Nothing]): Terminated = new Terminated(ref)
  def unapply(t: Terminated): Option[ActorRef[Nothing]] = Some(t.ref)
}

Tags: Scala Attribute

Posted on Wed, 27 May 2020 08:44:11 -0400 by Hafkas