akka-typed(3) - PersistentActor has EventSourcedBehavior
akka-typed中已经没有PersistentActor了。取而代之的是带有EventSourcedBehavior的actor,也就是一种专门支持EventSource模式的actor。EventSource的原理和作用在之前的博客里已经有了比较详细的介绍,这里就不再重复了。本篇直接从EventsourcedBehavior actor的具体应用开始介绍。支持EventSource应用的基本数据类型包括 指令Command, 事件Event,状态State。EventSourcing其实就是一个有限状态机fsm finite-state-machine,执行Command,产生Event,改变State,终而复始。下面是一个简单的EventSource类型定义:
trait CborSerializable {}
object Cart {
case class Item(name: String, price: Double) sealed trait Command extends CborSerializable
sealed trait Event //commands
case class AddItem(item: Item) extends Command
case object PayCart extends Command //event
case class ItemAdded(item: Item) extends Event
case object CartPaid extends Event //state
case class CartLoad(load: Set[Item] = Set.empty) val commandHandler: (CartLoad, Command) => Effect[Event,CartLoad] = { (state, cmd) =>
cmd match {
case AddItem(item) =>
Effect.persist(ItemAdded(item))
case PayCart =>
Effect.persist(CartPaid)
}
} val eventHandler: (CartLoad,Event) => CartLoad = { (state,evt) =>
evt match {
case ItemAdded(item) =>
val sts = state.copy(load = state.load+item)
println(s"current cart loading: ${sts}")
sts
case CartPaid =>
val sts = state.copy(load = Set.empty)
println(s"current cart loading: ${sts.load}")
sts
}
} def apply(): Behavior[Command] = EventSourcedBehavior[Command,Event,CartLoad](
persistenceId = PersistenceId("",""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
) } object EventSource extends App {
import Cart._
val cart = ActorSystem(Cart(),"shopping-cart")
cart ! Cart.AddItem(Item("banana",11.20))
cart ! Cart.AddItem(Item("watermelon",4.70)) scala.io.StdIn.readLine() cart.terminate() }
首先要搞清楚几件事:EvenSourcedBehavior定义了一个actor。从Behavior[Command]这个结果类型来看,这个actor可以接收并处理Command类型的消息。既然是个actor那么应该具备了receiveMessage,receiveSignal这两项基本能力,但我们又不用自己来定义这些功能。怎么回事呢?看看EventSourcedBehavior的源代码吧:
object EventSourcedBehavior {
...
def apply[Command, Event, State](
persistenceId: PersistenceId,
emptyState: State,
commandHandler: (State, Command) => Effect[Event, State],
eventHandler: (State, Event) => State): EventSourcedBehavior[Command, Event, State] = {
val loggerClass = LoggerClass.detectLoggerClassFromStack(classOf[EventSourcedBehavior[_, _, _]], logPrefixSkipList)
EventSourcedBehaviorImpl(persistenceId, emptyState, commandHandler, eventHandler, loggerClass)
}
...
}
这个EventSourcedBehavior就是某种Behavior。它的所有特殊功能看来应该是在EventSourcedBehaviorsImpl里实现的:
private[akka] final case class EventSourcedBehaviorImpl[Command, Event, State](
persistenceId: PersistenceId,
emptyState: State,
commandHandler: EventSourcedBehavior.CommandHandler[Command, Event, State],
eventHandler: EventSourcedBehavior.EventHandler[State, Event],
loggerClass: Class[_],
...
) extends EventSourcedBehavior[Command, Event, State] {
...
Behaviors
.supervise {
Behaviors.setup[Command] { _ =>
val eventSourcedSetup = new BehaviorSetup(
ctx.asInstanceOf[ActorContext[InternalProtocol]],
persistenceId,
emptyState,
commandHandler,
eventHandler,
WriterIdentity.newIdentity(),
actualSignalHandler,
tagger,
eventAdapter,
snapshotAdapter,
snapshotWhen,
recovery,
retention,
holdingRecoveryPermit = false,
settings = settings,
stashState = stashState) // needs to accept Any since we also can get messages from the journal
// not part of the user facing Command protocol
def interceptor: BehaviorInterceptor[Any, InternalProtocol] = new BehaviorInterceptor[Any, InternalProtocol] { import BehaviorInterceptor._
override def aroundReceive(
ctx: typed.TypedActorContext[Any],
msg: Any,
target: ReceiveTarget[InternalProtocol]): Behavior[InternalProtocol] = {
val innerMsg = msg match {
case res: JournalProtocol.Response => InternalProtocol.JournalResponse(res)
case res: SnapshotProtocol.Response => InternalProtocol.SnapshotterResponse(res)
case RecoveryPermitter.RecoveryPermitGranted => InternalProtocol.RecoveryPermitGranted
case internal: InternalProtocol => internal // such as RecoveryTickEvent
case cmd: Command @unchecked => InternalProtocol.IncomingCommand(cmd)
}
target(ctx, innerMsg)
} override def aroundSignal(
ctx: typed.TypedActorContext[Any],
signal: Signal,
target: SignalTarget[InternalProtocol]): Behavior[InternalProtocol] = {
if (signal == PostStop) {
eventSourcedSetup.cancelRecoveryTimer()
// clear stash to be GC friendly
stashState.clearStashBuffers()
}
target(ctx, signal)
} override def toString: String = "EventSourcedBehaviorInterceptor"
} Behaviors.intercept(() => interceptor)(RequestingRecoveryPermit(eventSourcedSetup)).narrow
} }
.onFailure[JournalFailureException](supervisionStrategy)
}
EventSourcedBehaviorImpl还是一种Behavior[Command],它又是通过一个BehaviorInterceptor实现的。BehaviorInterceptor.aroundReceive和BehaviorInterceptor.aroundSignal可以代替receiveMessage和receiveSignal的工作,这点从这两个函数的结果类型可以得到一些验证:
/* @tparam Outer The outer message type – the type of messages the intercepting behavior will accept
* @tparam Inner The inner message type - the type of message the wrapped behavior accepts
*
* @see [[BehaviorSignalInterceptor]]
*/
abstract class BehaviorInterceptor[Outer, Inner](val interceptMessageClass: Class[Outer]) {
import BehaviorInterceptor._
... /**
* Intercept a message sent to the running actor. Pass the message on to the next behavior
* in the stack by passing it to `target.apply`, return `Behaviors.same` without invoking `target`
* to filter out the message.
*
* @return The behavior for next message or signal
*/
def aroundReceive(ctx: TypedActorContext[Outer], msg: Outer, target: ReceiveTarget[Inner]): Behavior[Inner] /**
* Override to intercept a signal sent to the running actor. Pass the signal on to the next behavior
* in the stack by passing it to `target.apply`.
*
* @return The behavior for next message or signal
*
* @see [[BehaviorSignalInterceptor]]
*/
def aroundSignal(ctx: TypedActorContext[Outer], signal: Signal, target: SignalTarget[Inner]): Behavior[Inner] ...
}
另外,对于EventSourcedBehavior来说,收到Command, 处理Command方式应该是通过外部提供的这个commandHandler来实现才是最值得注意的:
final class HandlingCommands(state: RunningState[S])
extends AbstractBehavior[InternalProtocol](setup.context)
with WithSeqNrAccessible { def onMessage(msg: InternalProtocol): Behavior[InternalProtocol] = msg match {
case IncomingCommand(c: C @unchecked) => onCommand(state, c)
case JournalResponse(r) => onDeleteEventsJournalResponse(r, state.state)
case SnapshotterResponse(r) => onDeleteSnapshotResponse(r, state.state)
case get: GetState[S @unchecked] => onGetState(get)
case _ => Behaviors.unhandled
} override def onSignal: PartialFunction[Signal, Behavior[InternalProtocol]] = {
case PoisonPill =>
if (isInternalStashEmpty && !isUnstashAllInProgress) Behaviors.stopped
else new HandlingCommands(state.copy(receivedPoisonPill = true))
case signal =>
if (setup.onSignal(state.state, signal, catchAndLog = false)) this
else Behaviors.unhandled
} def onCommand(state: RunningState[S], cmd: C): Behavior[InternalProtocol] = {
val effect = setup.commandHandler(state.state, cmd)
applyEffects(cmd, state, effect.asInstanceOf[EffectImpl[E, S]]) // TODO can we avoid the cast?
} ...
}
上面这段代码已经足够说明了。根据commandHandler和eventHandler的函数类型可以得出EventSourcedBehavior处理流程 (State, Command) => (State, Event) => new State, 最终输出new State:
object EventSourcedBehavior {
type CommandHandler[Command, Event, State] = (State, Command) => Effect[Event, State]
type EventHandler[State, Event] = (State, Event) => State
...
}
commandHandler返回Effect[Event,State]类型结果,也就是说处理Command过程就是产生Event过程,下面是Effect的各种选项:
object Effect { /**
* Persist a single event
*
* Side effects can be chained with `thenRun`
*/
def persist[Event, State](event: Event): EffectBuilder[Event, State] = Persist(event) /**
* Persist multiple events
*
* Side effects can be chained with `thenRun`
*/
def persist[Event, A <: Event, B <: Event, State](evt1: A, evt2: B, events: Event*): EffectBuilder[Event, State] =
persist(evt1 :: evt2 :: events.toList) /**
* Persist multiple events
*
* Side effects can be chained with `thenRun`
*/
def persist[Event, State](events: im.Seq[Event]): EffectBuilder[Event, State] =
PersistAll(events) /**
* Do not persist anything
*
* Side effects can be chained with `thenRun`
*/
def none[Event, State]: EffectBuilder[Event, State] = PersistNothing.asInstanceOf[EffectBuilder[Event, State]] /**
* This command is not handled, but it is not an error that it isn't.
*
* Side effects can be chained with `thenRun`
*/
def unhandled[Event, State]: EffectBuilder[Event, State] = Unhandled.asInstanceOf[EffectBuilder[Event, State]] /**
* Stop this persistent actor
* Side effects can be chained with `thenRun`
*/
def stop[Event, State](): EffectBuilder[Event, State] =
none.thenStop() /**
* Stash the current command. Can be unstashed later with [[Effect.unstashAll]].
*
* Note that the stashed commands are kept in an in-memory buffer, so in case of a crash they will not be
* processed. They will also be discarded if the actor is restarted (or stopped) due to that an exception was
* thrown from processing a command or side effect after persisting. The stash buffer is preserved for persist
* failures if a backoff supervisor strategy is defined with [[EventSourcedBehavior.onPersistFailure]].
*
* Side effects can be chained with `thenRun`
*/
def stash[Event, State](): ReplyEffect[Event, State] =
Stash.asInstanceOf[EffectBuilder[Event, State]].thenNoReply() /**
* Unstash the commands that were stashed with [[Effect.stash]].
*
* It's allowed to stash messages while unstashing. Those newly added
* commands will not be processed by this `unstashAll` effect and have to be unstashed
* by another `unstashAll`.
*
* @see [[EffectBuilder.thenUnstashAll]]
*/
def unstashAll[Event, State](): Effect[Event, State] =
CompositeEffect(none.asInstanceOf[EffectBuilder[Event, State]], SideEffect.unstashAll[State]()) /**
* Send a reply message to the command. The type of the
* reply message must conform to the type specified by the passed replyTo `ActorRef`.
*
* This has the same semantics as `cmd.replyTo.tell`.
*
* It is provided as a convenience (reducing boilerplate) and a way to enforce that replies are not forgotten
* when the `EventSourcedBehavior` is created with [[EventSourcedBehavior.withEnforcedReplies]]. When
* `withEnforcedReplies` is used there will be compilation errors if the returned effect isn't a [[ReplyEffect]].
* The reply message will be sent also if `withEnforcedReplies` isn't used, but then the compiler will not help
* finding mistakes.
*/
def reply[ReplyMessage, Event, State](replyTo: ActorRef[ReplyMessage])(
replyWithMessage: ReplyMessage): ReplyEffect[Event, State] =
none[Event, State].thenReply[ReplyMessage](replyTo)(_ => replyWithMessage) /**
* When [[EventSourcedBehavior.withEnforcedReplies]] is used there will be compilation errors if the returned effect
* isn't a [[ReplyEffect]]. This `noReply` can be used as a conscious decision that a reply shouldn't be
* sent for a specific command or the reply will be sent later.
*/
def noReply[Event, State]: ReplyEffect[Event, State] =
none.thenNoReply() }
接着用handleEvent来根据产生的Event更新State,如下:
@tailrec def applyEffects(
msg: Any,
state: RunningState[S],
effect: Effect[E, S],
sideEffects: immutable.Seq[SideEffect[S]] = Nil): Behavior[InternalProtocol] = {
if (setup.log.isDebugEnabled && !effect.isInstanceOf[CompositeEffect[_, _]])
setup.log.debugN(
s"Handled command [{}], resulting effect: [{}], side effects: [{}]",
msg.getClass.getName,
effect,
sideEffects.size) effect match {
case CompositeEffect(eff, currentSideEffects) =>
// unwrap and accumulate effects
applyEffects(msg, state, eff, currentSideEffects ++ sideEffects) case Persist(event) =>
// apply the event before persist so that validation exception is handled before persisting
// the invalid event, in case such validation is implemented in the event handler.
// also, ensure that there is an event handler for each single event
val newState = state.applyEvent(setup, event) val eventToPersist = adaptEvent(event)
val eventAdapterManifest = setup.eventAdapter.manifest(event) val newState2 = internalPersist(setup.context, msg, newState, eventToPersist, eventAdapterManifest) val shouldSnapshotAfterPersist = setup.shouldSnapshot(newState2.state, event, newState2.seqNr) persistingEvents(newState2, state, numberOfEvents = , shouldSnapshotAfterPersist, sideEffects) case PersistAll(events) =>
if (events.nonEmpty) {
// apply the event before persist so that validation exception is handled before persisting
// the invalid event, in case such validation is implemented in the event handler.
// also, ensure that there is an event handler for each single event
var seqNr = state.seqNr
val (newState, shouldSnapshotAfterPersist) = events.foldLeft((state, NoSnapshot: SnapshotAfterPersist)) {
case ((currentState, snapshot), event) =>
seqNr +=
val shouldSnapshot =
if (snapshot == NoSnapshot) setup.shouldSnapshot(currentState.state, event, seqNr) else snapshot
(currentState.applyEvent(setup, event), shouldSnapshot)
} val eventsToPersist = events.map(evt => (adaptEvent(evt), setup.eventAdapter.manifest(evt))) val newState2 = internalPersistAll(setup.context, msg, newState, eventsToPersist) persistingEvents(newState2, state, events.size, shouldSnapshotAfterPersist, sideEffects) } else {
// run side-effects even when no events are emitted
tryUnstashOne(applySideEffects(sideEffects, state))
} case _: PersistNothing.type =>
tryUnstashOne(applySideEffects(sideEffects, state)) case _: Unhandled.type =>
import akka.actor.typed.scaladsl.adapter._
setup.context.system.toClassic.eventStream
.publish(UnhandledMessage(msg, setup.context.system.toClassic.deadLetters, setup.context.self.toClassic))
tryUnstashOne(applySideEffects(sideEffects, state)) case _: Stash.type =>
stashUser(IncomingCommand(msg))
tryUnstashOne(applySideEffects(sideEffects, state))
}
}
好了,基本原理都在这了,再挖下去会更肮脏。为上面的例子设了个运行环境,主要是测试persistence-cassandra-plugin的正确设置,如下:
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"
}
akka-typed(3) - PersistentActor has EventSourcedBehavior的更多相关文章
- 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 ...
- 一图看懂Actor Typed
引言 朋友看罢我之前整理的<Akka Typed 官方文档之随手记>,一人用了诗歌<长城长>作为回赠,另一人则要求推出简化版本.于是抽空整理了几张思维导图,并且用了一些不太恰当 ...
- CQRS与Event Sourcing之浅见
引言 DDD是近年软件设计的热门.CQRS与Event Sourcing作为实施DDD的一种选择,也逐步进入人们的视野.围绕这两个主题,软件开发的大咖[Martin Fowler].[Greg You ...
- Lagom 官方文档之随手记
引言 Lagom是出品Akka的Lightbend公司推出的一个微服务框架,目前最新版本为1.6.2.Lagom一词出自瑞典语,意为"适量". https://www.lagomf ...
- Akka(14): 持久化模式:PersistentActor
Akka程序的特点之一就是高弹性或者强韧性(resilient)的,因为Actor具有自我修复的能力.当Actor模式的程序出现中断情况如:系统崩溃.人为终结等,系统在重启后有关Actor可以恢复之前 ...
- akka-typed(4) - EventSourcedBehavior in action
前面提到过,akka-typed中较重要的改变是加入了EventSourcedBehavior.也就是说增加了一种专门负责EventSource模式的actor, 最终和其它种类的actor一道可以完 ...
随机推荐
- jQuery的事件绑定与触发 - 学习笔记
jQuery的事件绑定与触发 事件绑定 自动触发事件 常用的鼠标事件 事件冒泡和默认行为 事件冒泡 默认行为 获得当前鼠标的位置和按键 jQuery的事件绑定与触发 事件绑定 基本绑定 $(eleme ...
- Android 开发技术周报 Issue#279
新闻 丧心病狂or形势所迫?谈谈Android奇葩的"链式启动" 传闻称Android TV将更名为Google TV 谷歌官宣Android 11 Beta发布会:6月3日见 教 ...
- 初探Redis-基础类型Hash
Redis存在五种基础类型:字符串(String).队列(List).哈希(Hash).集合(Set).有序集合(Sorted Set).本次列举出Hash的常用操作. Redis官网:https:/ ...
- JDBC02 加载JDBC驱动 建立连接
JDBC(Java Database Connection)为Java开发者使用数据库提供了统一的编程接口 sun公司由于不知道各个主流商用数据库的程序代码,因此无法自己写代码连接各个数据库,因此su ...
- 实验三 UML建模工具的安装与使用
一. 实验目的 1) 学习使用 EA(Enterprise Architect) 开发环境创建模型的一般方法: 2) 理解 EA 界面布局和元素操作的一般技巧: 3) 熟悉 UML 中的各种图的建立和 ...
- python 基础应用5-简单购物车
1.列表去重 #列表去重 li = [1,2,33,33,2,1,4,5,6,6] set1 = set(li)# 转为集合 li = list(set1)# 转为列表 print(li)#[1, 2 ...
- SpringMvc 你该知道如何在HandlerExceptionResolver中获取Model
在项目开发中,我们通常通过参数的形式注入Model对象,如: @RequestMapping("/demo") public String demo(Model model) { ...
- Echarts关于tree树数据渲染图例最新实例
最近做项目接到新的需求,根据本身系统结构数据做一个图形化展示,要求好看易用,有交互,就说了这么多,然后就要求两天给一版瞅瞅,MMP,真把前端当神了(你倒是把待遇提到神的地位啊...) 唉,吐槽归吐槽, ...
- PC、APP、H5三端测试的区别
一,针对同一个系统功能的测试,三端所测的业务流程是一样的 二,一般情况下手机端和PC端都对应一套后台服务,比如说笔者公司所开发的互联网金融平台,整个平台做了分布式服务架构,后台服务包括用户服务.交易服 ...
- 8.3 Go channel
8.3 Go channel 在Go语言中,关键字go的引入使得Go语言并发编程更加简单而优雅,但是并发编程的复杂性,以及时刻关注并发编程容易出现的问题需要时刻警惕. 并发编程的难度在于协调,然而协调 ...