zoukankan      html  css  js  c++  java
  • akka-typed(4)

      前面提到过,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("10","1013"),
        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("10","1013"),
        emptyState = CartLoad(),
        commandHandler = commandHandler,
        eventHandler = eventHandler
      ).onPersistFailure(
        SupervisorStrategy
        .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
        .withMaxRestarts(3)
        .withResetBackoffAfter(10.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("10", "1013"),
              emptyState = CartLoad(),
              commandHandler = commandHandler,
              eventHandler = eventHandler
            ).onPersistFailure(
              SupervisorStrategy
                .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
                .withMaxRestarts(3)
                .withResetBackoffAfter(10.seconds))
          }
        ).onFailure(
          SupervisorStrategy
            .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
            .withMaxRestarts(3)
            .withResetBackoffAfter(10.seconds)
        )

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

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

     def apply(): Behavior[Command] =
        Behaviors.supervise(
          Behaviors.setup[Command] { ctx =>
            EventSourcedBehavior[Command, Event, CartLoad](
              persistenceId = PersistenceId("10", "1013"),
              emptyState = CartLoad(),
              commandHandler = commandHandler,
              eventHandler = eventHandler
            ).onPersistFailure(
              SupervisorStrategy
                .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
                .withMaxRestarts(3)
                .withResetBackoffAfter(10.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 = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
            .withMaxRestarts(3)
            .withResetBackoffAfter(10.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("10", "1013"),
              emptyState = CartLoad(),
              commandHandler = commandHandler,
              eventHandler = eventHandler
            ).onPersistFailure(
              SupervisorStrategy
                .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
                .withMaxRestarts(3)
                .withResetBackoffAfter(10.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 = 100, keepNSnapshots = 2))
          }
        ).onFailure(
          SupervisorStrategy
            .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
            .withMaxRestarts(3)
            .withResetBackoffAfter(10.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("10", "1013"),
              emptyState = CartLoad(),
              commandHandler = commandHandler,
              eventHandler = eventHandler
            ).onPersistFailure(
              SupervisorStrategy
                .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
                .withMaxRestarts(3)
                .withResetBackoffAfter(10.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 = 100, keepNSnapshots = 2))
          }
        ).onFailure(
          SupervisorStrategy
            .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
            .withMaxRestarts(3)
            .withResetBackoffAfter(10.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()
    
    }
  • 相关阅读:
    改变UITabbar顶部分割线颜色
    UITableViewCell添加点击时改变字体的颜色、背景、图标
    【转】有了Auto Layout,为什么你还是害怕写UITabelView的自适应布局?
    AFNetworking https自签名证书 -1012 解决方案
    关于AFNetWorking 2.5.4之后版本编译报错问题解决方案
    UIImageView 使图片圆形的方法
    关于使用IQKeyBoardManager键盘还是被遮挡的问题解决方案
    关于ios7 以上版本 view被导航栏遮挡的问题 解决方案
    手动导入第三方工程/类库
    “请不要直接访问超全局$_GET数组”
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/13054219.html
Copyright © 2011-2022 走看看