akka系統是一個分散式的訊息驅動系統。akka應用由一群負責不同運算工作的actor組成,每個actor都是被動等待外界的某種訊息來驅動自己的作業。所以,通俗點描述:akka應用就是一群actor相互之間傳送訊息的系統,每個actor接收到訊息後開始自己負責的工作。對於akka-typed來說,typed-actor只能接收指定型別的訊息,所以actor之間的訊息交流需要按照訊息型別來進行,即需要協議來規範訊息交流機制。想想看,如果使用者需要一個actor做某件事,他必須用這個actor明白的訊息型別來傳送訊息,這就是一種交流協議。
所謂訊息交流方式包括單向和雙向兩類。如果涉及兩個actor之間的訊息交換,訊息傳送方式可以是單向和雙向的。但如果是從外界向一個actor傳送訊息,那麼肯定只能是單向的傳送方式了,因為訊息傳送兩端只有一端是actor。
典型的單向訊息傳送fire-and-forget如下:
import akka.actor.typed._
import scaladsl._
object Printer {
case class PrintMe(message: String)
// 只接收PrintMe型別message
def apply(): Behavior[PrintMe] =
Behaviors.receive {
case (context, PrintMe(message)) =>
context.log.info(message)
Behaviors.same
}
}
object FireAndGo extends App {
// system就是一個root-actor
val system: ActorRef[Printer.PrintMe] = ActorSystem(Printer(), "fire-and-forget-sample")
val printer: ActorRef[Printer.PrintMe] = system
// 單向訊息傳送,printMe型別的訊息
printer ! Printer.PrintMe("hello")
printer ! Printer.PrintMe("world!")
system.asInstanceOf[ActorSystem[Printer.PrintMe]].terminate()
}
當然,在現實中通常我們要求actor去進行某些運算然後返回運算結果。這就涉及到actor之間雙向資訊交換了。第一種情況:兩個actor之間的訊息是任意無序的,這是一種典型的無順序request-response模式。就是說一個response不一定是按照request的接收順序返回的,只是它們之間能夠交流而已。不過,在akka-typed中這種模式最基本的要求就是傳送的訊息型別必須符合接收方actor的型別。
好了,我們先對這個模式做個示範。所有actor的定義可以先從它的訊息型別開始。對每個參加雙向交流的actor來說,可以從request和response兩種訊息來反映它的功能:
object FrontEnd {
sealed trait FrontMessages
case class SayHi(who: String) extends FrontMessages
}
object BackEnd {
//先從這個actor的回應訊息開始
sealed trait Response
case class HowAreU(msg: String) extends Response
case object Unknown extends Response
//可接收訊息型別
sealed trait BackMessages
//這個replyTo應該是一個能處理Reponse型別訊息的actor
case class MakeHello(who: String, replyTo: ActorRef[Response]) extends BackMessages
}
這個FrontEnd接收SayHi訊息後開始工作,不過目前還沒有定義返回的訊息型別。BackEnd接到MakeHello型別訊息後返回response型別訊息。從這個角度來講,返回的對方actor必須能夠處理Response型別的訊息。
我們試試實現這個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需要一個replyTo,應該是什麼呢?不過它一定是可以處理Response型別訊息的actor。但我們知道這個replyTo就是FrontEnd,不過FrontEnd只能處理FrontMessages型別訊息,應該怎麼辦呢?可不可以把replyTo直接寫成FrontEnd呢?雖然可以這麼做,但這個MakeHello訊息就只能跟FrontEnd綁死了。如果其它的actor也需要用到這個MakeHello的話就需要另外定義一個了。所以,最好的解決方案就是用某種型別轉換方式來實現。如下:
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將BackEnd.Response轉換成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
}
}
}
}
}
}
首先,我們用ctx.mesageAdapter產生了ActorRef[BackEnd.Response],正是我們需要提供給MakeHello訊息的replyTo。看看這個messageAdapter函式:
def messageAdapter[U: ClassTag](f: U => T): ActorRef[U]
如果我們進行型別替換U -> BackEnd.Response, T -> FrontMessage 那麼:
val backEndRef: ActorRef[BackEnd.Response] =
ctx.messageAdapter((response: BackEnd.Response) => WrappedBackEndResonse(response))
實際上這個messageAdapter函式在本地ActorContext範圍內登記了一個從BackEnd.Response型別到FrontMessages的轉換。把接收到的BackEnd.Response立即轉換成WrappedBackEndResponse(response)。
還有一種兩個actor之間的雙向交流模式是 1:1 request-response,即一對一模式。一對一的意思是傳送方傳送訊息後等待回應訊息。這就意味著收信方需要在完成運算任務後立即向發信方傳送回應,否則造成發信方的超時異常。無法避免的是,這種模式依然會涉及訊息型別的轉換,如下:
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需要超時上限
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
}
}
}
}
}
似乎型別轉換是在ask裡實現的,看看這個函式:
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。現在ask可以寫成下面這樣:
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)
}
這樣看起來更明白點,也就是說ask把接收的BackEnd.Response轉換成了FrontEnd處理的訊息型別WrappedBackEndRespnse,也就是FrontMessages
還有一種ask模式是在actor之外進行的,如下:
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()
}
這個ask是在akka.actor.typed.scaladsl.AskPattern包裡。函式款式如下:
def ask[Res](replyTo: ActorRef[Res] => Req)(implicit timeout: Timeout, scheduler: Scheduler): Future[Res]
向ask傳入一個函式ActorRef[BackEnd.Response] => BackEnd.BackMessages,然後返回Future[BackEnd.Response]。這個模式中接收回複方是在ActorContext之外,不存在訊息截獲機制,所以不涉及訊息型別的轉換。
另一種單actor雙向訊息交換模式,即自己ask自己。在ActorContext內向自己傳送訊息並提供回應訊息的接收,如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)
}
}
}
}
}