akka-typed - typed-actor, typed messages

Posted by aod on Tue, 26 May 2020 18:22:55 +0200

It has been a while since akka 2.6.x was officially released.The core change is the formal enabling of typed-actor s, but there are also big changes in modules such as persistence,cluster, and so on.Name estimation starts with changing traditional anytype messages to strongly typed ones, so you'll want to take a moment to see how this can have a profound impact on our existing akka-classic-based applications.But some of the system architectures I've considered recently forced me to start investigating akka-typed right away, which means that akka-classic is no longer able or difficult to implement a new system architecture, and let me hear: Recently I'm considering a microservice platform.As the only entry point for background data service calls, the platform should be a distributed software, so using akka-cluster is the only option at present, after all, many akka-cluster-based applications have been developed in the past.However, akka-cluster-sharding can only support one entity actor.After all, since akka-classic messages are untyped, code that should run can only be determined by type pattern matching after receiving the message.Therefore, this actor must include all business logic processing operations.That is, for a large application, this is a huge piece of code.Also, if it involves maintaining the state of actors, such as persistenceActor, or integrated business operations, how many kinds of data structures are needed and how are they maintained and managed?This is basically mission-impossible to me.In fact, logom should meet the requirements of this platform: cluster-sharding, CQRS.. With a curious mind, I know the source of lagom, and suddenly realize: this thing is based on akka-typed!Just think about it: if we can bind actors to message types, then we can correspond to an actor through message types.That is, based on akka-typed, we can divide a comprehensive business into multiple actor modules, and then we can specify which actor does those things.Of course, the design of actors is much simpler after functional subdivisions.Now this new platform can make foreground applications call the corresponding actors directly to process business.Don't worry, this is bound to be the future of akka apps. What else?

Start with the simplest hello program: basically two actors exchange messages with each other.Start with the first to demonstrate the standard actor building process:

 

  object HelloActor {
    sealed trait Request
    case class Greeting(fromWhom: String, replyTo: ActorRef[Greeter.Greeted]) extends Request

    def apply(): Behavior[Greeting] = {
      Behaviors.receive { (ctx, greeter) =>
        ctx.log.info("receive greeting from {}", greeter.fromWhom)
        greeter.replyTo ! Greeter.Greeted(s"hello ${greeter.fromWhom}!")
        Behaviors.same
      }
    }
  }

 

Akka-typed actors are built by defining its Behavior behavior.In particular, the type parameter Behavior[Greeting], which means that this actor only processes messages of type Greeting, is a typed-actor.sender() is no longer supported by akka-typed and comes with messages such as Greeting.replyTo.Behavior definitions are implemented through factory mode Behaviors, to see what Behaviors are defined:

 

/**
 * Factories for [[akka.actor.typed.Behavior]].
 */
object Behaviors {
  def setup[T](factory: ActorContext[T] => Behavior[T]): Behavior[T] 

  def withStash[T](capacity: Int)(factory: StashBuffer[T] => Behavior[T]): Behavior[T] 

  def same[T]: Behavior[T] 

  def unhandled[T]: Behavior[T] 

  def stopped[T]: Behavior[T] 

  def stopped[T](postStop: () => Unit): Behavior[T]

  def empty[T]: Behavior[T]

  def ignore[T]: Behavior[T] 

  def receive[T](onMessage: (ActorContext[T], T) => Behavior[T]): Receive[T]

  def receiveMessage[T](onMessage: T => Behavior[T]): Receive[T]

  def receivePartial[T](onMessage: PartialFunction[(ActorContext[T], T), Behavior[T]]): Receive[T] 
 
  def receiveMessagePartial[T](onMessage: PartialFunction[T, Behavior[T]]): Receive[T] 

  def receiveSignal[T](handler: PartialFunction[(ActorContext[T], Signal), Behavior[T]]): Behavior[T] 

  def supervise[T](wrapped: Behavior[T]): Supervise[T] 

  def withTimers[T](factory: TimerScheduler[T] => Behavior[T]): Behavior[T] 
 
 ...

}

In addition to returning Behavior[T], the above constructor also has Receive[T] and Supervise[T]. What are the two types?They are also Behavior[T]:

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


  def supervise[T](wrapped: Behavior[T]): Supervise[T] =
    new Supervise[T](wrapped)

  private final val ThrowableClassTag = ClassTag(classOf[Throwable])
  final class Supervise[T] private[akka] (val wrapped: Behavior[T]) extends AnyVal {

    /** Specify the [[SupervisorStrategy]] to be invoked when the wrapped behavior throws. */
    def onFailure[Thr <: Throwable: ClassTag](strategy: SupervisorStrategy): Behavior[T] = {
      val tag = classTag[Thr]
      val effectiveTag = if (tag == ClassTag.Nothing) ThrowableClassTag else tag
      Supervisor(Behavior.validateAsInitial(wrapped), strategy)(effectiveTag)
    }
  }

Be careful,Supervise.onFailure Returned Behavior[T].

HelloActor's Behavior is through Behaviors.receive Built.It can also be built with setup,receiveMessage.Note: The built-in parameters are also Behavior[T], so these constructors can be used nested one layer at a time.setup,receive provides ActorContext for the function inner layer, withTimers provides TimerScheduler[T].Then I can refine the functionality of HelloActor by adding Supervisor Strategy:

  object HelloActor {
    sealed trait Request
    case class Greeting(fromWhom: String, replyTo: ActorRef[Greeter.Greeted]) extends Request

    def apply(): Behavior[Greeting] = {
      Behaviors.supervise(
        Behaviors.receive[Greeting] { (ctx, greeter) =>
          ctx.log.info("receive greeting from {}", greeter.fromWhom)
          greeter.replyTo ! Greeter.Greeted(s"hello ${greeter.fromWhom}!")
          Behaviors.same
        }
      ).onFailure(SupervisorStrategy.restartWithBackoff(10.seconds, 1.minute, 0.20))
    }
  }

In akka-typed, actor regulation has moved from parent to self.Adding BackOff-Supervisor Strategy eliminates the need for a separate BackOff Supervisor actor.

Look at another Greeter:

 object Greeter {

    sealed trait Response
    case class Greeted(hello: String) extends Response
    
    def apply(): Behavior[Greeted] = {
      Behaviors.setup ( ctx =>
        Behaviors.receiveMessage { message =>
          ctx.log.info(message.hello)
          Behaviors.same
        }
      )
    }
  }

This is not different from HelloActor, but it uses the setup,receiveMessage suite.It is worth noting that Greeter is responsible for handling Greeted messages, a type without sender ActorRef, which means there is no need to reply to the sender after processing such messages.

Then you need an actor to build the two actor instances above and start a conversation:

 object GreetStarter {
    sealed trait Command
    case class SayHiTo(whom: String) extends Command
    case class RepeatedGreeting(whom: String, interval: FiniteDuration) extends Command

    def apply(): Behavior[Command] = {
      Behaviors.setup[Command] { ctx =>
        val helloActor = ctx.spawn(HelloActor(), "hello-actor")
        val greeter = ctx.spawn(Greeter(), "greeter")
        Behaviors.withTimers { timer =>
          new GreetStarter(
            helloActor,greeter,ctx,timer)
            .repeatGreeting(1,3)
        }
      }
    }
  }
  class GreetStarter private (
     helloActor: ActorRef[HelloActor.Greeting],
     greeter: ActorRef[Greeter.Greeted],
     ctx: ActorContext[GreetStarter.Command],
     timer: TimerScheduler[GreetStarter.Command]){
    import GreetStarter._

    private def repeatGreeting(count: Int, max: Int): Behavior[Command] =
       Behaviors.receiveMessage { msg =>
         msg match {
           case RepeatedGreeting(whom, interval) =>
             ctx.log.info2("start greeting to {} with interval {}", whom, interval)
             timer.startSingleTimer(SayHiTo(whom), interval)
             Behaviors.same
           case SayHiTo(whom) =>
             ctx.log.info2("{}th time greeting to {}",count,whom)
             if (max == count)
               Behaviors.stopped
             else {
               helloActor ! HelloActor.Greeting(whom, greeter)
               repeatGreeting(count + 1, max)
             }
         }
       }
  }

The above example is a bit complex and has some logic problems, mainly to demonstrate a fictitious model of functional actor construction and actor state transition.akka-typed no longer supports the becom method.

Finally, you need a top-level program equivalent to main:

  def main(args: Array[String]) {
    val man: ActorSystem[GreetStarter.Command] = ActorSystem(GreetStarter(), "greetDemo")
    man ! GreetStarter.RepeatedGreeting("Tiger",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Peter",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Susanna",5.seconds)
  }

The top-level actor for akka-classic, that is, /users, is created by default by the system.Akka-typed requires the user to provide this top-level actor.This is specified in the first parameter of ActorSystem.Let's look again at the constructor for akka-typed Actor System:

object ActorSystem {

  /**
   * Scala API: Create an ActorSystem
   */
  def apply[T](guardianBehavior: Behavior[T], name: String): ActorSystem[T] =
    createInternal(name, guardianBehavior, Props.empty, ActorSystemSetup.create(BootstrapSetup()))

  /**
   * Scala API: Create an ActorSystem
   */
  def apply[T](guardianBehavior: Behavior[T], name: String, config: Config): ActorSystem[T] =
    createInternal(name, guardianBehavior, Props.empty, ActorSystemSetup.create(BootstrapSetup(config)))

  /**
   * Scala API: Create an ActorSystem
   */
  def apply[T](guardianBehavior: Behavior[T], name: String, config: Config, guardianProps: Props): ActorSystem[T] =
    createInternal(name, guardianBehavior, guardianProps, ActorSystemSetup.create(BootstrapSetup(config)))
...
}

One of the apply is built in a similar way to akka-classic's Actor System:

  def main(args: Array[String]) {
    val config = ConfigFactory.load("application.conf")
    val man: ActorSystem[GreetStarter.Command] = ActorSystem(GreetStarter(), "greetDemo",config)
    man ! GreetStarter.RepeatedGreeting("Tiger",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Peter",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Susanna",5.seconds)
  }

Here is the full source code for this discussion:

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.2"

lazy val akkaVersion = "2.6.5"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed"            % akkaVersion,
  "ch.qos.logback"     % "logback-classic"             % "1.2.3"
)

fork in Test := true

Lesson01.scala

import akka.actor.typed._
import scaladsl._
import scala.concurrent.duration._
import com.typesafe.config._
object Lesson01 {

  object HelloActor {
    sealed trait Request
    case class Greeting(fromWhom: String, replyTo: ActorRef[Greeter.Greeted]) extends Request

    def apply(): Behavior[Greeting] = {
      Behaviors.supervise(
        Behaviors.receive[Greeting] { (ctx, greeter) =>
          ctx.log.info("receive greeting from {}", greeter.fromWhom)
          greeter.replyTo ! Greeter.Greeted(s"hello ${greeter.fromWhom}!")
          Behaviors.same
        }
      ).onFailure(SupervisorStrategy.restartWithBackoff(10.seconds, 1.minute, 0.20))
    }
  }

  object Greeter {

    sealed trait Response
    case class Greeted(hello: String) extends Response

    def apply(): Behavior[Greeted] = {
      Behaviors.setup ( ctx =>
        Behaviors.receiveMessage { message =>
          ctx.log.info(message.hello)
          Behaviors.same
        }
      )
    }
  }

  object GreetStarter {
    sealed trait Command
    case class SayHiTo(whom: String) extends Command
    case class RepeatedGreeting(whom: String, interval: FiniteDuration) extends Command

    def apply(): Behavior[Command] = {
      Behaviors.setup[Command] { ctx =>
        val helloActor = ctx.spawn(HelloActor(), "hello-actor")
        val greeter = ctx.spawn(Greeter(), "greeter")
        Behaviors.withTimers { timer =>
          new GreetStarter(
            helloActor,greeter,ctx,timer)
            .repeatGreeting(1,3)
        }
      }
    }
  }
  class GreetStarter private (
     helloActor: ActorRef[HelloActor.Greeting],
     greeter: ActorRef[Greeter.Greeted],
     ctx: ActorContext[GreetStarter.Command],
     timer: TimerScheduler[GreetStarter.Command]){
    import GreetStarter._

    private def repeatGreeting(count: Int, max: Int): Behavior[Command] =
       Behaviors.receiveMessage { msg =>
         msg match {
           case RepeatedGreeting(whom, interval) =>
             ctx.log.info2("start greeting to {} with interval {}", whom, interval)
             timer.startSingleTimer(SayHiTo(whom), interval)
             Behaviors.same
           case SayHiTo(whom) =>
             ctx.log.info2("{}th time greeting to {}",count,whom)
             if (max == count)
               Behaviors.stopped
             else {
               helloActor ! HelloActor.Greeting(whom, greeter)
               repeatGreeting(count + 1, max)
             }
         }
       }
  }


  def main(args: Array[String]) {
    val config = ConfigFactory.load("application.conf")
    val man: ActorSystem[GreetStarter.Command] = ActorSystem(GreetStarter(), "greetDemo",config)
    man ! GreetStarter.RepeatedGreeting("Tiger",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Peter",5.seconds)
    man ! GreetStarter.RepeatedGreeting("Susanna",5.seconds)
  }
}

Topics: Scala supervisor