前面提到过,akka-typed中较重要的改变是加入了EventSourcedBehavior。也就是说增加了一种专门负责EventSource模式的actor, 最终和其它种类的actor一道可以完美实现CQRS。新的actor,我还是把它称为persistentActor,还是一种能维护和维持运行状态的actor。即,actor内部状态可以存放在数据库里,然后通过一组功能函数来提供对状态的处理转变,即持续化处理persistence。当然作为一种具备EventSourcedBehavior的actor, 普遍应有的actor属性、方法、消息处理协议、监管什么的都还必须存在。在这篇讨论里我们就通过案例和源码来说明一下EventSourcedBehavior是如何维护内部状态及作为一种actor又应该怎么去使用它。

我们把上一篇讨论里购物车的例子拿来用,再增加一些消息回复response机制,主要是汇报购物车状态:

object ItemInfo {
case class Item(name: String, price: Double)
} object MyCart {
import ItemInfo._ sealed trait Command
sealed trait Event extends CborSerializable
sealed trait Response //commands
case class AddItem(item: Item) extends Command
case object PayCart extends Command
case class CountItems(replyTo: ActorRef[Response]) extends Command //event
case class ItemAdded(item: Item) extends Event
case object CartPaid extends Event //state
case class CartLoad(load: List[Item] = Nil) //response
case class PickedItems(items: List[Item]) extends Response
case object CartEmpty extends Response val commandHandler: (CartLoad, Command) => Effect[Event,CartLoad] = { (state, cmd) =>
cmd match {
case AddItem(item) =>
Effect.persist(ItemAdded(item))
case PayCart =>
Effect.persist(CartPaid)
case CountItems(replyTo) =>
Effect.none.thenRun { cart =>
cart.load match {
case Nil =>
replyTo ! CartEmpty
case listOfItems =>
replyTo ! PickedItems(listOfItems)
}
}
}
} val eventHandler: (CartLoad,Event) => CartLoad = { (state,evt) =>
evt match {
case ItemAdded(item) =>
state.copy(load = item :: state.load)
case CartPaid =>
state.copy(load = Nil)
}
} def apply(): Behavior[Command] = EventSourcedBehavior[Command,Event,CartLoad](
persistenceId = PersistenceId("",""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
) } object Shopper { import ItemInfo._ sealed trait Command extends CborSerializable case class GetItem(item: Item) extends Command
case object Settle extends Command
case object GetCount extends Command case class WrappedResponse(res: MyCart.Response) extends Command def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
val shoppingCart = ctx.spawn(MyCart(), "shopping-cart")
val cartRef: ActorRef[MyCart.Response] = ctx.messageAdapter(WrappedResponse)
Behaviors.receiveMessage { msg =>
msg match {
case GetItem(item) =>
shoppingCart ! MyCart.AddItem(item)
case Settle =>
shoppingCart ! MyCart.PayCart
case GetCount =>
shoppingCart ! MyCart.CountItems(cartRef)
case WrappedResponse(res) => res match {
case MyCart.PickedItems(items) =>
ctx.log.info("**************Current Items in Cart: {}*************", items)
case MyCart.CartEmpty =>
ctx.log.info("**************shopping cart is empty!***************")
}
}
Behaviors.same
}
} } object ShoppingCart extends App {
import ItemInfo._
val shopper = ActorSystem(Shopper(),"shopper")
shopper ! Shopper.GetItem(Item("banana",11.20))
shopper ! Shopper.GetItem(Item("watermelon",4.70))
shopper ! Shopper.GetCount
shopper ! Shopper.Settle
shopper ! Shopper.GetCount
scala.io.StdIn.readLine() shopper.terminate() }

实际上EventSourcedBehavior里还嵌入了回复机制,完成一项Command处理后必须回复指令方,否则程序无法通过编译。如下:

private def withdraw(acc: OpenedAccount, cmd: Withdraw): ReplyEffect[Event, Account] = {
if (acc.canWithdraw(cmd.amount))
Effect.persist(Withdrawn(cmd.amount)).thenReply(cmd.replyTo)(_ => Confirmed)
else
Effect.reply(cmd.replyTo)(Rejected(s"Insufficient balance ${acc.balance} to be able to withdraw ${cmd.amount}"))
}

不过这个回复机制是一种副作用。即,串连在Effect产生之后立即实施。这个动作是在eventHandler之前。在这个时段无法回复最新的状态。

说到side-effect, 如Effect.persist().thenRun(produceSideEffect): 当成功持续化event后可以安心进行一些其它的操作。例如,当影响库存数的event被persist后可以马上从账上扣减库存。

在上面这个ShoppingCart例子里我们没有发现状态转换代码如Behaviors.same。这只能是EventSourcedBehavior属于更高层次的Behavior,状态转换已经嵌入在eventHandler里了,还记着这个函数的款式吧  (State,Event) => State, 这个State就是状态了。

Events persist在journal里,如果persist操作中journal出现异常,EventSourcedBehavior自备了安全监管策略,如下:

  def apply(): Behavior[Command] = EventSourcedBehavior[Command,Event,CartLoad](
persistenceId = PersistenceId("",""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds))

值得注意的是:这个策略只适用于onPersistFailure(),从外部用Behaviors.supervisor()包嵌是无法实现处理PersistFailure效果的。但整个actor还是需要一种Backoff策略,因为在EventSourcedBehavior内部commandHandler,eventHandler里可能也会涉及一些数据库操作。在操作失败后需要某种Backoff重启策略。那么我们可以为actor增加监控策略如下:

  def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

现在这个MyCart可以说已经是个安全、强韧性的actor了。

既然是一种persistentActor,那么持久化的管理应该也算是核心功能了。EventSourcedBehavior通过接收信号提供了对持久化过程监控功能,如:

 def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

EventSourcedBehavior.receiveSignal是个偏函数:

  def receiveSignal(signalHandler: PartialFunction[(State, Signal), Unit]): EventSourcedBehavior[Command, Event, State]

下面是一个EventSourcedBehavior Signal 清单:

sealed trait EventSourcedSignal extends Signal

@DoNotInherit sealed abstract class RecoveryCompleted extends EventSourcedSignal
case object RecoveryCompleted extends RecoveryCompleted {
def instance: RecoveryCompleted = this
} final case class RecoveryFailed(failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
} final case class SnapshotCompleted(metadata: SnapshotMetadata) extends EventSourcedSignal {
def getSnapshotMetadata(): SnapshotMetadata = metadata
} final case class SnapshotFailed(metadata: SnapshotMetadata, failure: Throwable) extends EventSourcedSignal { def getFailure(): Throwable = failure
def getSnapshotMetadata(): SnapshotMetadata = metadata
} object SnapshotMetadata { /**
* @param persistenceId id of persistent actor from which the snapshot was taken.
* @param sequenceNr sequence number at which the snapshot was taken.
* @param timestamp time at which the snapshot was saved, defaults to 0 when unknown.
* in milliseconds from the epoch of 1970-01-01T00:00:00Z.
*/
def apply(persistenceId: String, sequenceNr: Long, timestamp: Long): SnapshotMetadata =
new SnapshotMetadata(persistenceId, sequenceNr, timestamp)
} /**
* Snapshot metadata.
*
* @param persistenceId id of persistent actor from which the snapshot was taken.
* @param sequenceNr sequence number at which the snapshot was taken.
* @param timestamp time at which the snapshot was saved, defaults to 0 when unknown.
* in milliseconds from the epoch of 1970-01-01T00:00:00Z.
*/
final class SnapshotMetadata(val persistenceId: String, val sequenceNr: Long, val timestamp: Long) {
override def toString: String =
s"SnapshotMetadata($persistenceId,$sequenceNr,$timestamp)"
} final case class DeleteSnapshotsCompleted(target: DeletionTarget) extends EventSourcedSignal {
def getTarget(): DeletionTarget = target
} final case class DeleteSnapshotsFailed(target: DeletionTarget, failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
def getTarget(): DeletionTarget = target
} final case class DeleteEventsCompleted(toSequenceNr: Long) extends EventSourcedSignal {
def getToSequenceNr(): Long = toSequenceNr
} final case class DeleteEventsFailed(toSequenceNr: Long, failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
def getToSequenceNr(): Long = toSequenceNr
}

当然,EventSourcedBehavior之所以能具备自我修复能力其中一项是因为它有对持久化的事件重演机制。如果每次启动都需要对所有历史事件进行重演的话会很不现实。必须用snapshot来浓缩历史事件:

  def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}.snapshotWhen {
case (state,CartPaid,seqnum) =>
ctx.log.info("*****************snapshot taken at: {} with state: {}",seqnum,state)
true
case (state,event,seqnum) => false
}.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = , keepNSnapshots = ))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

下面是本次示范的源码:

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.1"
scalacOptions in Compile ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlog-reflective-calls", "-Xlint")
javacOptions in Compile ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") val AkkaVersion = "2.6.5"
val AkkaPersistenceCassandraVersion = "1.0.0" libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-cluster-sharding-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-query" % AkkaVersion,
"com.typesafe.akka" %% "akka-serialization-jackson" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-cassandra" % AkkaPersistenceCassandraVersion,
"com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
"ch.qos.logback" % "logback-classic" % "1.2.3"
)

application.conf

akka.actor.allow-java-serialization = on
akka {
loglevel = DEBUG
actor {
serialization-bindings {
"com.learn.akka.CborSerializable" = jackson-cbor
}
}
# use Cassandra to store both snapshots and the events of the persistent actors
persistence {
journal.plugin = "akka.persistence.cassandra.journal"
snapshot-store.plugin = "akka.persistence.cassandra.snapshot"
} }
akka.persistence.cassandra {
# don't use autocreate in production
journal.keyspace = "poc"
journal.keyspace-autocreate = on
journal.tables-autocreate = on
snapshot.keyspace = "poc_snapshot"
snapshot.keyspace-autocreate = on
snapshot.tables-autocreate = on
} datastax-java-driver {
basic.contact-points = ["192.168.11.189:9042"]
basic.load-balancing-policy.local-datacenter = "datacenter1"
}

ShoppingCart.scala

package com.learn.akka

import akka.actor.typed._
import akka.persistence.typed._
import akka.actor.typed.scaladsl.Behaviors
import akka.persistence.typed.scaladsl._
import scala.concurrent.duration._ object ItemInfo {
case class Item(name: String, price: Double)
} object MyCart {
import ItemInfo._ sealed trait Command
sealed trait Event extends CborSerializable
sealed trait Response //commands
case class AddItem(item: Item) extends Command
case object PayCart extends Command
case class CountItems(replyTo: ActorRef[Response]) extends Command //event
case class ItemAdded(item: Item) extends Event
case object CartPaid extends Event //state
case class CartLoad(load: List[Item] = Nil) //response
case class PickedItems(items: List[Item]) extends Response
case object CartEmpty extends Response val commandHandler: (CartLoad, Command) => Effect[Event,CartLoad] = { (state, cmd) =>
cmd match {
case AddItem(item) =>
Effect.persist(ItemAdded(item))
case PayCart =>
Effect.persist(CartPaid)
case CountItems(replyTo) =>
Effect.none.thenRun { cart =>
cart.load match {
case Nil =>
replyTo ! CartEmpty
case listOfItems =>
replyTo ! PickedItems(listOfItems)
}
}
}
} val eventHandler: (CartLoad,Event) => CartLoad = { (state,evt) =>
evt match {
case ItemAdded(item) =>
state.copy(load = item :: state.load)
case CartPaid =>
state.copy(load = Nil)
}
} def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}.snapshotWhen {
case (state,CartPaid,seqnum) =>
ctx.log.info("*****************snapshot taken at: {} with state: {}",seqnum,state)
true
case (state,event,seqnum) => false
}.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = , keepNSnapshots = ))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)
} object Shopper { import ItemInfo._ sealed trait Command extends CborSerializable case class GetItem(item: Item) extends Command
case object Settle extends Command
case object GetCount extends Command case class WrappedResponse(res: MyCart.Response) extends Command def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
val shoppingCart = ctx.spawn(MyCart(), "shopping-cart")
val cartRef: ActorRef[MyCart.Response] = ctx.messageAdapter(WrappedResponse)
Behaviors.receiveMessage { msg =>
msg match {
case GetItem(item) =>
shoppingCart ! MyCart.AddItem(item)
case Settle =>
shoppingCart ! MyCart.PayCart
case GetCount =>
shoppingCart ! MyCart.CountItems(cartRef)
case WrappedResponse(res) => res match {
case MyCart.PickedItems(items) =>
ctx.log.info("**************Current Items in Cart: {}*************", items)
case MyCart.CartEmpty =>
ctx.log.info("**************shopping cart is empty!***************")
}
}
Behaviors.same
}
} } object ShoppingCart extends App {
import ItemInfo._
val shopper = ActorSystem(Shopper(),"shopper")
shopper ! Shopper.GetItem(Item("banana",11.20))
shopper ! Shopper.GetItem(Item("watermelon",4.70))
shopper ! Shopper.GetCount
shopper ! Shopper.Settle
shopper ! Shopper.GetCount
scala.io.StdIn.readLine() shopper.terminate() }

akka-typed(4) - EventSourcedBehavior in action的更多相关文章

  1. Akka Typed 官方文档之随手记

    ️ 引言 近两年,一直在折腾用FP与OO共存的编程语言Scala,采取以函数式编程为主的方式,结合TDD和BDD的手段,采用Domain Driven Design的方法学,去构造DDDD应用(Dom ...

  2. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  3. Akka Typed系列:协议&行为

    引言 2019年11月6号LightBend公司发布了AKKA 2.6版本,带来了类型安全的actor,新的Akka Cluster底层通信设施——Artery,带来了更好的稳定性,使用Jackson ...

  4. Akka Essentials - 2

    Actors Defining an actor class MyActor extends Actor { def receive = { } } In Scala, the receive blo ...

  5. 怎样在 Akka Persistence 中实现分页查询

    在 Akka Persistence 中,数据都缓存在服务内存(状态),后端存储的都是一些持久化的事件日志,没法使用类似 SQL 一样的 DSL 来进行分页查询.利用 Akka Streams 和 A ...

  6. 一图看懂Actor Typed

    引言 朋友看罢我之前整理的<Akka Typed 官方文档之随手记>,一人用了诗歌<长城长>作为回赠,另一人则要求推出简化版本.于是抽空整理了几张思维导图,并且用了一些不太恰当 ...

  7. Lagom 官方文档之随手记

    引言 Lagom是出品Akka的Lightbend公司推出的一个微服务框架,目前最新版本为1.6.2.Lagom一词出自瑞典语,意为"适量". https://www.lagomf ...

  8. CQRS与Event Sourcing之浅见

    引言 DDD是近年软件设计的热门.CQRS与Event Sourcing作为实施DDD的一种选择,也逐步进入人们的视野.围绕这两个主题,软件开发的大咖[Martin Fowler].[Greg You ...

  9. 网页样式——各种炫酷效果持续更新ing...

    1.evanyou效果-彩带的实现,效果如下 注:这个主要用的是Canvas画布实现的,点击背景绘制新的图形,代码如下: /*Html代码:*/ <canvas id=">< ...

随机推荐

  1. TreeSet的两种实现方法:Comparable和Comparator(Java比较器)

    Comparable与Comparator实际上是TreeSet集合的两种实现方式,用来实现对象的排序.下边介绍一下两种比较器的使用方法和区别. Comparable称为元素的自然顺序,或者叫做默认顺 ...

  2. gRPC负载均衡(客户端负载均衡)

    前言 上篇介绍了如何使用etcd实现服务发现,本篇将基于etcd的服务发现前提下,介绍如何实现gRPC客户端负载均衡. gRPC负载均衡 gRPC官方文档提供了关于gRPC负载均衡方案Load Bal ...

  3. Unity3D的UGUI布局锚点自动绑定关系

    [MenuItem("CONTEXT/RectTransform/Auto")] public static void AutoRectAnior() { Debug.Log(&q ...

  4. Linux系统rabbitmq安装

    rabbitmq消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递.消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管 ...

  5. 51Nod - 1255

    也是第十一届校赛的C题,不过他把1e5改成了1e7. 一开始就想到用贪心做.思路是这样的:开一个字符数组ans保存答案.然后从头到尾遍历题目给出的字符串S,如果ans数组中还没有这个字母,那么就把字母 ...

  6. Istio Sidecar注入原理

    概念 简单来说,Sidecar 注入会将额外容器的配置添加到 Pod 模板中.这里特指将Envoy容器注应用所在Pod中. Istio 服务网格目前所需的容器有: istio-init 用于设置 ip ...

  7. Kubernetes学习笔记(六):使用ConfigMap和Secret配置应用程序

    概述 本文的核心是:如何处理应用程序的数据配置. 配置应用程序可以使用以下几种途径: 向容器传递命令行参数 为每个容器配置环境变量 通过特殊的卷将配置文件挂载到容器中 向容器传递命令行参数 在Kube ...

  8. SpringBoot外部化配置使用Plus版

    本文如有任何纰漏.错误,请不吝指正! PS: 之前写过一篇关于SpringBoo中使用配置文件的一些姿势,不过嘛,有句话(我)说的好:曾见小桥流水,未睹观音坐莲!所以再写一篇增强版,以便记录. 序言 ...

  9. Python面向对象的特征跟私有属性

     面向对象的三大特征 继成.封装.多态: 继承:一个类可以继承一个或多个类,被继承的类叫做父类,继承的类叫做子类 封装:将数据和方法放在了一个类中就构成了封装 多态:指的一类事物有多重形态,一个抽象类 ...

  10. java第十三周课后作业 0529

    1.把多个企鹅的信息添加到集合中查看企鹅的数量及所有企鹅的信息删除集合中部分企鹅的元素判断集合中是否包含指定企鹅 package homework; import java.util.ArrayLis ...