Akka typed - typed actor communication mode and protocol

Akka system is a distributed message driven system. Akka applications are composed of a group of actors who are responsible for different operations. Each actor is passively waiting for some external message to drive its own jobs. So, popular point description: akka application is a system that a group of actors send messages to each other, and each actor begins to take charge of the work after receiving the messages. For akka typed, the typed actor can only receive messages of the specified type, so the message communication between actors needs to be carried out according to the message type, that is, protocol is needed to regulate the message communication mechanism. Think about it. If a user needs an actor to do something, he must use the message type understood by the actor to send messages. This is an exchange protocol.

The so-called way of information exchange includes one-way and two-way. If the message exchange between two actors is involved, the message sending method can be one-way and two-way. However, if the message is sent to an actor from the outside, it can only be sent in a one-way way way, because only one end of the message is the actor.

A typical one-way message sending fire and forget is as follows:

import akka.actor.typed._
import scaladsl._

object Printer {
  case class PrintMe(message: String)
// Receive only PrintMe type message
  def apply(): Behavior[PrintMe] =
    Behaviors.receive {
      case (context, PrintMe(message)) =>
        context.log.info(message)
        Behaviors.same
    }
}

object FireAndGo extends App {
  // system It's a root-actor
val system: ActorRef[Printer.PrintMe] = ActorSystem(Printer(), "fire-and-forget-sample")
val printer: ActorRef[Printer.PrintMe] = system
// One way message sending, printMe Message of type
printer ! Printer.PrintMe("hello")
printer ! Printer.PrintMe("world!")

system.asInstanceOf[ActorSystem[Printer.PrintMe]].terminate()

}

Of course, in reality, we usually require the actor to perform some operations and then return the results. This involves two-way information exchange between actors. In the first case, the messages between two actors are arbitrary and unordered, which is a typical unordered request response pattern. That is to say, a response does not have to be returned in the order of receiving the request, only they can communicate with each other. However, in akka typed, the basic requirement of this mode is that the type of message sent must conform to the type of receiver actor.

OK, let's demonstrate this model first. All actors can be defined starting with their message types. For each actor participating in two-way communication, its function can be reflected from two kinds of messages: request and response:

object FrontEnd {
  sealed trait FrontMessages
  case class SayHi(who: String) extends FrontMessages
}
object BackEnd {
  //First from this actor Response message start for
     sealed trait Response
     case class  HowAreU(msg: String) extends Response
     case object Unknown extends Response

  //Types of messages that can be received
  sealed trait BackMessages
  //this replyTo It should be able to handle Reponse Of type message actor
  case class MakeHello(who: String, replyTo: ActorRef[Response]) extends BackMessages

}

This FrontEnd starts to work after receiving the SayHi message, but the returned message type has not been defined yet. BackEnd returns a response type message after receiving a MakeHello type message. From this perspective, the returned opposite actor must be able to handle the response type message.

Let's try to implement this FrontEnd actor:

object FrontEnd {
  sealed trait FrontMessages
  case class SayHi(who: String) extends FrontMessages
  
  def apply(backEnd: ActorRef[BackEnd.BackMessages]): Behavior[FrontMessages] =  {
     Behaviors.receive { (ctx,msg) => msg match {
       case SayHi(who) =>
         ctx.log.info("requested to say hi to {}", who)
         backEnd ! BackEnd.MakeHello(who, ???)
       
     }
  }
}

MakeHello needs a replyTo. What should it be? However, it must be an actor that can handle Response type messages. But we know that replyTo is FrontEnd, but FrontEnd can only handle FrontMessages. What should we do? Can you write replyTo directly as FrontEnd? Although this can be done, the MakeHello message can only be bound to FrontEnd. If other actors need to use MakeHello, they need to define another one. So the best solution is to implement it in a certain type conversion way. As follows:

import akka.actor.typed._
import scaladsl._

object FrontEnd {
  sealed trait FrontMessages
  case class SayHi(who: String) extends FrontMessages

  case class WrappedBackEndResonse(res: BackEnd.Response) extends FrontMessages

  def apply(backEnd: ActorRef[BackEnd.BackMessages]): Behavior[FrontMessages] = {
    Behaviors.setup[FrontMessages] { ctx =>
                                                   //ctx.messageAdapter(ref => WrappedBackEndResonse(ref))
      val backEndRef: ActorRef[BackEnd.Response] = ctx.messageAdapter(WrappedBackEndResonse)
      Behaviors.receive { (ctx, msg) =>
        msg match {
          case SayHi(who) =>
            ctx.log.info("requested to say hi to {}", who)
            backEnd ! BackEnd.MakeHello(who, backEndRef)
            Behaviors.same
          //messageAdapter take BackEnd.Response convert to WrappedBackEndResponse
          case WrappedBackEndResonse(msg) => msg match {
            case BackEnd.HowAreU(msg) =>
              ctx.log.info(msg)
              Behaviors.same
            case BackEnd.Unknown =>
              ctx.log.info("Unable to say hello")
              Behaviors.same
          }
        }
      }
    }
  }
}

First of all, we use ctx.mesageAdapter Produced ActorRef[BackEnd.Response ], which is the replyTo we need to provide to the MakeHello message. Take a look at the messageAdapter function:

def messageAdapter[U: ClassTag](f: U => T): ActorRef[U]

If we do type substitution U - > BackEnd.Response , T - > frontmessage then:

      val backEndRef: ActorRef[BackEnd.Response] = 
            ctx.messageAdapter((response: BackEnd.Response) => WrappedBackEndResonse(response))

In fact, the messageAdapter function registers a BackEnd.Response Type to FrontMessages. Put the received BackEnd.Response Convert to WrappedBackEndResponse(response) now.

The first mock exam is a two-way communication between two actor, which is 1:1 request-response, one to one mode. One to one means that the sender waits for a response message after sending it. This means that the receiver needs to send a response to the sender immediately after completing the calculation task, otherwise the timeout exception of the sender will be caused. Inevitably, this pattern still involves conversion of message types, as follows:

object FrontEnd {
  sealed trait FrontMessages
  case class SayHi(who: String) extends FrontMessages

  case class WrappedBackEndResonse(res: BackEnd.Response) extends FrontMessages
  case class ErrorResponse(errmsg: String) extends FrontMessages

  def apply(backEnd: ActorRef[BackEnd.BackMessages]): Behavior[FrontMessages] = {
    Behaviors.setup[FrontMessages] { ctx =>
      //ask Maximum timeout required
      import scala.concurrent.duration._
      import scala.util._
      implicit val timeOut: Timeout = 3.seconds
      Behaviors.receive[FrontMessages] { (ctx, msg) =>
        msg match {
          case SayHi(who) =>
            ctx.log.info("requested to say hi to {}", who)
            
            ctx.ask(backEnd,(backEndRef: ActorRef[BackEnd.Response]) => BackEnd.MakeHello(who,backEndRef) ){
              case Success(backResponse) => WrappedBackEndResonse(backResponse)
              case Failure(err) =>ErrorResponse(err.getLocalizedMessage)
            }
            Behaviors.same

          case WrappedBackEndResonse(msg) => msg match {
            case BackEnd.HowAreU(msg) =>
              ctx.log.info(msg)
              Behaviors.same
            case BackEnd.Unknown =>
              ctx.log.info("Unable to say hello")
              Behaviors.same
          }
          case ErrorResponse(errmsg) =>
            ctx.log.info("ask error: {}",errmsg)
            Behaviors.same
        }
      }
    }
  }
}

It seems that type conversion is implemented in ask. Look at this function:

  def ask[Req, Res](target: RecipientRef[Req], createRequest: ActorRef[Res] => Req)(
      mapResponse: Try[Res] => T)(implicit responseTimeout: Timeout, classTag: ClassTag[Res]): Unit

req -> BackEnd.BackMessages , res -> BackEnd.Response , T -> FrontMessages. Now ask can be written as follows:

 

            ctx.ask[BackEnd.BackMessages,BackEnd.Response](backEnd,
               (backEndRef: ActorRef[BackEnd.Response]) => BackEnd.MakeHello(who,backEndRef) ){
              case Success(backResponse:BackEnd.Response) => WrappedBackEndResonse(backResponse)
              case Failure(err) =>ErrorResponse(err.getLocalizedMessage)
            }

 

This makes it more obvious. That is to say, ask receives BackEnd.Response Converted to the message type WrappedBackEndRespnse processed by FrontEnd, that is, FrontMessages

Another ask mode is implemented outside the actor, as follows:

object AskDemo extends App {
  import akka.actor.typed.scaladsl.AskPattern._
  import scala.concurrent._
  import scala.concurrent.duration._
  import akka.util._
  import scala.util._

  implicit val system: ActorSystem[BackEnd.BackMessages] = ActorSystem(BackEnd(), "front-app")
  // asking someone requires a timeout if the timeout hits without response
  // the ask is failed with a TimeoutException
  implicit val timeout: Timeout = 3.seconds

  val result: Future[BackEnd.Response] =
        system.asInstanceOf[ActorRef[BackEnd.BackMessages]]
            .ask[BackEnd.Response]((ref: ActorRef[BackEnd.Response]) =>
          BackEnd.MakeHello("John", ref))

  // the response callback will be executed on this execution context
  implicit val ec = system.executionContext

  result.onComplete {
    case Success(res)  => res match {
      case BackEnd.HowAreU(msg) =>
        println(msg)
      case BackEnd.Unknown =>
        println("Unable to say hello")
    }
    case Failure(ex)  => 
      println(s"error: ${ex.getMessage}")
  }

  system.terminate()

}

This ask is akka.actor.typed.scaladsl.AskPattern In the bag. Function styles are as follows:

   def ask[Res](replyTo: ActorRef[Res] => Req)(implicit timeout: Timeout, scheduler: Scheduler): Future[Res]

 

Pass in a function actorref to ask[ BackEnd.Response ] => BackEnd.BackMessages , then return to Future[BackEnd.Response ]. In this mode, the receive back compound is outside of ActorContext, and there is no message interception mechanism, so the conversion of message type is not involved.

Another kind of single actor two-way message exchange mode is ask itself. Send a message to yourself in ActorContext and provide the receiving of response message, such as pipeToSelf:

 

object PipeFutureTo {
  trait CustomerDataAccess {
    def update(value: Customer): Future[Done]
  }

  final case class Customer(id: String, version: Long, name: String, address: String)

  object CustomerRepository {

    sealed trait Command

    final case class Update(value: Customer, replyTo: ActorRef[UpdateResult]) extends Command

    sealed trait UpdateResult

    final case class UpdateSuccess(id: String) extends UpdateResult

    final case class UpdateFailure(id: String, reason: String) extends UpdateResult

    private final case class WrappedUpdateResult(result: UpdateResult, replyTo: ActorRef[UpdateResult])
      extends Command

    private val MaxOperationsInProgress = 10

    def apply(dataAccess: CustomerDataAccess): Behavior[Command] = {
      Behaviors.setup[Command] { ctx =>
          implicit val dispatcher =  ctx.system.dispatchers.lookup(DispatcherSelector.fromConfig("my-dispatcher"))
          next(dataAccess, operationsInProgress = 0)
      }
    }

    private def next(dataAccess: CustomerDataAccess, operationsInProgress: Int)(implicit ec: ExecutionContextExecutor): Behavior[Command] = {
      Behaviors.receive { (context, command) =>
        command match {
          case Update(value, replyTo) =>
            if (operationsInProgress == MaxOperationsInProgress) {
              replyTo ! UpdateFailure(value.id, s"Max $MaxOperationsInProgress concurrent operations supported")
              Behaviors.same
            } else {
              val futureResult = dataAccess.update(value)
              context.pipeToSelf(futureResult) {
                // map the Future value to a message, handled by this actor
                case Success(_) => WrappedUpdateResult(UpdateSuccess(value.id), replyTo)
                case Failure(e) => WrappedUpdateResult(UpdateFailure(value.id, e.getMessage), replyTo)
              }
              // increase operationsInProgress counter
              next(dataAccess, operationsInProgress + 1)
            }

          case WrappedUpdateResult(result, replyTo) =>
            // send result to original requestor
            replyTo ! result
            // decrease operationsInProgress counter
            next(dataAccess, operationsInProgress - 1)
        }
      }
    }
  }
}

Tags: Scala

Posted on Fri, 29 May 2020 02:04:37 -0400 by Masca