前面提到过,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. 我的linux学习日记day1

    红帽考试 1.RHCSA ------>RHCE 210/300分 2015 RHEL7 2020 RHCE8 8月1改每个月25号 所以我如果想要在6月份考试,就要在 5月25前预约一个考场可 ...

  2. 浅谈PostgreSQL用户权限

    问题 经常在PG群里看到有人在问“为什么我对表赋予了权限:但是还是不能访问表” 解析 若你看懂德哥这篇文章PostgreSQL逻辑结构和权限体系介绍:上面对你就不是困扰你的问题 解决这个问题很简单:在 ...

  3. React:Composition

    在日常的UI构建中,经常会遇到一种情况:组件本身更多是作为一个容器,它所包含的内容可能是动态的.未预先定义的.这时候它的内容取决另一个组件或外部的输入.比如弹层. props.children: Re ...

  4. 8.1Go并发

    第八章 Go并发 Go语言区别于其他语言的一大特点就是出色的并发性能,最重要的一个特性那就是go关键字. 并发场景: UI小姐姐一边开着PS软件,一边微信疯狂的和产品经理打字交流,后台还听着网易云音乐 ...

  5. MySQL(6)— 事务

    六.事务 ACID: 原子性.一致性.隔离性和持久性 原子性(atomicity) :一组事务,不能再细分了,其中的sql, 要么全部提交成功,要么全部失败回滚,不能只提交其中的一部分操作. 一致性( ...

  6. POJ1930

    题目链接:http://poj.org/problem?id=1930 题目大意: 给一个无限循环小数(循环节不知),要求你输出当该小数所化成的最简分数分母最小时所对应的最简分数. AC思路: 完全没 ...

  7. 十、理解JavaBean

    1. 理解Bean 1.JavaBean本身就是一个类,属于Java的面向对象编程. 2.在JSP中如果要应用JSP提供的Javabean的标签来操作简单类的话,则此类必须满足如下的开发要求: (1) ...

  8. 还不会K8S吗?先从kubeadm开始吧

    目录 1. 准备工作 1.1 机器准备 1.2 系统配置 1.2.1 主机名及域名解析 1.2.2 免密登录 1.2.3 配置yum源 1.2.4 安装必要依赖包 1.2.5 关闭防火墙.SELinu ...

  9. PAT 1032 Sharing (25分) 从自信到自闭

    题目 To store English words, one method is to use linked lists and store a word letter by letter. To s ...

  10. CF652E Pursuit For Aritifacts

    题目传送门 这是一道很好的练习强联通的题目. 首先,从题中可以看到,题目的要求就是要我们求出从起点到终点是否可以经过flag = 1 的边. 由于是无向图,且要求很多,直接暴力dfs会很凌乱. 那么, ...