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 responsi...

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: $") } 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) } } } } }

29 May 2020, 02:04 | Views: 2722

Add new comment

For adding a comment, please log in
or create account

0 comments