前面我们讨论过FSM,一种专门为维护内部状态而设计的Actor,它的特点是一套特殊的DSL能很方便地进行状态转换。FSM的状态转换模式特别适合对应现实中的业务流程,因为它那套DSL可以更形象的描述业务功能。为了实现FSM的可用性,就必须为FSM再增加自我修复能力,PersistentFSM是FSM和PersistentActor的合并,是在状态机器模式的基础上再增加了状态转变事件的持久化,从而实现内部状态的自我修复功能的。在FSM结构基础上,PersistentFSM又增加了领域事件(domain-event)这一元素,也就是事件来源(event-sourcing)模式里持久化的目标。PersistentFSM trait是如下定义的:

/**
* A FSM implementation with persistent state.
*
* Supports the usual [[akka.actor.FSM]] functionality with additional persistence features.
* `PersistentFSM` is identified by 'persistenceId' value.
* State changes are persisted atomically together with domain events, which means that either both succeed or both fail,
* i.e. a state transition event will not be stored if persistence of an event related to that change fails.
* Persistence execution order is: persist -> wait for ack -> apply state.
* Incoming messages are deferred until the state is applied.
* State Data is constructed based on domain events, according to user's implementation of applyEvent function.
*
*/
trait PersistentFSM[S <: FSMState, D, E] extends PersistentActor with PersistentFSMBase[S, D, E] with ActorLogging {...}

我们看到:PersistentFSM继承了PersistentActor,代表它具备了事件来源模式中的事件持久化和日志恢复能力。继承的另一个类型PersistentFSMBase是FSM trait的重新定义,针对状态机器增加的持久化特性设计了一套持久化状态转换的DSL。PersistentFSM trait的三个类参数S,D,E分别代表状态类型(State)、状态数据(Data)、领域事件(event)。与FSM比较:PersistentFSM除增加了event参数外,State类型是以FSMState类型为基础的,方便对State进行序列化(serialization):

 /**
* FSM state and data snapshot
*
* @param stateIdentifier FSM state identifier
* @param data FSM state data
* @param timeout FSM state timeout
* @tparam D state data type
*/
@InternalApi
private[persistence] case class PersistentFSMSnapshot[D](stateIdentifier: String, data: D, timeout: Option[FiniteDuration]) extends Message /**
* FSMState base trait, makes possible for simple default serialization by conversion to String
*/
trait FSMState {
def identifier: String
}

PersistentFSM程序结构与FSM相似:

class PersistentFSMActor extends PersistentFSM[StateType,DataType,EventType] {

  startWith(initState,initData)  //起始状态

  when(stateA) {...}             //处理各种状态
when(stateB) {...} whenUnhandled {...} //处理共性状态 onTransition {...} //状态转变跟踪 }

从这个程序结构来看,日志恢复(recovery)receiveRecovery函数实现应该隐含在类型定义里面:

 /**
* After recovery events are handled as in usual FSM actor
*/
override def receiveCommand: Receive = {
super[PersistentFSMBase].receive
} /**
* Discover the latest recorded state
*/
override def receiveRecover: Receive = {
case domainEventTag(event) ⇒ startWith(stateName, applyEvent(event, stateData))
case StateChangeEvent(stateIdentifier, timeout) ⇒ startWith(statesMap(stateIdentifier), stateData, timeout)
case SnapshotOffer(_, PersistentFSMSnapshot(stateIdentifier, data: D, timeout)) ⇒ startWith(statesMap(stateIdentifier), data, timeout)
case RecoveryCompleted ⇒
initialize()
onRecoveryCompleted()
}

注意initialize已经过时,不要再用,我们可以重写onRecoveryCompleted()来实现一些初始化工作。那么事件写入日志又放在哪里了呢:

  /**
* Persist FSM State and FSM State Data
*/
override private[akka] def applyState(nextState: State): Unit = {
var eventsToPersist: immutable.Seq[Any] = nextState.domainEvents.toList //Prevent StateChangeEvent persistence when staying in the same state, except when state defines a timeout
if (nextState.notifies || nextState.timeout.nonEmpty) {
eventsToPersist = eventsToPersist :+ StateChangeEvent(nextState.stateName.identifier, nextState.timeout)
} if (eventsToPersist.isEmpty) {
//If there are no events to persist, just apply the state
super.applyState(nextState)
} else {
//Persist the events and apply the new state after all event handlers were executed
var nextData: D = stateData
var handlersExecutedCounter = def applyStateOnLastHandler() = {
handlersExecutedCounter +=
if (handlersExecutedCounter == eventsToPersist.size) {
super.applyState(nextState using nextData)
currentStateTimeout = nextState.timeout
nextState.afterTransitionDo(stateData)
}
} persistAll[Any](eventsToPersist) {
case domainEventTag(event) ⇒
nextData = applyEvent(event, nextData)
applyStateOnLastHandler()
case StateChangeEvent(stateIdentifier, timeout) ⇒
applyStateOnLastHandler()
}
}
}

注意这个内部函数applyState重写(override)了父辈PersistentFSMBase中的applyState:

  /*
* *******************************************
* Main actor receive() method
* *******************************************
*/
override def receive: Receive = {
case TimeoutMarker(gen) ⇒
if (generation == gen) {
processMsg(StateTimeout, "state timeout")
}
case t @ Timer(name, msg, repeat, gen) ⇒
if ((timers contains name) && (timers(name).generation == gen)) {
if (timeoutFuture.isDefined) {
timeoutFuture.get.cancel()
timeoutFuture = None
}
generation +=
if (!repeat) {
timers -= name
}
processMsg(msg, t)
}
case SubscribeTransitionCallBack(actorRef) ⇒
// TODO Use context.watch(actor) and receive Terminated(actor) to clean up list
listeners.add(actorRef)
// send current state back as reference point
actorRef ! CurrentState(self, currentState.stateName, currentState.timeout)
case Listen(actorRef) ⇒
// TODO Use context.watch(actor) and receive Terminated(actor) to clean up list
listeners.add(actorRef)
// send current state back as reference point
actorRef ! CurrentState(self, currentState.stateName, currentState.timeout)
case UnsubscribeTransitionCallBack(actorRef) ⇒
listeners.remove(actorRef)
case Deafen(actorRef) ⇒
listeners.remove(actorRef)
case value ⇒
if (timeoutFuture.isDefined) {
timeoutFuture.get.cancel()
timeoutFuture = None
}
generation +=
processMsg(value, sender())
} private def processMsg(value: Any, source: AnyRef): Unit = {
val event = Event(value, currentState.stateData)
processEvent(event, source)
} private[akka] def processEvent(event: Event, source: AnyRef): Unit = {
val stateFunc = stateFunctions(currentState.stateName)
val nextState = if (stateFunc isDefinedAt event) {
stateFunc(event)
} else {
// handleEventDefault ensures that this is always defined
handleEvent(event)
}
applyState(nextState)
} private[akka] def applyState(nextState: State): Unit = {
nextState.stopReason match {
case None ⇒ makeTransition(nextState)
case _ ⇒
nextState.replies.reverse foreach { r ⇒ sender() ! r }
terminate(nextState)
context.stop(self)
}
}

在PersistentFSM trait中的抽象函数receiveCommand在实现时直接调用了PersistentFSMBase中的receive:

 /**
* After recovery events are handled as in usual FSM actor
*/
override def receiveCommand: Receive = {
super[PersistentFSMBase].receive
}

PersistentFSM还需要实现抽象函数applyEvent:

  /**
* Override this handler to define the action on Domain Event
*
* @param domainEvent domain event to apply
* @param currentData state data of the previous state
* @return updated state data
*/
def applyEvent(domainEvent: E, currentData: D): D

这个函数的主要功能是针对发生的事件进行当前状态数据的转换。另一个需要实现的抽象函数是domainEventClassTag。这是一个ClassTag[E]实例,用来解决泛型E的模式匹配问题(由scala语言类型擦拭type-erasure造成):

  /**
* Enables to pass a ClassTag of a domain event base type from the implementing class
*
* @return [[scala.reflect.ClassTag]] of domain event base type
*/
implicit def domainEventClassTag: ClassTag[E] /**
* Domain event's [[scala.reflect.ClassTag]]
* Used for identifying domain events during recovery
*/
val domainEventTag = domainEventClassTag
...
/**
* Discover the latest recorded state
*/
override def receiveRecover: Receive = {
case domainEventTag(event) ⇒ startWith(stateName, applyEvent(event, stateData))
...
persistAll[Any](eventsToPersist) {
case domainEventTag(event) ⇒
nextData = applyEvent(event, nextData)
applyStateOnLastHandler()
case StateChangeEvent(stateIdentifier, timeout) ⇒
applyStateOnLastHandler()
}

akka-persistentFSM官方文档中的例子挺有代表性,下面我就根据这个例子来进行示范。这是个电商购物车的例子。用PersistentFSM来实现最大的优点就是在任何情况下都可以保证购物车内容的一致性。而且可以自动保存电商用户所有的历史选购过程方便将来大数据分析-这已经是一种潮流了,甚至对中途暂时放弃了的购物车也可以在下次登陆时自动恢复。好了,我们先来研究一下这个例子:首先是数据结构:

import akka.persistence.fsm.PersistentFSM._

object WebShopping {
sealed trait UserState extends FSMState //状态类型
case object LookingAround extends UserState { //浏览状态,可转Shopping状态
override def identifier: String = "Looking Around"
}
case object Shopping extends UserState { //拣选状态,可转到Paid状态或超时变Inactive
override def identifier: String = "Shopping"
}
case object Inactive extends UserState { //停滞状态,可转回Shopping状态
override def identifier: String = "Inactive"
}
case object Paid extends UserState { //结账完成购物,只能查询购物结果,或退出
override def identifier: String = "Paid"
} case class Item(id: String, name: String, price: Float)
//state data
sealed trait ShoppingCart { //true functional structure
def addItem(item: Item): ShoppingCart
def removeItem(id: String): ShoppingCart
def empty(): ShoppingCart
}
case class LoadedCart(items: Seq[Item]) extends ShoppingCart {
override def addItem(item: Item): ShoppingCart = LoadedCart(items :+ item)
override def removeItem(id: String): ShoppingCart = {
val newItems = items.filter {item => item.id != id}
if (newItems.length > )
LoadedCart(newItems)
else
EmptyCart
}
override def empty() = EmptyCart
}
case object EmptyCart extends ShoppingCart {
override def addItem(item: Item) = LoadedCart(item :: Nil)
override def empty() = this
override def removeItem(id: String): ShoppingCart = this
} }

UserState是FSM的当前状态。状态代表FSM的流程,每种状态运行它自己的业务流程:

  when(LookingAround) {...}             //处理各种状态
when(Shopping) {...}
when(Inactive) {...}
when(Paid) {...}
...

ShoppingCart代表FSM当前状态的数据。每种状态都有可能具备不同的数据。注意ShoppingCart是典型的函数式数据结构:不可变结构,任何更新操作都返回新的结构。StateData ShoppingCart是在抽象函数applyEvent里更新的。再看看applyEvent的函数款式:

  /**
* Override this handler to define the action on Domain Event
*
* @param domainEvent domain event to apply
* @param currentData state data of the previous state
* @return updated state data
*/
def applyEvent(domainEvent: E, currentData: D): D

要求用户提供这个函数的实现:根据发生的事件及当前状态数据产生新的状态数据。applyEvent函数是如下调用的:

 override def receiveRecover: Receive = {
case domainEventTag(event) ⇒ startWith(stateName, applyEvent(event, stateData))
case StateChangeEvent(stateIdentifier, timeout) ⇒ startWith(statesMap(stateIdentifier), stateData, timeout)
case SnapshotOffer(_, PersistentFSMSnapshot(stateIdentifier, data: D, timeout)) ⇒ startWith(statesMap(stateIdentifier), data, timeout)
case RecoveryCompleted ⇒
initialize()
onRecoveryCompleted()
}
...
/**
* Persist FSM State and FSM State Data
*/
override private[akka] def applyState(nextState: State): Unit = {
var eventsToPersist: immutable.Seq[Any] = nextState.domainEvents.toList //Prevent StateChangeEvent persistence when staying in the same state, except when state defines a timeout
if (nextState.notifies || nextState.timeout.nonEmpty) {
eventsToPersist = eventsToPersist :+ StateChangeEvent(nextState.stateName.identifier, nextState.timeout)
} if (eventsToPersist.isEmpty) {
//If there are no events to persist, just apply the state
super.applyState(nextState)
} else {
//Persist the events and apply the new state after all event handlers were executed
var nextData: D = stateData
var handlersExecutedCounter = def applyStateOnLastHandler() = {
handlersExecutedCounter +=
if (handlersExecutedCounter == eventsToPersist.size) {
super.applyState(nextState using nextData)
currentStateTimeout = nextState.timeout
nextState.afterTransitionDo(stateData)
}
} persistAll[Any](eventsToPersist) {
case domainEventTag(event) ⇒
nextData = applyEvent(event, nextData)
applyStateOnLastHandler()
case StateChangeEvent(stateIdentifier, timeout) ⇒
applyStateOnLastHandler()
}
}
}

状态转换是通过stay, goto,stop实现的:

 /**
* Produce transition to other state.
* Return this from a state function in order to effect the transition.
*
* This method always triggers transition events, even for `A -> A` transitions.
* If you want to stay in the same state without triggering an state transition event use [[#stay]] instead.
*
* @param nextStateName state designator for the next state
* @return state transition descriptor
*/
final def goto(nextStateName: S): State = PersistentFSM.State(nextStateName, currentState.stateData)() /**
* Produce "empty" transition descriptor.
* Return this from a state function when no state change is to be effected.
*
* No transition event will be triggered by [[#stay]].
* If you want to trigger an event like `S -> S` for `onTransition` to handle use `goto` instead.
*
* @return descriptor for staying in current state
*/
final def stay(): State = goto(currentState.stateName).withNotification(false) // cannot directly use currentState because of the timeout field /**
* Produce change descriptor to stop this FSM actor with reason "Normal".
*/
final def stop(): State = stop(Normal)

状态数据转换是用applying实现的:

 /**
* Specify domain events to be applied when transitioning to the new state.
*/
@varargs def applying(events: E*): State[S, D, E] = {
copy(domainEvents = domainEvents ++ events)
} /**
* Register a handler to be triggered after the state has been persisted successfully
*/
def andThen(handler: D ⇒ Unit): State[S, D, E] = {
copy(afterTransitionDo = handler)
}

applying对State[S,D,E]类型进行操作,State[S,D,E]的定义如下:

 /**
* This captures all of the managed state of the [[akka.actor.FSM]]: the state
* name, the state data, possibly custom timeout, stop reason, replies
* accumulated while processing the last message, possibly domain event and handler
* to be executed after FSM moves to the new state (also triggered when staying in the same state)
*/
final case class State[S, D, E](
stateName: S,
stateData: D,
timeout: Option[FiniteDuration] = None,
stopReason: Option[Reason] = None,
replies: List[Any] = Nil,
domainEvents: Seq[E] = Nil,
afterTransitionDo: D ⇒ Unit = { _: D ⇒ })(private[akka] val notifies: Boolean = true) { /**
* Copy object and update values if needed.
*/
@InternalApi
private[akka] def copy(stateName: S = stateName, stateData: D = stateData, timeout: Option[FiniteDuration] = timeout, stopReason: Option[Reason] = stopReason, replies: List[Any] = replies, notifies: Boolean = notifies, domainEvents: Seq[E] = domainEvents, afterTransitionDo: D ⇒ Unit = afterTransitionDo): State[S, D, E] = {
State(stateName, stateData, timeout, stopReason, replies, domainEvents, afterTransitionDo)(notifies)
}

applying实际上是把发生事件存入一个清单domainEvents,然后在调用applyState函数时再施用:

 /**
* Persist FSM State and FSM State Data
*/
override private[akka] def applyState(nextState: State): Unit = {
var eventsToPersist: immutable.Seq[Any] = nextState.domainEvents.toList
...

PersistentFSM继承了PersistentActor事件来源(event-sourcing)模式。下面是command和event的类型定义:

  sealed trait Command
case class AddItem(item: Item) extends Command
case class RemoveItem(id: String) extends Command
case object Buy extends Command
case object Leave extends Command
case object GetCart extends Command sealed trait DomainEvent
case class ItemAdded(item: Item) extends DomainEvent
case class ItemRemoved(id: String) extends DomainEvent
case object OrderClosed extends DomainEvent

我们知道:DomainEvent将会被写入日志,它与Command的关系是:运算某些Command时会产生DomainEvent,然后这些产生的DomainEvent会被写入日志。

我们开始设计这个PersistentFSM:

class WebShopping(webUserId: String) extends PersistentFSM[UserState,ShoppingCart,DomainEvent] {
override def persistenceId: String = webUserId
override def domainEventClassTag: ClassTag[DomainEvent] = classTag[DomainEvent] override def applyEvent(domainEvent: DomainEvent, currentCart: ShoppingCart): ShoppingCart =
domainEvent match {
case ItemAdded(item) => currentCart.addItem(item)
case ItemRemoved(id) => currentCart.removeItem(id)
case OrderClosed => currentCart.empty()
}
}

我们首先实现了trait中的抽象函数。其中persistenceId代表了当前购物者的userid。这样我们就可以把用户的购物过程写入日志。试想想这里面的意义:我们用一个独立的Actor来处理一个用户的购物过程。Actor对资源要求很低,但运算能力却高效强大,一个服务器上如果有足够内存就可以轻松负载几十万甚至百万级Actor实例,如果再使用akka-cluster的话不知不觉我们已经实现了可以容纳百万级用户的电商网站了。

好了,现在我们看看这个PersistentFSM的完整业务流程:

class WebShopping(webUserId: String) extends PersistentFSM[UserState,ShoppingCart,DomainEvent] {
override def persistenceId: String = webUserId
override def domainEventClassTag: ClassTag[DomainEvent] = classTag[DomainEvent] override def applyEvent(event: DomainEvent, currentCart: ShoppingCart): ShoppingCart =
event match {
case ItemAdded(item) => currentCart.addItem(item)
case ItemRemoved(id) => currentCart.removeItem(id)
case OrderClosed => currentCart.empty() //买单成功后清空ShoppingCart
} startWith(LookingAround,EmptyCart) //初次登陆购物状态 when(LookingAround) { //浏览时可以加入购物车转到Shopping状态
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"LookingAround-Adding Item: $item",currentCart))
goto(Shopping) applying ItemAdded(item) forMax( second)
case Event(GetCart,currentCart) =>
stay replying currentCart
} when(Shopping) {
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Adding Item: $item",currentCart))
stay applying ItemAdded(item) forMax ( second) andThen {
case cart @ _ =>
context.system.eventStream.publish(CurrentCart(s"Shopping-after adding Item: $item",cart))
}
case Event(RemoveItem(id),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Removing Item: $id",currentCart))
stay applying ItemRemoved(id) forMax ( second) andThen {
case cart @ _ =>
context.system.eventStream.publish(CurrentCart(s"Shopping-after removing Item: $id",cart))
}
case Event(Buy,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Buying",currentCart))
goto(Paid) applying OrderClosed forMax ( second) andThen {
case cart @ _ => saveStateSnapshot()
context.system.eventStream.publish(CurrentCart(s"Shopping-after paid",cart))
} case Event(Leave,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Leaving",currentCart))
stop()
case Event(StateTimeout,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Timeout",currentCart))
goto(Inactive) forMax( second)
case Event(GetCart,currentCart) =>
stay replying currentCart
} when(Inactive) {
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Inactive-Adding Item: $item",currentCart))
goto(Shopping) applying ItemAdded(item) forMax( second)
case Event(StateTimeout,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Inactive-Timeout",currentCart))
stop()
} when(Paid) {
case Event(Leave,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Paid-Leaving",currentCart))
stop()
case Event(GetCart,currentCart) =>
stay replying currentCart
}
}

我们看到通过FSM的DSL,PersistentActor和FSM的具体技术特征和细节被隐藏了,呈现给编程人员的是一段对业务流程的描述,这样可以使整段代码代表的功能更贴近现实应用,容易理解。

下面是有关数据快照、日志维护以及过程跟踪等方法的示范:

  whenUnhandled {
case Event(SaveSnapshotSuccess(metadata),currentCart) =>
context.system.eventStream.publish(CurrentCart("Successfully saved snapshot",currentCart))
//假如不需要保存历史购物过程,可以清理日志和旧快照
deleteSnapshots(SnapshotSelectionCriteria(maxSequenceNr = metadata.sequenceNr - ))
deleteMessages(metadata.sequenceNr)
stay()
case Event(SaveSnapshotFailure(metadata, reason),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Fail to save snapshot for $reason",currentCart))
stay()
case Event(DeleteMessagesSuccess(toSeq),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Succefully deleted journal upto: $toSeq",currentCart))
stay()
case Event(DeleteMessagesFailure(cause,toSeq),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Failed to delete journal upto: $toSeq because: $cause",currentCart))
stay()
case Event(DeleteSnapshotsSuccess(crit),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Successfully deleted snapshots for $crit",currentCart))
stay()
case Event(DeleteSnapshotsFailure(crit,cause),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Failed to delete snapshots $crit because: $cause",currentCart))
stay()
} onTransition {
case LookingAround -> Shopping =>
context.system.eventStream.publish(CurrentCart("LookingAround -> Shopping",stateData))
case Shopping -> Inactive =>
context.system.eventStream.publish(CurrentCart("Shopping -> Inactive",stateData))
case Shopping -> Paid =>
context.system.eventStream.publish(CurrentCart("Shopping -> Paid",stateData))
case Inactive -> Shopping =>
context.system.eventStream.publish(CurrentCart("Inactive -> Shopping",stateData))
}
override def onRecoveryCompleted(): Unit =
context.system.eventStream.publish(CurrentCart("OnRecoveryCompleted",stateData)) override def onPersistFailure(cause: Throwable, event: Any, seqNr: Long): Unit =
context.system.eventStream.publish(CurrentCart(s"onPersistFailure ${cause.getMessage}",stateData)) override def onPersistRejected(cause: Throwable, event: Any, seqNr: Long): Unit =
context.system.eventStream.publish(CurrentCart(s"onPersistRejected ${cause.getMessage}",stateData)) override def onRecoveryFailure(cause: Throwable, event: Option[Any]): Unit =
context.system.eventStream.publish(CurrentCart(s"onRecoveryFailure ${cause.getMessage}",stateData))

下面是过程跟踪器的设计代码:

package persistentfsm.tracker
import akka.actor._
import persistentfsm.cart.WebShopping
object EventTracker {
def props = Props(new EventTracker)
}
class EventTracker extends Actor {
override def preStart(): Unit = {
context.system.eventStream.subscribe(self,classOf[WebShopping.CurrentCart])
super.preStart()
} override def postStop(): Unit = {
context.system.eventStream.unsubscribe(self)
super.postStop()
} override def receive: Receive = {
case WebShopping.CurrentCart(loc,cart) =>
println(loc)
cart match {
case WebShopping.EmptyCart => println("empty cart!")
case WebShopping.LoadedCart(items) => println(s"Current content in cart: $items")
}
} }

用下面的代码来测试运行:

package persistentfsm.demo
import persistentfsm.cart._
import persistentfsm.tracker._
import akka.actor._
import WebShopping._ object PersistentFSMDemo extends App {
val pfSystem = ActorSystem("persistentfsm-system")
val trackerActor = pfSystem.actorOf(EventTracker.props,"tracker")
val cart123 = pfSystem.actorOf(WebShopping.props("")) cart123 ! GetCart
cart123 ! AddItem(Item("","Cigar",12.50))
cart123 ! AddItem(Item("","Wine",18.30))
cart123 ! AddItem(Item("","Coffee",5.50))
cart123 ! GetCart
cart123 ! RemoveItem("")
cart123 ! Buy
cart123 ! GetCart
cart123 ! AddItem(Item("","Bread",3.25))
cart123 ! AddItem(Item("","Cake",5.25)) scala.io.StdIn.readLine() pfSystem.terminate() }

重复运算可以得出:结账后选购的商品可以恢复。如果中途异常退出,购物车中已经选购的商品任然保留。

下面是本次示范的完整源代码:

build.sbt

name := "persistent-fsm"

version := "1.0"

scalaVersion := "2.11.9"

sbtVersion := "0.13.5"

libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.5.3",
"com.typesafe.akka" %% "akka-persistence" % "2.5.3",
"ch.qos.logback" % "logback-classic" % "1.1.7",
"com.typesafe.akka" %% "akka-persistence-cassandra" % "0.54",
"com.typesafe.akka" %% "akka-persistence-cassandra-launcher" % "0.54" % Test
)

application.conf

akka {
persistence {
journal.plugin = "cassandra-journal"
snapshot-store.plugin = "cassandra-snapshot-store"
fsm {
snapshot-after =
}
}
}
akka.actor.warn-about-java-serializer-usage = off

WebShopping.scala

package persistentfsm.cart
import WebShopping._
import akka.persistence.fsm._
import akka.persistence.fsm.PersistentFSM._
import akka.persistence._
import akka.actor._
import scala.concurrent.duration._
import scala.reflect._ object WebShopping {
sealed trait UserState extends FSMState //状态类型
case object LookingAround extends UserState { //浏览状态,可转Shopping状态
override def identifier: String = "Looking Around"
}
case object Shopping extends UserState { //拣选状态,可转到Paid状态或超时变Inactive
override def identifier: String = "Shopping"
}
case object Inactive extends UserState { //停滞状态,可转回Shopping状态
override def identifier: String = "Inactive"
}
case object Paid extends UserState { //结账完成购物,只能查询购物结果,或退出
override def identifier: String = "Paid"
} case class Item(id: String, name: String, price: Double)
//state data
sealed trait ShoppingCart { //true functional structure
def addItem(item: Item): ShoppingCart
def removeItem(id: String): ShoppingCart
def empty(): ShoppingCart
}
case class LoadedCart(items: Seq[Item]) extends ShoppingCart {
override def addItem(item: Item): ShoppingCart = LoadedCart(items :+ item)
override def removeItem(id: String): ShoppingCart = {
val newItems = items.filter {item => item.id != id}
if (newItems.length > )
LoadedCart(newItems)
else
EmptyCart
}
override def empty() = EmptyCart
}
case object EmptyCart extends ShoppingCart {
override def addItem(item: Item) = LoadedCart(item :: Nil)
override def empty() = this
override def removeItem(id: String): ShoppingCart = this
} sealed trait Command
case class AddItem(item: Item) extends Command
case class RemoveItem(id: String) extends Command
case object Buy extends Command
case object Leave extends Command
case object GetCart extends Command sealed trait DomainEvent
case class ItemAdded(item: Item) extends DomainEvent
case class ItemRemoved(id: String) extends DomainEvent
case object OrderClosed extends DomainEvent
//logging message type
case class CurrentCart(location: String, cart: ShoppingCart) def props(uid: String) = Props(new WebShopping(uid)) }
class WebShopping(webUserId: String) extends PersistentFSM[UserState,ShoppingCart,DomainEvent] {
override def persistenceId: String = webUserId
override def domainEventClassTag: ClassTag[DomainEvent] = classTag[DomainEvent] override def applyEvent(event: DomainEvent, currentCart: ShoppingCart): ShoppingCart =
event match {
case ItemAdded(item) => currentCart.addItem(item)
case ItemRemoved(id) => currentCart.removeItem(id)
case OrderClosed => currentCart.empty() //买单成功后清空ShoppingCart
} startWith(LookingAround,EmptyCart) //初次登陆购物状态 when(LookingAround) { //浏览时可以加入购物车转到Shopping状态
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"LookingAround-Adding Item: $item",currentCart))
goto(Shopping) applying ItemAdded(item) forMax( second)
case Event(GetCart,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"LookingAround-Showing",currentCart))
stay replying currentCart
} when(Shopping) {
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Adding Item: $item",currentCart))
stay applying ItemAdded(item) forMax ( second) andThen {
case cart @ _ =>
context.system.eventStream.publish(CurrentCart(s"Shopping-after adding Item: $item",cart))
}
case Event(RemoveItem(id),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Removing Item: $id",currentCart))
stay applying ItemRemoved(id) forMax ( second) andThen {
case cart @ _ =>
context.system.eventStream.publish(CurrentCart(s"Shopping-after removing Item: $id",cart))
}
case Event(Buy,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Buying",currentCart))
goto(Paid) applying OrderClosed forMax ( second) andThen {
case cart @ _ => saveStateSnapshot()
context.system.eventStream.publish(CurrentCart(s"Shopping-after paid",cart))
} case Event(Leave,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Leaving",currentCart))
stop()
case Event(StateTimeout,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Shopping-Timeout",currentCart))
goto(Inactive) forMax( second)
case Event(GetCart,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"LookingAround-Showing",currentCart))
stay replying currentCart
} when(Inactive) {
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Inactive-Adding Item: $item",currentCart))
goto(Shopping) applying ItemAdded(item) forMax( second)
case Event(StateTimeout,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Inactive-Timeout",currentCart))
stop()
} when(Paid) {
case Event(Leave,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Paid-Leaving",currentCart))
stop()
case Event(GetCart,currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Paid-Showing",currentCart))
stay replying currentCart
case Event(AddItem(item),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Paid-Adding Item: $item",currentCart))
goto(Shopping) applying ItemAdded(item) forMax( second)
} whenUnhandled {
case Event(SaveSnapshotSuccess(metadata),currentCart) =>
context.system.eventStream.publish(CurrentCart("Successfully saved snapshot",currentCart))
//假如不需要保存历史购物过程,可以清理日志和旧快照
deleteSnapshots(SnapshotSelectionCriteria(maxSequenceNr = metadata.sequenceNr - ))
deleteMessages(metadata.sequenceNr)
stay()
case Event(SaveSnapshotFailure(metadata, reason),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Fail to save snapshot for $reason",currentCart))
stay()
case Event(DeleteMessagesSuccess(toSeq),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Succefully deleted journal upto: $toSeq",currentCart))
stay()
case Event(DeleteMessagesFailure(cause,toSeq),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Failed to delete journal upto: $toSeq because: $cause",currentCart))
stay()
case Event(DeleteSnapshotsSuccess(crit),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Successfully deleted snapshots for $crit",currentCart))
stay()
case Event(DeleteSnapshotsFailure(crit,cause),currentCart) =>
context.system.eventStream.publish(CurrentCart(s"Failed to delete snapshots $crit because: $cause",currentCart))
stay()
case _ => goto(LookingAround)
} onTransition {
case LookingAround -> Shopping =>
context.system.eventStream.publish(CurrentCart("State changed: LookingAround -> Shopping",stateData))
case Shopping -> Inactive =>
context.system.eventStream.publish(CurrentCart("State changed: Shopping -> Inactive",stateData))
case Shopping -> Paid =>
context.system.eventStream.publish(CurrentCart("State changed: Shopping -> Paid",stateData))
case Inactive -> Shopping =>
context.system.eventStream.publish(CurrentCart("State changed: Inactive -> Shopping",stateData))
case Paid -> LookingAround =>
context.system.eventStream.publish(CurrentCart("State changed: Paid -> LookingAround",stateData))
}
override def onRecoveryCompleted(): Unit = {
context.system.eventStream.publish(CurrentCart("OnRecoveryCompleted", stateData))
} override def onPersistFailure(cause: Throwable, event: Any, seqNr: Long): Unit =
context.system.eventStream.publish(CurrentCart(s"onPersistFailure ${cause.getMessage}",stateData)) override def onPersistRejected(cause: Throwable, event: Any, seqNr: Long): Unit =
context.system.eventStream.publish(CurrentCart(s"onPersistRejected ${cause.getMessage}",stateData)) override def onRecoveryFailure(cause: Throwable, event: Option[Any]): Unit =
context.system.eventStream.publish(CurrentCart(s"onRecoveryFailure ${cause.getMessage}",stateData)) }

EventTracker.scala

package persistentfsm.tracker
import akka.actor._
import persistentfsm.cart.WebShopping
object EventTracker {
def props = Props(new EventTracker)
}
class EventTracker extends Actor {
override def preStart(): Unit = {
context.system.eventStream.subscribe(self,classOf[WebShopping.CurrentCart])
super.preStart()
} override def postStop(): Unit = {
context.system.eventStream.unsubscribe(self)
super.postStop()
} override def receive: Receive = {
case WebShopping.CurrentCart(loc,cart) =>
println(loc)
cart match {
case WebShopping.EmptyCart => println("empty cart!")
case WebShopping.LoadedCart(items) => println(s"Current content in cart: $items")
}
} }

PersistentFSMDemo.scala

package persistentfsm.demo
import persistentfsm.cart._
import persistentfsm.tracker._
import akka.actor._
import WebShopping._ object PersistentFSMDemo extends App {
val pfSystem = ActorSystem("persistentfsm-system")
val trackerActor = pfSystem.actorOf(EventTracker.props,"tracker")
val cart123 = pfSystem.actorOf(WebShopping.props("")) cart123 ! GetCart
cart123 ! AddItem(Item("","Cigar",12.50))
cart123 ! AddItem(Item("","Wine",18.30))
cart123 ! AddItem(Item("","Coffee",5.50))
cart123 ! GetCart
cart123 ! RemoveItem("")
cart123 ! Buy
cart123 ! GetCart
cart123 ! AddItem(Item("","Bread",3.25))
cart123 ! AddItem(Item("","Cake",5.25)) scala.io.StdIn.readLine() pfSystem.terminate() }

Akka(16): 持久化模式:PersistentFSM-可以自动修复的状态机器的更多相关文章

  1. Redis 2种持久化模式的缺陷

    http://blog.csdn.net/hexieshangwang/article/details/47254087 一.RDB持久化模式缺陷 1.问题描述: 并发200路,模拟不断写Redis, ...

  2. 用纯c语言完成16位模式下的引导程序

    gcc在16位模式下做引导程序遇到的问题太多了,不过费了很大劲逐一解决了. 自己的小demo也从一开始的1个扇区增加到了20几个扇区. 先上图做个标记,后面有时间再上代码.

  3. 16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16

    一.Intel 32 位处理器的工作模式 如上图所示,Intel 32 位处理器有3种工作模式. (1)实模式:工作方式相当于一个8086 (2)保护模式:提供支持多任务环境的工作方式,建立保护机制 ...

  4. 烂泥:openvpn tun模式下客户端与内网机器通信

    本文由秀依林枫提供友情赞助,首发于烂泥行天下 前两篇文章我们介绍了有关openvpn的搭建与配置文件的讲解,这篇文章我们再聊介绍下,在tun模式下openvpn客户端如何与内网机器通信的问题. 一.实 ...

  5. Akka(14): 持久化模式:PersistentActor

    Akka程序的特点之一就是高弹性或者强韧性(resilient)的,因为Actor具有自我修复的能力.当Actor模式的程序出现中断情况如:系统崩溃.人为终结等,系统在重启后有关Actor可以恢复之前 ...

  6. Akka(15): 持久化模式:AtLeastOnceDelivery-消息保证送达模式

    消息保证送达是指消息发送方保证在任何情况下都会至少一次确定的消息送达.AtleastOnceDelivery是一个独立的trait,主要作用是对不确定已送达的消息进行补发,这是一种自动的操作,无需用户 ...

  7. Redis不重启的情况下 切换持久化模式

    确保redis版本在2.2以上 [root@localhost /]# redis-server -v Redis server v=4.0.10 sha=00000000:0 malloc=jema ...

  8. 16.迭代器模式(Iterator Pattern)

    using System; namespace ConsoleApplication9 { class Program { /// <summary> /// 迭代器模式提供了一种方法顺序 ...

  9. C#设计模式(16)——迭代器模式(Iterator Pattern)

    一.引言 在上篇博文中分享了我对命令模式的理解,命令模式主要是把行为进行抽象成命令,使得请求者的行为和接受者的行为形成低耦合.在一章中,将介绍一下迭代器模式.下面废话不多说了,直接进入本博文的主题. ...

随机推荐

  1. java编码详解

    举个例子 我们在开发过程中,特别是多种编码格式并存的情况下,很容易遇到乱码问题. 假如有一个GBK编码java文件,然后再使用-Dfile.encoding=GBK参数,写入的文件中哪些是乱码呢.那如 ...

  2. php追加数组

    <?php //追加数组 array_merge_recursive()函数与array_merge()相同,可以将两个或多个数组合并在一起,形成一个联合的数组.两者之间的区别在于,当某个输入数 ...

  3. 使用c#解析json库

    写了个c#版的json解析库,提供了json到hashtable以及hashtable到json字符串的转换 受惠于c#的语法特性,hashtable到json的解析变得非常简单 先判断传入的obje ...

  4. JAVAEE——SSH项目实战01:SVN介绍、安装和使用方法

    1 学习目标 1.掌握svn服务端.svn客户端.svn eclipse插件安装方法 2.掌握svn的基本使用方法 2 svn介绍 2.1 项目管理中的版本控制问题 通常软件开发由多人协作开发,如果对 ...

  5. 使用hexdump追踪FAT32文件系统中的一个文件

    最近在看文件系统基础结构等知识,本来重点是想看EXT4文件系统,但是目前没有找到比较详细说明EXT4文件系统详细结构的,用EXT3的对应着找结果有点出入,在想是不是我用hexdump的参数有问题,于是 ...

  6. (转载)基于Bash命令行的百度云上传下载工具

    原文链接:http://hi.baidu.com/meoow/item/aef5814bbd5be3e1bcf451e9 这是我根据百度云PCS的API写的一个基于bash的命令行工具, 使用了cur ...

  7. 从零搭建DotnetCore2.0

    右键解决方案>新建项目> 选择Web>ASP.NETCoreWeb应用程序(.NET Core) 选择Web应用程序,暂时不选择启用Docker,身份验证选择个人用户账户(会自动生成 ...

  8. Arcgis Engine axMapControl1.get_layer(index)中index意义

    像 ILayer pLayer = this.axMapControl1.get_Layer(0); 意思是获取axMapControl1中的第一个图层,复制给pLayer.

  9. Javsssist用InsertAt()方法对语句插桩

    基于上一篇的方法插桩,这一篇则是进一步的对每行的语句进行插桩. 对于存在分支的方法(例如if(){}else{}),对方法插桩的方法是不能够全部涉及到的.所以要对程序的每条语句进行插桩. 插入什么语句 ...

  10. mysql数据库左联的使用(一张数据库表关联到另外一张数据库表)

    左联的数据库表,然后显示的在页面显示的jsp里面改一下代理种类ID的name,这样在页面上显示的不是id了,而是变成你修改了以后相对于的name了