akka-typed(4) - EventSourcedBehavior in action
前面提到过,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的更多相关文章
- Akka Typed 官方文档之随手记
️ 引言 近两年,一直在折腾用FP与OO共存的编程语言Scala,采取以函数式编程为主的方式,结合TDD和BDD的手段,采用Domain Driven Design的方法学,去构造DDDD应用(Dom ...
- Akka源码分析-Akka Typed
对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...
- Akka Typed系列:协议&行为
引言 2019年11月6号LightBend公司发布了AKKA 2.6版本,带来了类型安全的actor,新的Akka Cluster底层通信设施——Artery,带来了更好的稳定性,使用Jackson ...
- Akka Essentials - 2
Actors Defining an actor class MyActor extends Actor { def receive = { } } In Scala, the receive blo ...
- 怎样在 Akka Persistence 中实现分页查询
在 Akka Persistence 中,数据都缓存在服务内存(状态),后端存储的都是一些持久化的事件日志,没法使用类似 SQL 一样的 DSL 来进行分页查询.利用 Akka Streams 和 A ...
- 一图看懂Actor Typed
引言 朋友看罢我之前整理的<Akka Typed 官方文档之随手记>,一人用了诗歌<长城长>作为回赠,另一人则要求推出简化版本.于是抽空整理了几张思维导图,并且用了一些不太恰当 ...
- Lagom 官方文档之随手记
引言 Lagom是出品Akka的Lightbend公司推出的一个微服务框架,目前最新版本为1.6.2.Lagom一词出自瑞典语,意为"适量". https://www.lagomf ...
- CQRS与Event Sourcing之浅见
引言 DDD是近年软件设计的热门.CQRS与Event Sourcing作为实施DDD的一种选择,也逐步进入人们的视野.围绕这两个主题,软件开发的大咖[Martin Fowler].[Greg You ...
- 网页样式——各种炫酷效果持续更新ing...
1.evanyou效果-彩带的实现,效果如下 注:这个主要用的是Canvas画布实现的,点击背景绘制新的图形,代码如下: /*Html代码:*/ <canvas id=">< ...
随机推荐
- Spring Junit--第一个测试
配置成功后,需要启动测试用例! package com.cml.controller; import javax.annotation.Resource; import org.junit.Test; ...
- Linux设定系统变量和单个用户的环境变量问题
环境为Ubuntu. 设定系统变量意思就是所有用户都可以使用设定的环境变量,而设定用户的单个环境变量意思就是只有设定环境的该用户才可以使用这个环境变量. 这里以java的jdk环境变量JAVA_HOM ...
- 【雕爷学编程】Arduino动手做(56)---8路LED跑马灯模块
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...
- Scrapy 框架 入门教程
Scrapy入门教程 在本篇教程中,我已经安装好Scrapy 本篇教程中将带您完成下列任务: 创建一个Scrapy项目 定义提取的Item 编写爬取网站的 spider 并提取 Item 编写 Ite ...
- css3弹性布局
二.弹性布局(重点******************************************) 1.什么是弹性布局 弹性布局,是一种布局方式. 主要解决的是某个元素中子元素的布局方式 让页面 ...
- 百度编辑器ueditor异步载入的操作方法
http://www.dookay.com/zh-cn/n/928 百度编辑器ueditor异步载入的操作方法 Time:2014-09-30 | View:830 | Source:佚名 返回列表 ...
- poj1780欧拉回路
转载 #include<cstdio> #include<cstring> ; bool vis[N]; char ans[N]; int main() { int n; wh ...
- Shone.Math开源系列1 — 基于.NET 5实现Math<T>泛型数值计算
Shone.Math开源系列1 — 基于.NET 5实现Math<T>泛型数值计算 作者:Shone .NET 5 preview 4已经可用了,从微软Build2020给出的信息看,.N ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录
在开始之前,我们实现一个之前的遗留问题,这个问题是有人在GitHub Issues(https://github.com/Meowv/Blog/issues/8)上提出来的,就是当我们对Swagger ...
- PHP链式操作原理
1)第一种方法 <?php /* *类功能:实现数据库的连贯查询操作 */ class mysql_query{ var $tbl=’user’;//要操作的表名 var $limit=”;// ...