zoukankan      html  css  js  c++  java
  • Akka-CQRS(8)- CQRS Reader Actor 应用实例

      前面我们已经讨论了CQRS-Reader-Actor的基本工作原理,现在是时候在之前那个POS例子里进行实际的应用示范了。

    假如我们有个业务系统也是在cassandra上的,那么reader就需要把从日志读出来的事件恢复成cassandra表里的数据行row。首先,我们需要在cassandra上创建相关的keyspace和table。下面是在scala中使用cassandra-java-driver的例子:

    import com.datastax.driver.core._
    import akka.actor.ActorSystem
    import akka.stream.ActorMaterializer
    import sdp.cql.engine._
    import CQLEngine._
    import CQLHelpers._
    import monix.execution.Scheduler.Implicits.global
    import scala.util._
    
    object CQLCreatTables extends App {
      //#init-mat
      implicit val cqlsys = ActorSystem("cqlSystem")
      implicit val mat = ActorMaterializer()
    //  implicit val ec = cqlsys.dispatcher
    
      val cluster = new Cluster
      .Builder()
        .addContactPoints("192.168.11.189")
        .withPort(9042)
        .build()
    
      useJava8DateTime(cluster)
      implicit val session = cluster.connect()
    
      val createKeyspace = """
      CREATE KEYSPACE pos_on_cloud WITH replication = {    //pos业务数据库
      'class': 'SimpleStrategy',
      'replication_factor': '3'
      }"""
    
      val createVchLog ="""
      CREATE TABLE pos_on_cloud.vch_log (       //销售单号日志 (可以从某日开始重新运算交易)
        terminal text,
        txndate text,
        vchnum int,
        begin_seq bigint,
        end_seq bigint,
        PRIMARY KEY (terminal,txndate,vchnum)
      )"""
    
      val createTxnItems ="""
      CREATE TABLE pos_on_cloud.txn_log (        //pos交易记录表
         terminal text,
         txndate text,
         txntime text,
         opr text,
         num int,
         seq int,
         txntype int,
         salestype int,
         qty int,
         price int,
         amount int,
         disc int,
         dscamt int,
         member text,
         code text,
         acct text,
         dpt  text,
         PRIMARY KEY (terminal,txndate,num,seq)
       )"""
    
      val createTxnSuspend ="""
      CREATE TABLE pos_on_cloud.txn_hold (      //临时挂单表
         terminal text,
         txndate text,
         txntime text,
         opr text,
         num int,
         seq int,
         txntype int,
         salestype int,
         qty int,
         price int,
         amount int,
         disc int
         dscamt int,
         member text,
         code text,
         acct text,
         dpt  text,
         PRIMARY KEY (terminal,txndate,num,seq)
       )"""
    
    
      val ctxKeyspace = CQLContext().setCommand(createKeyspace)
      val ctxVchlog = CQLContext().setCommand(createVchLog)
      val ctxTxnlog = CQLContext().setCommand(createTxnItems)
      val ctxTxnhold = CQLContext().setCommand(createTxnSuspend)
    
      val results = for {
         stsKeyspace <- cqlExecute(ctxKeyspace)
         stsVchlog <- cqlExecute(ctxVchlog)
         stsTxnlog <- cqlExecute(ctxTxnlog)
         stsTxnhold <- cqlExecute(ctxTxnhold)
      } yield (stsKeyspace,stsVchlog,stsTxnlog,stsTxnhold)
    
      val task = results.value.value
    
      val cancellableFut = task.runToFuture
      cancellableFut.onComplete {
        case Success(value) =>
          println(s"returned status: $value")
        case Failure(ex) =>
          System.err.println(s"ERROR: ${ex.getMessage}")
    
      }
    
    
    //  cancellableFut.cancel()
    /*
      val cancelable = task.runAsync { result =>
        result match {
          case Right(value) =>
            println(value)
          case Left(ex) =>
            System.err.println(s"ERROR: ${ex.getMessage}")
        }
      } */
    
      scala.io.StdIn.readLine()
    
      session.close()
      cluster.close()
    
      cqlsys.terminate()
    
    
    }
    

    这里面调用了之前PICE系列博文中设计的CassandraEngine里的工具源代码。下面是用CassandraEngine工具向cassandra表里插入数据的示范代码: 

    object DBWriter {
      def writeTxnsToDB(vchnum: Int, susp: Boolean, txns: List[TxnItem])(pid: String, bseq: Long, eseq: Long) = {
    
        import monix.execution.Scheduler.Implicits.global
    
        val cluster = new Cluster
        .Builder()
          .addContactPoints("192.168.11.189")
          .withPort(9042)
          .build()
    
        useJava8DateTime(cluster)
        implicit val session = cluster.connect()
        val insertVchLog = """
            |insert into pos_on_cloud.vch_log(terminal,txndate,vchnum,begin_seq,end_seq)
            |values(?,?,?,?,?)
            |""".stripMargin
    
        val insertTxns = """
           |insert into pos_on_cloud.txn_log(terminal,txndate,txntime,opr,num,seq,txntype,salestype,
           |qty,price,amount,disc,dscamt,member,code,acct,dpt)
           |values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                         """.stripMargin
    
        val insertSusp = """
           |insert into pos_on_cloud.txn_hold(terminal,txndate,txntime,opr,num,seq,txntype,salestype,
           |qty,price,amount,disc,dscamt,member,code,acct,dpt)
           |values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                         """.stripMargin
    
       val vchParams: Seq[Object] = Seq(
         pid.asInstanceOf[Object],
         LocalDate.now.format(DateTimeFormatter.ofPattern("yyyyMMdd")).asInstanceOf[Object],
         vchnum.asInstanceOf[Object],
         bseq.asInstanceOf[Object],
         eseq.asInstanceOf[Object]
       )
    
    
       val txnParams: Seq[Seq[Object]] = txns.foldRight(Seq[Seq[Object]]()) { (txnitem,b) =>
         (Seq(pid.asInstanceOf[Object]) ++ ccToList(txnitem)) +: b
       }
    
       val ctxVchlog = CQLContext().setCommand(insertVchLog, 1,vchParams)
       val ctxTxnlog = CQLContext().setCommand((if(susp) insertSusp else insertTxns),txnParams.size,txnParams)
    
    
        val results = for {
          stsVchlog <- cqlExecute(ctxVchlog)
          stsTxnlog <- cqlExecute(ctxTxnlog)
        } yield (stsTxnlog)
    
        val task = results.value.value
    
        val cancellableFut = task.runToFuture
        cancellableFut.onComplete {
          case Success(value) =>
            println(s"returned status: $value")
            session.close()
            cluster.close()
          case Failure(ex) =>
            System.err.println(s"ERROR: ${ex.getMessage}")
            session.close()
            cluster.close()
        }
        
      //  cqlsys.terminate()
    
      }
    
      def getMapFromCC(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
        .zip( cc.productIterator.to ).toMap // zipped with all values
    
      def ccFieldsToMap(cc: Product) = {
        val values = cc.productIterator
        cc.getClass.getDeclaredFields.map( _.getName -> (values.next).asInstanceOf[Object] ).toMap
      }
    
      def ccToList(cc: Product) = {
        val values = cc.productIterator
        cc.getClass.getDeclaredFields.map(_ => (values.next).asInstanceOf[Object] ).toList
      }
      def ccToMap(cc: Product): Map[String, Object] = {
        val values = cc.productIterator
        cc.getClass.getDeclaredFields.map {
          _.getName -> (values.next() match {
            case p: Product if p.productArity > 0 => ccToMap(p)
            case x => x.asInstanceOf[Object]
          })
        }.toMap
      }
    
    }
    

    用cqlsh: select * from txn_hold 检查了一下,插入数据正确。对于这种批量数据同类处理,可能用akka-stream会更方便高效: 

        val actionStreamVs = CassandraActionStream(insertVchLog,vsToParams)
          .setParallelism(2)
          .setProcessOrder(false)
        val actionFlowVs: Flow[Seq[Object],Seq[Object],NotUsed] = actionStreamVs.performOnRow
    
        val sinkVs = Sink.foreach[Seq[Object]]{ r =>
          log.step(s"insert: $insertVchLog, values: ${r}")
        }
        // insert to vch_log
        val stsVs = Source.fromIterator(() => Seq(vchParams).iterator).via(actionFlowVs).to(sinkVs).run()
    
        
        val insertTxn = if (susp) insertSusp else insertTxns
    
       val txnitemToParams: TxnItem => Seq[Object] = txn =>
         (Seq(pid.asInstanceOf[Object]) ++ ccToList(txn))
    
       val actionStreamTxn = CassandraActionStream(insertTxn,txnitemToParams)
          .setParallelism(2)
          .setProcessOrder(false)
       val  actionFlowTxn: Flow[TxnItem,TxnItem,NotUsed] = actionStreamTxn.performOnRow
    
       val sinkTxn = Sink.foreach[TxnItem]{ r =>
          log.step(s"insert: $insertTxn, values: ${r}")
        }
        // insert to txn_???
        val stsTxn = Source.fromIterator(() => txns.iterator).via(actionFlowTxn).to(sinkTxn).run()
    

    检查cassandra数据库表,结果正确。用stream方式来做重复类型的处理会比较方便,在当前这个例子的场合下建议使用。

    好了,完成了事件日志读取和转换成数据行格式并写入数据库表后,下一步就是建一个reader-actor负责完成这一轮工作。这个reader-actor只根据下面这个消息进行相关的工作:

     case class PerformRead(pid: String, vchnum: Int, bseq: Long, eseq: Long)
    

    这个消息描述了读端需要读取的日志记录范围和persistenceId。然后再加一个远程路由remote-router,负责按照某种算法来向各个集群节点上的reader-actor分发读端任务。下面是reader-actor: 

    package datatech.cloud.pos
    import akka.actor._
    import akka.cluster._
    import akka.pattern._
    import scala.concurrent.duration._
    import com.typesafe.config.ConfigFactory
    import sdp.logging.LogSupport
    import Messages._
    import Reader._
    
    object ActionReader {
      def actionReaderProps(trace: Boolean): Props = Props(new ActionReader(trace))
    
      //backoff suppervisor  must use onStop mode
      //respond only to failure of child
      def readerProps(trace:Boolean): Props = {
        val options = BackoffOpts.onFailure(
          childProps = actionReaderProps(trace),
          childName = "cqrs-reader",
          minBackoff = 1 second,
          maxBackoff = 10 seconds,
          randomFactor = 0.20
        ).withMaxNrOfRetries(3)
        BackoffSupervisor.props(options)
      }
    
      def create(port: Int): Unit = {
        val config = ConfigFactory.parseString(s"akka.remote.netty.tcp.port=$port")
          .withFallback(ConfigFactory.load())
    
        val system = ActorSystem("cloud-pos-server", config)
        system.actorOf(readerProps(true),"reader")
      }
    }
    
    class ActionReader(trace: Boolean) extends Actor with LogSupport {
      val cluster = Cluster(context.system)
      val host = Cluster(context.system).selfAddress.host.get
      implicit val nodeAddress: NodeAddress = NodeAddress(cluster.selfAddress.toString)
      val readerId = "ActionReader"
      log.stepOn = trace
    
      log.step(s"${nodeAddress.address}-${readerId}")
    
      override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        super.preRestart(reason, message)
        log.step(s"${nodeAddress.address}-${readerId} Restarting for $message ...")
      }
    
      override def postRestart(reason: Throwable): Unit = {
        super.postRestart(reason)
        log.step(s"${nodeAddress.address}-${readerId} restarted for ${reason.getMessage}.")
      }
    
      override def postStop(): Unit = {
        log.step(s"${nodeAddress.address}-${readerId} stooped.")
      }
    
      override def preStart(): Unit = {
        log.step(s"${nodeAddress.address}-${readerId} Starting ...")
      }
    
      var debugConfig: com.typesafe.config.Config = _
      var debug: Boolean = _
      try {
    
        debugConfig = ConfigFactory.load("pos.conf").getConfig("pos.server")
        debug = debugConfig.getBoolean("debug")
      }
      catch {
        case _ : Throwable => debug = false
      }
    
      log.step(s"${nodeAddress.address}-${readerId} debug mode = $debug")
    
      implicit val debugMode = DebugMode(debug)
    
      override def receive: Receive = {
        case PerformRead(pid, vchnum, bseq, eseq) =>
          log.step(s"${nodeAddress.address}-${readerId} PerformRead($pid, $vchnum, $bseq, $eseq)")
          readActions(host,bseq,eseq,pid,vchnum)(context.system,context.dispatcher,nodeAddress)
        case msg @ _ =>
          log.step(s"${nodeAddress.address}-${readerId} receive unsupported command:[$msg]")
      }
    
    }
    

    这是一个在backoffSupervisor后面的actor。remote-router是从配置文件定义创建的:

    akka.actor.deployment {
      /readerRouter/readerRouter = {
        # Router type provided by metrics extension.
        router = cluster-metrics-adaptive-group
        # Router parameter specific for metrics extension.
        # metrics-selector = heap
        # metrics-selector = load
        # metrics-selector = cpu
        metrics-selector = mix
        #
        routees.paths = ["/user/reader"]
        cluster {
          max-nr-of-instances-per-node = 10
          max-total-nr-of-instances = 1000
          enabled = on
          allow-local-routees = on
        }
      }
    }
    

    ReaderRouter的代码如下: 

    package datatech.cloud.pos
    import akka.actor._
    import akka.routing._
    import akka.cluster._
    import com.typesafe.config.ConfigFactory
    
    class ReaderRouter extends Actor {
      val router = context.actorOf(FromConfig.props(), name = "readerRouter")
    
      def receive: Receive = {
        case msg => router ! msg
      }
    }
    
    object ReaderRouter {
      var router: ActorRef = _
      def props = Props(new ReaderRouter)
    
      def create(port: Int) = {
        val config = ConfigFactory.parseString(s"akka.remote.netty.tcp.port=$port")
          .withFallback(ConfigFactory.load())
    
        val system = ActorSystem("cloud-pos-server",config)
    
        Cluster(system).registerOnMemberUp{
          router = system.actorOf(props,"readerRouter")
        }
    
      }
    
      def getRouter = router
    }
    

    好了,可以写个例子来测试运行这个router/routee。reader-actor所做的工作在前面的讨论里已经测试过了。 

    ackage datatech.cloud.pos
    import akka.actor._
    
    import datatech.cloud.pos.Messages.PerformRead
    object ReaderDemo extends App {
      ActionReader.create(2551)
    
      ActionReader.create(2552)
    
      ActionReader.create(2553)
    
      ReaderRouter.create(2558)
    
      scala.io.StdIn.readLine()
    
      val router = ReaderRouter.getRouter
    
      router ! PerformRead("1022",111,0,Long.MaxValue)
    
      scala.io.StdIn.readLine()
    
      router ! PerformRead("1022",222,0,Long.MaxValue)
    
      scala.io.StdIn.readLine()
    
    }
    

    在这个例子里我们先在本机的2551,2552,2553端口上部署了routees, 即reader-actor。然后在2558端口部署router,再向router发送任务PerformRead。这里有些东西值得留意:akka-cluster使用了netty,而netty也需要占用一个端口。在配置文件里: 

      remote {
        log-remote-lifecycle-events = on
        netty.tcp {
          hostname = "192.168.11.189"
    # port set to 0 for netty to randomly choose from      
          port = 0
        }
      }
    

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

    project/plugin.sbt

    addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "0.6.1")
    addSbtPlugin("com.lightbend.sbt" % "sbt-javaagent" % "0.1.4") // ALPN agent
    

    build.sbt

    name := "akka-cluster-reader"
    
    version := "0.1"
    
    scalaVersion := "2.12.8"
    
    scalacOptions += "-Ypartial-unification"
    
    // in build.sbt:
    //enablePlugins(AkkaGrpcPlugin)
    // ALPN agent
    //enablePlugins(JavaAgent)
    //javaAgents += "org.mortbay.jetty.alpn" % "jetty-alpn-agent" % "2.0.9" % "runtime;test"
    
    libraryDependencies := Seq(
      "com.typesafe.akka" %% "akka-cluster-metrics" % "2.5.19",
      "com.typesafe.akka" %% "akka-cluster-sharding" % "2.5.19",
      "com.typesafe.akka" %% "akka-persistence" % "2.5.19",
      "com.lightbend.akka" %% "akka-stream-alpakka-cassandra" % "1.0.1",
      "org.mongodb.scala" %% "mongo-scala-driver" % "2.6.0",
      "com.lightbend.akka" %% "akka-stream-alpakka-mongodb" % "1.0.1",
      "com.typesafe.akka" %% "akka-persistence-query" % "2.5.19",
      "com.typesafe.akka" %% "akka-persistence-cassandra" % "0.93",
      "com.typesafe.akka" %% "akka-persistence-cassandra-launcher" % "0.93" % Test,
      "com.datastax.cassandra" % "cassandra-driver-core" % "3.6.0",
      "com.datastax.cassandra" % "cassandra-driver-extras" % "3.6.0",
      "ch.qos.logback"  %  "logback-classic"   % "1.2.3",
      "io.monix" %% "monix" % "3.0.0-RC2",
      "org.typelevel" %% "cats-core" % "2.0.0-M1",
      "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf",
     "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion
    
    )
    
    // (optional) If you need scalapb/scalapb.proto or anything from
    // google/protobuf/*.proto
    //libraryDependencies += "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf"
    
    
    PB.targets in Compile := Seq(
      scalapb.gen() -> (sourceManaged in Compile).value
    )
    

    resources/application.conf

    akka.actor.warn-about-java-serializer-usage = off
    akka.log-dead-letters-during-shutdown = off
    akka.log-dead-letters = off
    akka.remote.use-passive-connections=off
    
    akka {
      loglevel = INFO
      actor {
        provider = "cluster"
      }
    
      remote {
        log-remote-lifecycle-events = on
        netty.tcp {
          hostname = "192.168.11.189"
    # port set to 0 for netty to randomly choose from
          port = 0
        }
      }
    
      cluster {
        seed-nodes = [
          "akka.tcp://cloud-pos-server@192.168.11.189:2551"]
        log-info = off
        sharding {
          role = "shard"
          passivate-idle-entity-after = 10 m
        }
      }
    
      persistence {
        journal.plugin = "cassandra-journal"
        snapshot-store.plugin = "cassandra-snapshot-store"
      }
    
    }
    
    cassandra-journal {
      contact-points = ["192.168.11.189"]
    }
    
    cassandra-snapshot-store {
      contact-points = ["192.168.11.189"]
    }
    
    # Enable metrics extension in akka-cluster-metrics.
    akka.extensions=["akka.cluster.metrics.ClusterMetricsExtension"]
    
    akka.actor.deployment {
      /readerRouter/readerRouter = {
        # Router type provided by metrics extension.
        router = cluster-metrics-adaptive-group
        # Router parameter specific for metrics extension.
        # metrics-selector = heap
        # metrics-selector = load
        # metrics-selector = cpu
        metrics-selector = mix
        #
        routees.paths = ["/user/reader"]
        cluster {
          max-nr-of-instances-per-node = 10
          max-total-nr-of-instances = 1000
          enabled = on
          #default in reference.conf
          #allow-local-routees = on
          #very important to set this off, could cause missing msg in local cluster
          allow-local-routees = off
        }
      }
    }
    
    dbwork-dispatcher {
      # Dispatcher is the name of the event-based dispatcher
      type = Dispatcher
      # What kind of ExecutionService to use
      executor = "fork-join-executor"
      # Configuration for the fork join pool
      fork-join-executor {
        # Min number of threads to cap factor-based parallelism number to
        parallelism-min = 2
        # Parallelism (threads) ... ceil(available processors * factor)
        parallelism-factor = 2.0
        # Max number of threads to cap factor-based parallelism number to
        parallelism-max = 10
      }
      # Throughput defines the maximum number of messages to be
      # processed per actor before the thread jumps to the next actor.
      # Set to 1 for as fair as possible.
      throughput = 100
    }
    

     logback.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <Pattern>
                    %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
                </Pattern>
            </encoder>
        </appender>
    
        <logger name="datatech" level="info"
                additivity="false">
            <appender-ref ref="STDOUT" />
        </logger>
    
        <logger name="sdp" level="info"
                additivity="false">
            <appender-ref ref="STDOUT" />
        </logger>
    
        <root level="warn">
            <appender-ref ref="STDOUT" />
        </root>
    </configuration>

    ActionReader.scala

    package datatech.cloud.pos
    import akka.actor._
    import akka.cluster._
    import akka.pattern._
    import scala.concurrent.duration._
    import com.typesafe.config.ConfigFactory
    import sdp.logging.LogSupport
    import Messages._
    import Reader._
    
    
    
    object ActionReader {
      def actionReaderProps(trace: Boolean): Props = Props(new ActionReader(trace))
    
      //backoff suppervisor  must use onStop mode
      //respond only to failure of child
      def readerProps(trace:Boolean): Props = {
        val options = BackoffOpts.onFailure(
          childProps = actionReaderProps(trace),
          childName = "cqrs-reader",
          minBackoff = 1 second,
          maxBackoff = 10 seconds,
          randomFactor = 0.20
        ).withMaxNrOfRetries(3)
        BackoffSupervisor.props(options)
      }
    
      def create(port: Int): Unit = {
        val config = ConfigFactory.parseString(s"akka.remote.netty.tcp.port=$port")
          .withFallback(ConfigFactory.load())
    
        val system = ActorSystem("cloud-pos-server", config)
        system.actorOf(readerProps(true),"reader")
      }
    
    }
    
    class ActionReader(trace: Boolean) extends Actor with LogSupport {
      val cluster = Cluster(context.system)
      val host = Cluster(context.system).selfAddress.host.get
      implicit val nodeAddress: NodeAddress = NodeAddress(cluster.selfAddress.toString)
      val readerId = "ActionReader"
      log.stepOn = trace
    
      log.step(s"${nodeAddress.address}-${readerId}")
    
      override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        super.preRestart(reason, message)
        log.step(s"${nodeAddress.address}-${readerId} Restarting for $message ...")
      }
    
      override def postRestart(reason: Throwable): Unit = {
        super.postRestart(reason)
        log.step(s"${nodeAddress.address}-${readerId} restarted for ${reason.getMessage}.")
      }
    
      override def postStop(): Unit = {
        log.step(s"${nodeAddress.address}-${readerId} stooped.")
      }
    
      override def preStart(): Unit = {
        log.step(s"${nodeAddress.address}-${readerId} Starting ...")
      }
    
      var debugConfig: com.typesafe.config.Config = _
      var debug: Boolean = _
      try {
    
        debugConfig = ConfigFactory.load("pos.conf").getConfig("pos.server")
        debug = debugConfig.getBoolean("debug")
      }
      catch {
        case _ : Throwable => debug = false
      }
    
      log.step(s"${nodeAddress.address}-${readerId} debug mode = $debug")
    
      implicit val debugMode = DebugMode(debug)
    
      override def receive: Receive = {
        case PerformRead(pid, vchnum, bseq, eseq) =>
          log.step(s"${nodeAddress.address}-${readerId} PerformRead($pid, $vchnum, $bseq, $eseq)")
          readActions(host,bseq,eseq,pid,vchnum)(context.system,context.dispatcher,nodeAddress)
        case msg @ _ =>
          log.step(s"${nodeAddress.address}-${readerId} receive unsupported command:[$msg]")
      }
    
    }
    

    ReaderRouter.scala

    package datatech.cloud.pos
    import akka.actor._
    import akka.routing._
    import akka.cluster._
    import com.typesafe.config.ConfigFactory
    
    
    class ReaderRouter extends Actor {
    
      val router = context.actorOf(FromConfig.props(), name = "readerRouter")
    
      def receive: Receive = {
        case msg => router ! msg
      }
    
    }
    
    object ReaderRouter {
     var router: ActorRef = _
    
      def props = Props(new ReaderRouter)
    
      def create(port: Int) = {
        val config = ConfigFactory.parseString(s"akka.remote.netty.tcp.port=$port")
          .withFallback(ConfigFactory.load())
    
        val system = ActorSystem("cloud-pos-server",config)
    
        Cluster(system).registerOnMemberUp{
          router = system.actorOf(props,"readerRouter")
        }
    
      }
      def getRouter = router
    }
    

    Reader.scala

    package datatech.cloud.pos
    import akka.actor._
    import akka.stream.scaladsl._
    
    import scala.util._
    import akka._
    import akka.persistence.query._
    import akka.persistence.cassandra.query.scaladsl.CassandraReadJournal
    
    import scala.concurrent._
    import akka.stream._
    import sdp.logging._
    import Actions._
    import States._
    import Messages._
    import akka.cluster._
    import DBWriter._
    
    object Reader extends LogSupport {
    
      def readActions(cqlhost: String, startSeq: Long, endSeq: Long, persistenceId: String, vchnum: Int)(implicit sys: ActorSystem, ec: ExecutionContextExecutor, nodeAddress: NodeAddress) = {
        implicit var vchState = VchStates().copy(num = vchnum)
        implicit var vchItems = VchItems()
        implicit var curTxnItem = TxnItem()
        implicit val pid = PID(persistenceId)
        implicit val mat = ActorMaterializer()
    
    
        val readerId = "ActionReader"
    
        // obtain read journal by plugin id
        val readJournal =
          PersistenceQuery(sys).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier)
    
        // issue query to journal
        val source: Source[EventEnvelope, NotUsed] =
          readJournal.currentEventsByPersistenceId(persistenceId, startSeq, endSeq)
    
        // materialize stream, consuming events
        val futureActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny }
    
        futureActions.onComplete {
          case Success(txns) =>
            log.step(s"${nodeAddress.address}-${readerId} recovered actions: $txns")
            buildVoucher(txns)
          case Failure(excpt) =>
            log.error(s"${nodeAddress.address}-${readerId} read actions error: ${excpt.getMessage}")
    
        }
    
        def buildVoucher(actions: List[Any])= {
          actions.reverse.foreach { txn =>
            txn match {
              case EndVoucher(_) =>
                 writeTxnsToDB(cqlhost,vchState.num,vchState.susp,vchItems.txnitems)(persistenceId,startSeq,endSeq)
                mat.shutdown()
              case ti@_ =>
                curTxnItem = buildTxnItem(ti.asInstanceOf[Action])
                val sts = updateState(ti.asInstanceOf[Action],0)
                vchState = sts._1
                vchItems = sts._2
            }
          }
        }
      }
    
    }
    

    DBWriter.scala

    package datatech.cloud.pos
    import java.time.LocalDate
    import java.time.format.DateTimeFormatter
    import sdp.logging._
    import Messages._
    import com.datastax.driver.core._
    import akka.actor.ActorSystem
    import akka.stream.ActorMaterializer
    import sdp.cql.engine._
    import CQLEngine._
    import CQLHelpers._
    import com.typesafe.config._
    import akka.stream.scaladsl._
    import akka._
    import scala.concurrent._
    
    
    object DBWriter extends LogSupport {
      var posConfig: com.typesafe.config.Config = _
      def writeTxnsToDB(cqlhost: String, vchnum: Int, susp: Boolean, txns: List[TxnItem])(pid: String, bseq: Long, eseq: Long)(
                       implicit sys: ActorSystem, ec: ExecutionContextExecutor, mat: ActorMaterializer, nodeAddress: NodeAddress) = {
    
        val readerId = "DBWriter"
        var cqlport: Int = 9042
        try {
          posConfig = ConfigFactory.load("pos.conf").getConfig("pos.cqlport")
          cqlport = posConfig.getInt("cqlport")
        }
        catch {
          case _ : Throwable => cqlport = 9042
        }
    
        val cluster = new Cluster
        .Builder()
          .addContactPoints(cqlhost)
          .withPort(cqlport)
          .build()
    
        useJava8DateTime(cluster)
        implicit val session = cluster.connect()
        val insertVchLog = """
            |insert into pos_on_cloud.vch_log(
            |terminal,
            |txndate,
            |vchnum,
            |begin_seq,
            |end_seq)
            |values(?,?,?,?,?)
            |""".stripMargin
    
        val insertTxns = """
           |insert into pos_on_cloud.txn_log(
           |terminal,
           |txndate,
           |txntime,
           |opr,
           |num,
           |seq,
           |txntype,
           |salestype,
           |qty,
           |price,
           |amount,
           |disc,
           |dscamt,
           |member,
           |code,
           |acct,
           |dpt)
           |values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                         """.stripMargin
    
        val insertSusp = """
           |insert into pos_on_cloud.txn_hold(
           |terminal,
           |txndate,
           |txntime,
           |opr,
           |num,
           |seq,
           |txntype,
           |salestype,
           |qty,
           |price,
           |amount,
           |disc,
           |dscamt,
           |member,
           |code,
           |acct,
           |dpt)
           |values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
                         """.stripMargin
    
       val vchParams: Seq[Object] = Seq(
         pid.asInstanceOf[Object],
         LocalDate.now.format(DateTimeFormatter.ofPattern("yyyyMMdd")).asInstanceOf[Object],
         vchnum.asInstanceOf[Object],
         bseq.asInstanceOf[Object],
         eseq.asInstanceOf[Object]
       )
    
        val vsToParams: Seq[Object] => Seq[Object] = vchParams => vchParams
    
        val actionStreamVs = CassandraActionStream(insertVchLog,vsToParams)
          .setParallelism(2)
          .setProcessOrder(false)
        val actionFlowVs: Flow[Seq[Object],Seq[Object],NotUsed] = actionStreamVs.performOnRow
    
        val sinkVs = Sink.foreach[Seq[Object]]{ r =>
          log.step(s"${nodeAddress.address}-${readerId} insert: $insertVchLog, values: ${r}")
        }
        // insert to vch_log
        val stsVs = Source.fromIterator(() => Seq(vchParams).iterator).via(actionFlowVs).to(sinkVs).run()
    
        val insertTxn = if (susp) insertSusp else insertTxns
    
       val txnitemToParams: TxnItem => Seq[Object] = txn =>
         (Seq(pid.asInstanceOf[Object]) ++ ccToList(txn))
    
       val actionStreamTxn = CassandraActionStream(insertTxn,txnitemToParams)
          .setParallelism(2)
          .setProcessOrder(false)
       val  actionFlowTxn: Flow[TxnItem,TxnItem,NotUsed] = actionStreamTxn.performOnRow
    
       val sinkTxn = Sink.foreach[TxnItem]{ r =>
         log.step(s"${nodeAddress.address}-${readerId} insert: $insertTxn, values: ${r}")
        }
        // insert to txn_???
        val stsTxn = Source.fromIterator(() => txns.iterator).via(actionFlowTxn).to(sinkTxn).run()
      }
    
      def ccToList(cc: Product) = {
        val values = cc.productIterator
        cc.getClass.getDeclaredFields.map(_ => (values.next).asInstanceOf[Object] ).toList
      }
    
    }
    

    ReaderDemo.scala

    package datatech.cloud.pos
    import akka.actor._
    
    import datatech.cloud.pos.Messages.PerformRead
    object ReaderDemo extends App {
      ActionReader.create(2551)
    
      ActionReader.create(2552)
    
      ActionReader.create(2553)
    
    
      ReaderRouter.create(2558)
    
      scala.io.StdIn.readLine()
    
      val router = ReaderRouter.getRouter
    
      router ! PerformRead("1022",111,0,Long.MaxValue)
    
      scala.io.StdIn.readLine()
    
      router ! PerformRead("1022",222,0,Long.MaxValue)
    
      scala.io.StdIn.readLine()
    
    
    }
    

    States.scala

    package datatech.cloud.pos
    import java.time.LocalDate
    import java.time.LocalDateTime
    import java.time.format.DateTimeFormatter
    
    
    import Messages._
    import sdp.logging._
    
    object Actions {
    
    
      implicit class FoldLeftWhile[A](trav: Seq[A]) {
        def foldLeftWhile[B](z: B)(op: ((B,Boolean), A) => (B, Boolean)): B = {
          def go(acc: (B, Boolean), l: Seq[A]): (B, Boolean) = l match {
            case h +: t =>
              val nacc = op(acc, h)
              if (!nacc._2)
                go(nacc, t)
              else
                nacc
            case _ => acc
          }
          go((z, false), trav)._1
        }
      }
    
    
      case class ReadActions(startSeq: Int, endSeq: Int, persistenceId: String)
    
      sealed trait Action {}
      case class LogOned(opr: String) extends Action
      case object LogOffed extends Action
      case class SuperOned(su: String) extends Action
      case object SuperOffed extends Action
      case class MemberOned(cardnum: String) extends Action
      case object MemberOffed extends Action   //remove member status for the voucher
      case object RefundOned extends Action
      case object RefundOffed extends Action
      case object VoidOned extends Action
      case object VoidOffed extends Action
    
    
      case class SalesLogged(acct: String, dpt: String, code: String, qty: Int, price: Int) extends Action
      case class Subtotaled(level: Int) extends Action
      case class Discounted(disctype: Int, grouped: Boolean, code: String, percent: Int) extends Action
    
      case class NewVoucher(vnum: Int) extends Action //新单, reminder for read-side to set new vnum
      case class EndVoucher(vnum: Int) extends Action //单据终结标示
      case object VoidVoucher extends Action
    
    
      case object SuspVoucher extends Action
    
      case class VoucherNumed(fnum: Int, tnum: Int) extends Action
    
      case class PaymentMade(acct: String, num: String, amount: Int) extends Action          //settlement   结算支付
    
    }
    
    
    object States extends LogSupport {
      import Actions._
    
      def setShowSteps(b: Boolean) = log.stepOn = b
    
      def buildTxnItem(evt: Action)(implicit vs: VchStates, vi: VchItems): TxnItem = evt match {
        case LogOned(op) => TxnItem(vs).copy(
          txntype = TXNTYPE.logon,
          salestype = SALESTYPE.crd,
          opr = op,
          code = op
        )
        case LogOffed => TxnItem(vs).copy(
          txntype = TXNTYPE.logon,
          salestype = SALESTYPE.crd
        )
        case SuperOned(su) => TxnItem(vs).copy(
          txntype = TXNTYPE.supon,
          salestype = SALESTYPE.crd,
          code = su
        )
        case SuperOffed => TxnItem(vs).copy(
          txntype = TXNTYPE.supon,
          salestype = SALESTYPE.crd
        )
        case MemberOned(cardnum) => TxnItem(vs).copy(
          txntype = TXNTYPE.sales,
          salestype = SALESTYPE.crd,
          member = cardnum
        )
        case MemberOffed => TxnItem(vs).copy(
          txntype = TXNTYPE.sales,
          salestype = SALESTYPE.crd
        )
        case RefundOned => TxnItem(vs).copy(
          txntype = TXNTYPE.refund
        )
        case RefundOffed => TxnItem(vs).copy(
          txntype = TXNTYPE.refund
        )
        case VoidOned => TxnItem(vs).copy(
          txntype = TXNTYPE.void
        )
        case VoidOffed => TxnItem(vs).copy(
          txntype = TXNTYPE.void
        )
        case VoidVoucher => TxnItem(vs).copy(
          txntype = TXNTYPE.voidall,
          code = vs.num.toString,
          acct = vs.num.toString
        )
        case SuspVoucher => TxnItem(vs).copy(
          txntype = TXNTYPE.suspend,
          code = vs.num.toString,
          acct = vs.num.toString
        )
        case Subtotaled(level) =>
          TxnItem(vs).copy(
            txntype = TXNTYPE.sales,
            salestype = SALESTYPE.sub
          )
        case Discounted(dt,gp,code,pct) => TxnItem(vs).copy(
          txntype = TXNTYPE.sales,
          salestype = SALESTYPE.dsc,
          acct = code,
          disc = pct
        )
        case PaymentMade(act,num,amt) => TxnItem(vs).copy(
          txntype = TXNTYPE.sales,
          salestype = SALESTYPE.ttl,
          acct = act,
          code = num,
          amount = amt
        )
    
        case SalesLogged(sacct,sdpt,scode,sqty,sprice)  => TxnItem(vs).copy(
          txntype = TXNTYPE.sales,
          salestype = SALESTYPE.itm,
          acct = sacct,
          dpt = sdpt,
          code = scode,
          qty = sqty,
          price = sprice,
          amount = sprice * sqty,
          dscamt = 0
        )
        case _ => TxnItem(vs)
      }
    
      case class VchItems(txnitems: List[TxnItem] = Nil) {
    
        def noSales: Boolean = (txnitems.find(txn => txn.salestype == SALESTYPE.itm)).isEmpty
    
        def subTotal: (Int, Int, Int, Int) = txnitems.foldRight((0, 0, 0, 0)) { case (txn, b) =>
          if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
            b.copy(_1 = b._1 + 1, _2 = b._2 + txn.qty, _3 = b._3 + txn.amount, _4 = b._4 + txn.dscamt)
          else b
        }
    
        def groupTotal(level:Int): (Int, Int, Int, Int) = {
          val gts = txnitems.foldLeftWhile((0, 0, 0, 0, 0)) { case (b,txn) =>
            if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
              ((b._1._1 +1,b._1._2 + txn.qty, b._1._3 + txn.amount, b._1._4 + txn.dscamt, b._1._5),false)
            else {
              if (txn.salestype == SALESTYPE.sub) {
                if (((b._1._5) + 1) >= level)
                  ((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), true)
                else
                  ((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), false)
              } else b
            }
          }
          (gts._1,gts._2,gts._3,gts._4)
        }
    
        def updateDisc(dt: Int, grouped: Boolean, disc: Int): (List[TxnItem],(Int,Int,Int,Int)) = {
          //(salestype,(cnt,qty,amt,dsc),hassub,list)
          val accu = txnitems.foldLeft((-1, (0,0,0,0), false, List[TxnItem]())) { case (b, txn) =>
            var discAmt = 0
            if ((b._1) < 0) {
              if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales) {
                if (txn.dscamt == 0)
                  ((txn.salestype, (
                    (b._2._1) + 1,
                    (b._2._2) + txn.qty,
                    (b._2._3) + txn.amount,
                    (b._2._4) - (txn.amount * disc / 100)
                    ), false, txn.copy(
                    dscamt = - (txn.amount * disc / 100)) :: (b._4)))
                else {
                  dt match {
                    case DISCTYPE.duplicated =>
                      if (txn.dscamt != 0) {
                        ((txn.salestype, (
                          (b._2._1) + 1,
                          (b._2._2) + txn.qty,
                          (b._2._3) + txn.amount,
                          (b._2._4) - (txn.amount + txn.dscamt) * disc / 100
                          ), false, txn.copy(
                          dscamt = -(txn.amount + txn.dscamt) * disc / 100) :: (b._4)
                        ))
                      } else {
                        ((txn.salestype, (
                          (b._2._1) + 1,
                          (b._2._2) + txn.qty,
                          (b._2._3) + txn.amount,
                          (b._2._4) - txn.amount * disc / 100
                          ), false, txn.copy(
                          dscamt = -txn.amount * disc / 100) :: (b._4)
                        ))
                      }
                    case DISCTYPE.keep => ((txn.salestype, (
                      (b._2._1) + 1,
                      (b._2._2) + txn.qty,
                      (b._2._3) + txn.amount,
                      (b._2._4) + txn.dscamt), false, txn :: (b._4)))
                    case DISCTYPE.best =>
                      discAmt = -(txn.amount * disc / 100)
                      if (discAmt < txn.dscamt)
                        ((txn.salestype, (
                          (b._2._1) + 1,
                          (b._2._2) + txn.qty,
                          (b._2._3) + txn.amount,
                          (b._2._4) + discAmt), false, txn.copy(
                          dscamt = discAmt
                        ) :: (b._4)))
                      else
                        ((txn.salestype, (
                          (b._2._1) + 1,
                          (b._2._2) + txn.qty,
                          (b._2._3) + txn.amount,
                          (b._2._4) + txn.dscamt), false, txn :: (b._4)))
                  }
                }
    
              } else ((b._1,b._2,b._3,txn :: (b._4)))
            } else {
              if ((b._3))
                (((b._1), (b._2), true, txn :: (b._4)))
              else {
                if (txn.salestype == SALESTYPE.sub) {
                  if (grouped)
                    (((b._1), (b._2), true, txn :: (b._4)))
                  else
                    (((b._1), (b._2), false, txn :: (b._4)))
                } else {
                  if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales) {
                    dt match {
                      case DISCTYPE.duplicated =>
                        if (txn.dscamt != 0) {
                          ((txn.salestype, (
                            (b._2._1) + 1,
                            (b._2._2) + txn.qty,
                            (b._2._3) + txn.amount,
                            (b._2._4) - (txn.amount + txn.dscamt) * disc / 100), false, txn.copy(
                            dscamt = -(txn.amount + txn.dscamt) * disc / 100) :: (b._4)
                          ))
                        } else {
                          ((txn.salestype, (
                            (b._2._1) + 1,
                            (b._2._2) + txn.qty,
                            (b._2._3) + txn.amount,
                            (b._2._4) - txn.amount * disc / 100), false, txn.copy(
                            dscamt = -(txn.amount * disc / 100)) :: (b._4)
                          ))
                        }
                      case DISCTYPE.keep => ((txn.salestype, (
                        (b._2._1) + 1,
                        (b._2._2) + txn.qty,
                        (b._2._3) + txn.amount,
                        (b._2._4) + txn.dscamt), false, txn :: (b._4)))
                      case DISCTYPE.best =>
                        discAmt = -(txn.amount * disc / 100)
                        if (discAmt < txn.dscamt)
                          ((txn.salestype, (
                            (b._2._1) + 1,
                            (b._2._2) + txn.qty,
                            (b._2._3) + txn.amount,
                            (b._2._4) + discAmt), false, txn.copy(
                            dscamt = discAmt
                          ) :: (b._4)))
                        else
                          ((txn.salestype, (
                            (b._2._1) + 1,
                            (b._2._2) + txn.qty,
                            (b._2._3) + txn.amount,
                            (b._2._4) + txn.dscamt), false, txn :: (b._4)))
                    }
                  }
                  else ((b._1, b._2, b._3, txn :: (b._4)))
                }
              }
            }
    
          }
          (accu._4.reverse,accu._2)
        }
    
        def totalSales: Int = txnitems.foldRight(0) { case (txn, b) =>
          if (txn.salestype == SALESTYPE.itm)
            (txn.amount + txn.dscamt) + b
          else b
    
          /*
                val amt: Int = txn.salestype match {
                  case (SALESTYPE.plu | SALESTYPE.cat | SALESTYPE.brd | SALESTYPE.ra) => txn.amount + txn.dscamt
                  case _ => 0
                }
                amt + b */
        }
    
        def totalPaid: Int = txnitems.foldRight(0) { case (txn, b) =>
          if (txn.txntype == TXNTYPE.sales && txn.salestype == SALESTYPE.ttl)
            txn.amount + b
          else b
        }
    
        def addItem(item: TxnItem): VchItems = VchItems((item :: txnitems)) //.reverse)
    
      }
    
      def LastSecOfDate(ldate: LocalDate): LocalDateTime = {
        val dtStr = ldate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 23:59:59"
        LocalDateTime.parse(dtStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
      }
    
      def dateStr(dt: LocalDate): String = dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
    
      def updateState(evt: Action, lastSeqNr: BigInt = 0)(implicit nodeAddress: NodeAddress, persistenceId: PID, state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = {
        val (vs, vi) = updateStateImpl(evt, lastSeqNr)
        log.step(s"${nodeAddress.address}-${persistenceId.id} run updateState($evt, $lastSeqNr) with results state[$vs], txns[$vi].")
        (vs, vi)
      }
    
      def updateStateImpl(evt: Action, lastSeqNr: BigInt = 0)(implicit state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = evt match {
        case LogOned(csr) => (state.copy(seq = state.seq + 1, opr = csr, jseq = lastSeqNr), items)
        case LogOffed => (state.copy(seq = state.seq + 1, opr = ""), items)
        case RefundOned => (state.copy(seq = state.seq + 1, refd = true), items)
        case RefundOffed => (state.copy(seq = state.seq + 1, refd = false), items)
        case VoidOned => (state.copy(seq = state.seq + 1, void = true), items)
        case VoidOffed => (state.copy(seq = state.seq + 1, void = false), items)
        case SuperOned(suser) => (state.copy(seq = state.seq + 1, su = suser), items)
        case SuperOffed => (state.copy(seq = state.seq + 1, su = ""), items)
        case MemberOned(num) => (state.copy(seq = state.seq + 1, mbr = num), items)
        case MemberOffed => (state.copy(seq = state.seq + 1, mbr = ""), items)
    
    
        case SalesLogged(_,_,_,_,_) => (state.copy(
          seq = state.seq + 1)
          , items.addItem(curItem))
    
        case Subtotaled(level) =>
          var subs = (0,0,0,0)
          if (level == 0)
            subs = items.subTotal
          else
            subs = items.groupTotal(level)
          val (cnt, tqty, tamt, tdsc) = subs
    
          val subttlItem =
            TxnItem(state).copy(
              txntype = TXNTYPE.sales,
              salestype = SALESTYPE.sub,
              qty = tqty,
              amount = tamt,
              dscamt = tdsc,
              price = cnt
            )
          (state.copy(
          seq = state.seq + 1)
          , items.addItem(subttlItem))
    
        case Discounted(dt,gp,code,pct) =>
          val (lstItems, accum) = items.updateDisc(dt,gp,pct)
          val discItem = TxnItem(state).copy(
            txntype = TXNTYPE.sales,
            salestype = SALESTYPE.dsc,
            acct = code,
            disc = pct,
            price = accum._1,
            qty = accum._2,
            amount = accum._3,
            dscamt = accum._4
          )
          (state.copy(
          seq = state.seq + 1)
          , items.copy(txnitems = lstItems).addItem(discItem))
    
    
        case PaymentMade(_,_,_) =>
          val due = if (items.totalSales > 0) items.totalSales - items.totalPaid else items.totalSales + items.totalPaid
          val bal = if (items.totalSales > 0) due - curItem.amount else due + curItem.amount
          (state.copy(
            seq = state.seq + 1,
            due = (if ((curItem.amount.abs + items.totalPaid.abs) >= items.totalSales.abs) false else true)
          )
            ,items.addItem(curItem.copy(
            salestype = SALESTYPE.ttl,
            price = due,
            amount = curItem.amount,
            dscamt = bal
          )))
    
        case VoucherNumed(_, tnum) =>
          val vi = items.copy(txnitems = items.txnitems.map { it => it.copy(num = tnum) })
          (state.copy(seq = state.seq + 1, num = tnum), vi)
    
        case SuspVoucher => (state.copy(seq = state.seq + 1, susp = true), items)
    
        case VoidVoucher => (state.copy(seq = state.seq + 1, canc = true), items)
    
        case EndVoucher(vnum) => (state.nextVoucher.copy(jseq = lastSeqNr + 1), VchItems())
    
        case _ => (state, items)
      }
    
    
    }
    

    Messages.scala

    package datatech.cloud.pos
    
    import java.time.LocalDate
    import java.time.LocalDateTime
    import java.time.format.DateTimeFormatter
    import java.util.Locale
    import akka.cluster.sharding._
    
    object Messages {
    
      val  dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
    
      sealed trait Command {}
    
      case class LogOn(opr: String, passwd: String) extends Command
      case object LogOff extends Command
      case class SuperOn(su: String, passwd: String) extends Command
      case object SuperOff extends Command
      case class MemberOn(cardnum: String, passwd: String) extends Command
      case object MemberOff extends Command   //remove member status for the voucher
      case object RefundOn extends Command
      case object RefundOff extends Command
      case object VoidOn extends Command
      case object VoidOff extends Command
      case object VoidAll extends Command
      case object Suspend extends Command
    
      case class VoucherNum(vnum: Int) extends Command
    
    
    
      case class LogSales(acct: String, dpt: String, code: String, qty: Int, price: Int) extends Command
      case class Subtotal(level: Int) extends Command
      case class Discount(disctype: Int, grouped: Boolean, code: String, percent: Int) extends Command
    
      case class Payment(acct: String, num: String, amount: Int) extends Command          //settlement   结算支付
    
      // read only command, no update event
      case class Plu(itemCode: String) extends Command  //read only
      case object GetTxnItems extends Command
    
    
      /* discount type */
      object DISCTYPE  {
        val duplicated: Int = 0
        val best: Int = 1
        val least: Int = 2
        val keep: Int = 3
      }
    
      /* result message returned to client on the wire */
      object TXNTYPE {
        val sales: Int = 0
        val refund: Int = 1
        val void: Int = 2
        val voided: Int = 3
        val voidall: Int = 4
        val subtotal: Int = 5
        val logon: Int = 6
        val supon: Int = 7       // super user on/off
        val suspend: Int = 8
    
      }
    
      object SALESTYPE {
        val itm: Int = 2
        val sub: Int = 10
        val ttl: Int = 11
        val dsc: Int = 12
        val crd: Int = 13
      }
    
      case class TxnItem(
                          txndate: String = LocalDate.now.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
                          ,txntime: String = LocalDateTime.now.format(dateTimeFormatter).substring(11)
                          ,opr: String = ""//工号
                          ,num: Int = 0 //销售单号
                          ,seq: Int = 1 //交易序号
                          ,txntype: Int = TXNTYPE.sales//交易类型
                          ,salestype: Int = SALESTYPE.itm //销售类型
                          ,qty: Int =  1 //交易数量
                          ,price: Int = 0 //单价(分)
                          ,amount: Int = 0 //码洋(分)
                          ,disc: Int = 0   //折扣率 (%)
                          ,dscamt: Int = 0 //折扣额:负值  net实洋 = amount + dscamt
                          ,member: String = "" //会员卡号
                          ,code: String = "" //编号(商品、卡号...)
                          ,acct: String = "" //账号
                          ,dpt: String = "" //部类
                        )
      object TxnItem {
        def apply(vs: VchStates): TxnItem = TxnItem().copy(
          opr = vs.opr,
          num = vs.num,
          seq = vs.seq,
          member = vs.mbr
        )
      }
    
      case class VchStatus( //操作状态锁留给前端维护
                            qty: Int = 1,
                            refund: Boolean = false,
                            void: Boolean = false)
    
      case class VchStates(
                            opr: String = "",      //收款员
                            jseq: BigInt = 0,      //begin journal sequence for read-side replay
                            num: Int = 0,          //当前单号
                            seq: Int = 0,          //当前序号
                            void: Boolean = false, //取消模式
                            refd: Boolean = false, //退款模式
                            susp: Boolean = false, //挂单
                            canc: Boolean = false, //废单
                            due: Boolean = true,   //当前余额
                            su: String = "",
                            mbr: String = ""
                          ) {
    
        def nextVoucher : VchStates = VchStates().copy(
          opr = this.opr,
          jseq = this.jseq + 1,
          num = this.num + 1
        )
      }
    
    
      object STATUS {
        val OK: Int = 0
        val FAIL: Int = -1
      }
    
      case class POSResponse (sts: Int, msg: String, voucher: VchStates, txnItems: List[TxnItem])
    
      /* message on the wire (exchanged message) */
      val shardName = "POSShard"
    
      case class POSMessage(id: Long, cmd: Command) {
        def shopId = id.toString.head.toString
        def posId = id.toString
      }
    
      val getPOSId: ShardRegion.ExtractEntityId =  {
        case posCommand: POSMessage => (posCommand.posId,posCommand.cmd)
      }
      val getShopId: ShardRegion.ExtractShardId = {
        case posCommand: POSMessage => posCommand.shopId
      }
    
    
      case object PassivatePOS    //passivate message
      case object FilteredOut
      case class DebugMode(debug: Boolean)
      case class NodeAddress(address: String)
      case class PID(id: String)
      case class PerformRead(pid: String, vchnum: Int, bseq: Long, eseq: Long)
    }
    

    cql/CassandraEngine.scala

    package sdp.cql.engine
    
    import akka.NotUsed
    import akka.stream.alpakka.cassandra.scaladsl._
    import akka.stream.scaladsl._
    import com.datastax.driver.core._
    import com.google.common.util.concurrent.{FutureCallback, Futures, ListenableFuture}
    import protobuf.bytes.Converter._
    import sdp.logging.LogSupport
    import sdp.result.DBOResult._
    
    import scala.collection.JavaConverters._
    import scala.collection.generic.CanBuildFrom
    import scala.concurrent._
    
    object CQLContext {
      // Consistency Levels
      type CONSISTENCY_LEVEL = Int
      val ANY: CONSISTENCY_LEVEL          =                                        0x0000
      val ONE: CONSISTENCY_LEVEL          =                                        0x0001
      val TWO: CONSISTENCY_LEVEL          =                                        0x0002
      val THREE: CONSISTENCY_LEVEL        =                                        0x0003
      val QUORUM : CONSISTENCY_LEVEL      =                                        0x0004
      val ALL: CONSISTENCY_LEVEL          =                                        0x0005
      val LOCAL_QUORUM: CONSISTENCY_LEVEL =                                        0x0006
      val EACH_QUORUM: CONSISTENCY_LEVEL  =                                        0x0007
      val LOCAL_ONE: CONSISTENCY_LEVEL    =                                        0x000A
      val LOCAL_SERIAL: CONSISTENCY_LEVEL =                                        0x000B
      val SERIAL: CONSISTENCY_LEVEL       =                                        0x000C
    
      def apply(): CQLUpdateContext = CQLUpdateContext(statements = Nil)
    
      def consistencyLevel: CONSISTENCY_LEVEL => ConsistencyLevel = consistency => {
        consistency match {
          case ALL => ConsistencyLevel.ALL
          case ONE => ConsistencyLevel.ONE
          case TWO => ConsistencyLevel.TWO
          case THREE => ConsistencyLevel.THREE
          case ANY => ConsistencyLevel.ANY
          case EACH_QUORUM => ConsistencyLevel.EACH_QUORUM
          case LOCAL_ONE => ConsistencyLevel.LOCAL_ONE
          case QUORUM => ConsistencyLevel.QUORUM
          case SERIAL => ConsistencyLevel.SERIAL
          case LOCAL_SERIAL => ConsistencyLevel.LOCAL_SERIAL
    
        }
      }
    
    }
    case class CQLQueryContext(
                                statement: String,
                                parameters: Seq[Object] = Nil,
                                consistency: Option[CQLContext.CONSISTENCY_LEVEL] = None,
                                fetchSize: Int = 100
                              ) { ctx =>
      def setConsistencyLevel(_consistency: CQLContext.CONSISTENCY_LEVEL): CQLQueryContext =
        ctx.copy(consistency = Some(_consistency))
      def setFetchSize(pageSize: Int): CQLQueryContext =
        ctx.copy(fetchSize = pageSize)
      def setParameters(param: Seq[Object]): CQLQueryContext =
        ctx.copy(parameters = param)
    
      def toProto = new sdp.grpc.services.ProtoCQLQuery(
        statement = this.statement,
        parameters = { if (this.parameters == Nil) None
        else Some(sdp.grpc.services.ProtoAny(marshal(this.parameters))) },
        consistency = this.consistency,
        fetchSize = this.fetchSize
      )
    }
    object CQLQueryContext {
      def apply[M](stmt: String, param: Seq[Object]): CQLQueryContext = new CQLQueryContext(statement = stmt, parameters = param)
      def fromProto(proto: sdp.grpc.services.ProtoCQLQuery) =
        new CQLQueryContext(
          statement = proto.statement,
          parameters =
            proto.parameters match {
              case None => Nil
              case Some(so) =>
                if (so.value == _root_.com.google.protobuf.ByteString.EMPTY)
                  Nil
                else
                  unmarshal[Seq[Object]](so.value)
            },
          consistency = proto.consistency,
          fetchSize = proto.fetchSize
        )
    }
    
    case class CQLUpdateContext(
                                 statements: Seq[String],
                                 parameters: Seq[Seq[Object]] = Nil,
                                 psize: Int = 0,
                                 consistency: Option[CQLContext.CONSISTENCY_LEVEL] = None,
                                 batch: Boolean = false
                               ) extends LogSupport { ctx =>
      def setBatch(bat: Boolean) = ctx.copy(batch = bat)
      def setConsistencyLevel(_consistency: CQLContext.CONSISTENCY_LEVEL): CQLUpdateContext =
        ctx.copy(consistency = Some(_consistency))
      def setCommand(_statement: String, _psize: Int, _parameters: Object*): CQLUpdateContext = {
        log.info(s"setCommand> setting: statement: ${_statement}, parameters: ${_parameters}")
        var _params = Seq[Seq[Object]]()
        if ( _psize > 0) {
          if (_psize == 1)
            _params = Seq(_parameters.asInstanceOf[Seq[Object]])
          else
            _params = _parameters.asInstanceOf[Seq[Seq[Object]]]
        }
        val nc = ctx.copy(statements = Seq(_statement), psize = _psize, parameters = _params)
        log.info(s"setCommand> set: statements: ${nc.statements}, parameters: ${nc.parameters}")
        nc
      }
      def appendCommand(_statement: String, _parameters: Object*): CQLUpdateContext = {
        log.info(s"appendCommand> appending: statement: ${_statement}, parameters: ${_parameters}")
        val nc = ctx.copy(statements = ctx.statements :+ _statement,
          parameters = ctx.parameters ++ Seq(_parameters))
        log.info(s"appendCommand> appended: statements: ${nc.statements}, parameters: ${nc.parameters}")
        nc
      }
    
      def toProto = new sdp.grpc.services.ProtoCQLUpdate(
        statements = this.statements,
        parameters = { if (this.parameters == Nil) None
        else Some(sdp.grpc.services.ProtoAny(marshal(this.parameters))) },
        consistency = this.consistency,
        batch = Some(this.batch)
      )
    }
    
    object CQLUpdateContext {
      def fromProto(proto: sdp.grpc.services.ProtoCQLUpdate) =
        new CQLUpdateContext(
          statements = proto.statements,
          parameters =
            proto.parameters match {
              case None => Nil
              case Some(so) =>
                if (so.value == _root_.com.google.protobuf.ByteString.EMPTY)
                  Nil
                else
                  unmarshal[Seq[Seq[Object]]](so.value)
            },
          consistency = proto.consistency,
          batch = if(proto.batch == None) false else proto.batch.get
        )
    }
    
    object CQLEngine extends LogSupport {
      import CQLContext._
      import CQLHelpers._
    
      import scala.concurrent.Await
      import scala.concurrent.duration._
    
      def fetchResult[C[_] <: TraversableOnce[_],A](ctx: CQLQueryContext, pageSize: Int = 100
                                                    ,extractor: Row => A)(
                                                     implicit session: Session, cbf: CanBuildFrom[Nothing, A, C[A]]): DBOResult[C[A]]= {
    
        val prepStmt = session.prepare(ctx.statement)
    
        var boundStmt =  prepStmt.bind()
        var params: Seq[Object] = Nil
        if (ctx.parameters != Nil) {
          try {
            params = processParameters(ctx.parameters)
            boundStmt = prepStmt.bind(params: _*)
          }
          catch {
            case e: Exception =>
              log.error(s"fetchResult> prepStmt.bind error: ${e.getMessage}")
              Left(new RuntimeException(s"fetchResult> prepStmt.bind Error: ${e.getMessage}"))
          }
        }
        log.info(s"fetchResult>  statement: ${prepStmt.getQueryString}, parameters: ${params}")
    
        try {
          ctx.consistency.foreach { consistency =>
            boundStmt.setConsistencyLevel(consistencyLevel(consistency))
          }
    
          val resultSet = session.execute(boundStmt.setFetchSize(pageSize))
          val rows = resultSet.asScala.view.map(extractor).to[C]
          valueToDBOResult(rows)
          /*
          val ores = if(rows.isEmpty) None else Some(rows)
          optionToDBOResult(ores: Option[C[A]]) */
        }
        catch {
          case e: Exception =>
            log.error(s"fetchResult> runtime error: ${e.getMessage}")
            Left(new RuntimeException(s"fetchResult> Error: ${e.getMessage}"))
        }
      }
    
      def fetchResultPage[C[_] <: TraversableOnce[_],A](ctx: CQLQueryContext, pageSize: Int = 100
                                                        ,extractor: Row => A)(
                                                         implicit session: Session, cbf: CanBuildFrom[Nothing, A, C[A]]): (ResultSet, C[A])= {
    
        val prepStmt = session.prepare(ctx.statement)
    
        var boundStmt =  prepStmt.bind()
        var params: Seq[Object] = Nil
        if (ctx.parameters != Nil) {
          params = processParameters(ctx.parameters)
          boundStmt = prepStmt.bind(params:_*)
        }
        log.info(s"fetchResultPage>  statement: ${prepStmt.getQueryString}, parameters: ${params}")
    
        ctx.consistency.foreach {consistency =>
          boundStmt.setConsistencyLevel(consistencyLevel(consistency))}
    
        val resultSet = session.execute(boundStmt.setFetchSize(pageSize))
        (resultSet,(resultSet.asScala.view.map(extractor)).to[C])
      }
    
      def fetchMorePages[C[_] <: TraversableOnce[_],A](resultSet: ResultSet, timeOut: Duration)(
        extractor: Row => A)(implicit ec: ExecutionContext, cbf: CanBuildFrom[Nothing, A, C[A]]): (ResultSet,Option[C[A]]) =
        if (resultSet.isFullyFetched) {
          (resultSet, None)
        } else {
          try {
            val result = Await.result((resultSet.fetchMoreResults()).asScala, timeOut)
            (result, Some((result.asScala.view.map(extractor)).to[C]))
          } catch { case e: Throwable => (resultSet, None) }
        }
    
      def cqlExecute(ctx: CQLUpdateContext)(
        implicit session: Session, ec: ExecutionContext): DBOResult[Boolean] = {
        var ctxparameters = Seq[Seq[Object]]()
        if (ctx.parameters != Nil)
          if (ctx.parameters.head != Nil) {
            ctxparameters = ctx.parameters.asInstanceOf[Seq[Seq[Seq[Object]]]].head
          }
    
        var invalidBat = false
        if ( ctx.batch ) {
          if (ctxparameters == Nil)
            invalidBat = true
          else if (ctxparameters.size < 2)
            invalidBat = true;
        }
        if (!ctx.batch || invalidBat) {
          if(invalidBat)
            log.warn(s"cqlExecute> batch update must have at least 2 sets of parameters! change to single-command.")
    
          if (ctx.statements.size == 1 && ctx.psize <= 1) {
            var param: Seq[Seq[Object]] = Nil
            if (ctxparameters != Nil)
              param = ctxparameters
              log.info(s"cqlExecute>  single-command: statement: ${ctx.statements.head} parameters: ${param}")
            cqlSingleUpdate(ctx.consistency, ctx.statements.head, param)
          }
          else {
            var params: Seq[Seq[Object]] = ctxparameters
            var ctxstatements = ctx.statements
            if (ctxparameters.size < ctx.statements.size) {
              log.warn(s"cqlExecute> fewer parameters than statements! pad with 'Nil'.")
              val pnils = Seq.fill(ctx.statements.length - ctxparameters.size)(Nil)
              params = ctxparameters ++ pnils
            }
            else {
              if (ctx.statements.size < ctxparameters.size) {
                log.warn(s"cqlExecute> fewer statements than parameters! pad with 'head'.")
                val heads = Seq.fill(ctxparameters.size - ctx.statements.size)(ctx.statements.head)
                ctxstatements = ctx.statements ++ heads
              }
            }
    
            val commands: Seq[(String, Seq[Object])] = ctxstatements zip params
            log.info(s"cqlExecute>  multi-commands: ${commands}")
            /*
                    //using sequence to flip List[Future[Boolean]] => Future[List[Boolean]]
                    //therefore, make sure no command replies on prev command effect
                    val lstCmds: List[Future[Boolean]] = commands.map { case (stmt,param) =>
                      cqlSingleUpdate(ctx.consistency, stmt, param)
                    }.toList
    
                    val futList = Future.sequence(lstCmds).map(_ => true)   //must map to execute
            */
            /*
                    //using traverse to have some degree of parallelism = max(runtimes)
                    //therefore, make sure no command replies on prev command effect
                    val futList = Future.traverse(commands) { case (stmt,param)  =>
                      cqlSingleUpdate(ctx.consistency, stmt, param)
                    }.map(_ => true)
    
                    Await.result(futList, 3 seconds)
                    Future.successful(true)
            */
            // run sync directly
            try {
              commands.foreach { case (stm, pars) =>
                cqlExecuteSync(ctx.consistency, stm, pars)
              }
              Right(true)
            }
            catch {
              case e: Exception =>
                log.error(s"cqlExecute> runtime error: ${e.getMessage}")
                Left(new RuntimeException(s"cqlExecute> Error: ${e.getMessage}"))
            }
          }
        }
        else
          cqlBatchUpdate(ctx)
      }
      def cqlSingleUpdate(cons: Option[CQLContext.CONSISTENCY_LEVEL],stmt: String, params: Seq[Seq[Object]])(
        implicit session: Session, ec: ExecutionContext): DBOResult[Boolean] = {
    
        val prepStmt = session.prepare(stmt)
    
        var boundStmt = prepStmt.bind()
        var pars: Seq[Seq[Object]] = Nil
        if (params != Nil) {
          try {
            pars = params.map(processParameters(_))
            boundStmt = prepStmt.bind(pars.head: _*)
          }
          catch {
            case e: Exception =>
              log.error(s"cqlSingleUpdate> prepStmt.bind error: ${e.getMessage}")
              Left(new RuntimeException(s"cqlSingleUpdate> prepStmt.bind Error: ${e.getMessage}"))
          }
        }
        log.info(s"cqlSingleUpdate>  statement: ${prepStmt.getQueryString}, parameters: ${pars}")
    
        try {
          cons.foreach { consistency =>
            boundStmt.setConsistencyLevel(consistencyLevel(consistency))
          }
          val res = session.execute(boundStmt) //executeAsync(boundStmt).map(_.wasApplied())
          Right(res.wasApplied())
        }
        catch {
          case e: Exception =>
            log.error(s"cqlExecute> runtime error: ${e.getMessage}")
            Left(new RuntimeException(s"cqlExecute> Error: ${e.getMessage}"))
        }
      }
    
      def cqlExecuteSync(cons: Option[CQLContext.CONSISTENCY_LEVEL],stmt: String, params: Seq[Object])(
        implicit session: Session, ec: ExecutionContext): Boolean = {
    
        val prepStmt = session.prepare(stmt)
    
        var boundStmt = prepStmt.bind()
        var pars: Seq[Object] = Nil
        if (params != Nil) {
          pars = processParameters(params)
          boundStmt = prepStmt.bind(pars: _*)
        }
        log.info(s"cqlExecuteSync>  statement: ${prepStmt.getQueryString}, parameters: ${pars}")
    
        cons.foreach { consistency =>
          boundStmt.setConsistencyLevel(consistencyLevel(consistency))
        }
        session.execute(boundStmt).wasApplied()
    
      }
    
      def cqlBatchUpdate(ctx: CQLUpdateContext)(
        implicit session: Session, ec: ExecutionContext): DBOResult[Boolean] = {
        var ctxparameters = Seq[Seq[Object]]()
        if(ctx.parameters != Nil)
          if (ctx.parameters.head != Nil)
            ctxparameters = ctx.parameters.asInstanceOf[Seq[Seq[Seq[Object]]]].head
        var params: Seq[Seq[Object]] = ctxparameters
        var ctxstatements = ctx.statements
        if (ctxparameters.size < ctx.statements.size) {
          log.warn(s"cqlBatchUpdate> fewer parameters than statements! pad with 'Nil'.")
          val pnils = Seq.fill(ctx.statements.length - ctxparameters.size)(Nil)
          params = ctxparameters ++ pnils
        }
        else {
          if (ctx.statements.size < ctxparameters.size) {
            log.warn(s"cqlBatchUpdate> fewer statements than parameters! pad with 'head'.")
            val heads = Seq.fill(ctxparameters.size - ctx.statements.size)(ctx.statements.head)
            ctxstatements = ctx.statements ++ heads
          }
        }
    
        val commands: Seq[(String, Seq[Object])] = ctxstatements zip params
        log.info(s"cqlBatchUpdate>  batch-commands: ${commands}")
    
    
    
        var batch = new BatchStatement()
        try {
          commands.foreach { cmd =>
            val prepStmt = session.prepare(cmd._1)
            log.info(s"cqlBatchUpdate>  batch with statement: ${cmd._1}, raw parameter: ${cmd._2}")
            if (cmd._2 == Nil) {
              val pars = processParameters(cmd._2)
              log.info(s"cqlBatchUpdate>  batch with cooked parameters: ${pars}")
              batch.add(prepStmt.bind(pars: _*))
            } else {
              log.info(s"cqlBatchUpdate>  batch with no parameter")
              batch.add(prepStmt.bind())
            }
          }
          ctx.consistency.foreach { consistency =>
            batch.setConsistencyLevel(consistencyLevel(consistency))
          }
          val res = session.execute(batch) //session.executeAsync(batch).map(_.wasApplied())
          Right(res.wasApplied())
        }
        catch {
          case e: Exception =>
            log.error(s"cqlBatchUpdate> runtime error: ${e.getMessage}")
            Left(new RuntimeException(s"cqlBatchUpdate> Error: ${e.getMessage}"))
        }
    
      }
    
      def cassandraStream[A](ctx: CQLQueryContext,extractor: Row => A)
                            (implicit session: Session, ec: ExecutionContextExecutor): Source[A,NotUsed] = {
    
        val prepStmt = session.prepare(ctx.statement)
        var boundStmt =  prepStmt.bind()
        val params = processParameters(ctx.parameters)
        boundStmt = prepStmt.bind(params:_*)
        ctx.consistency.foreach {consistency =>
          boundStmt.setConsistencyLevel(consistencyLevel(consistency))}
    
        log.info(s"cassandraStream>  statement: ${prepStmt.getQueryString}, parameters: ${params}")
        CassandraSource(boundStmt.setFetchSize(ctx.fetchSize)).map(extractor)
      }
    
      case class CassandraActionStream[R](parallelism: Int = 1, processInOrder: Boolean = true,
                                          statement: String, prepareParams: R => Seq[Object],
                                          consistency: Option[CQLContext.CONSISTENCY_LEVEL] = None){ cas =>
        def setParallelism(parLevel: Int): CassandraActionStream[R] = cas.copy(parallelism=parLevel)
        def setProcessOrder(ordered: Boolean): CassandraActionStream[R] = cas.copy(processInOrder = ordered)
        def setConsistencyLevel(_consistency: CQLContext.CONSISTENCY_LEVEL): CassandraActionStream[R] =
          cas.copy(consistency = Some(_consistency))
    
        def perform(r: R)(implicit session: Session, ec: ExecutionContext) = {
          var prepStmt = session.prepare(statement)
          var boundStmt =  prepStmt.bind()
          val params = processParameters(prepareParams(r))
          boundStmt = prepStmt.bind(params: _*)
          consistency.foreach { cons =>
            boundStmt.setConsistencyLevel(CQLContext.consistencyLevel(cons))
          }
          log.info(s"CassandraActionStream.perform>  statement: ${prepStmt.getQueryString}, parameters: ${params}")
          import monix.eval.Task
          import monix.execution.Scheduler.Implicits.global
          session.execute(boundStmt)
          Task.now {r}.runToFuture
          //session.executeAsync(boundStmt).map(_ => r)
        }
    
        def performOnRow(implicit session: Session, ec: ExecutionContext): Flow[R, R, NotUsed] =
          if (processInOrder)
            Flow[R].mapAsync(parallelism)(perform)
          else
            Flow[R].mapAsyncUnordered(parallelism)(perform)
    
        def unloggedBatch[K](statementBinder: (
          R, PreparedStatement) => BoundStatement,partitionKey: R => K)(
                              implicit session: Session, ec: ExecutionContext): Flow[R, R, NotUsed] = {
          val preparedStatement = session.prepare(statement)
          log.info(s"CassandraActionStream.unloggedBatch>  statement: ${preparedStatement.getQueryString}")
          CassandraFlow.createUnloggedBatchWithPassThrough[R, K](
            parallelism,
            preparedStatement,
            statementBinder,
            partitionKey)
        }
    
      }
      object CassandraActionStream {
        def apply[R](_statement: String, params: R => Seq[Object]): CassandraActionStream[R] =
          new CassandraActionStream[R]( statement=_statement, prepareParams = params)
      }
    
    
    }
    
    object CQLHelpers extends LogSupport {
      import java.io._
      import java.nio.ByteBuffer
      import java.nio.file._
      import java.time.Instant
      import scala.util.Try
    
    
      import akka.stream._
      import akka.stream.scaladsl._
      import com.datastax.driver.core.LocalDate
      import com.datastax.driver.extras.codecs.jdk8.InstantCodec
      import java.util.concurrent.Executor
    /*
      implicit def listenableFutureToFuture[T](
                                                listenableFuture: ListenableFuture[T]): Future[T] = {
        val promise = Promise[T]()
        Futures.addCallback(listenableFuture, new FutureCallback[T] {
          def onFailure(error: Throwable): Unit = {
            promise.failure(error)
            ()
          }
          def onSuccess(result: T): Unit = {
            promise.success(result)
            ()
          }
        })
        promise.future
      } */
    
      implicit def listenableFutureToFuture[A](lf: ListenableFuture[A])(implicit executionContext: ExecutionContext): Future[A] = {
        val promise = Promise[A]
        lf.addListener(new Runnable {
          def run() = promise.complete(Try(lf.get()))
        }, executionContext.asInstanceOf[Executor])
        promise.future
      }
    
      implicit class ListenableFutureConverter[A](val lf: ListenableFuture[A]) extends AnyVal {
        def asScala(implicit ec: ExecutionContext): Future[A] = {
          val promise = Promise[A]
          lf.addListener(new Runnable {
            def run() = promise.complete(Try(lf.get()))
          }, ec.asInstanceOf[Executor])
          promise.future
        }
      }
    /*
      implicit def toScalaFuture[A](a: ListenableFuture[A])(implicit ec: ExecutionContext): Future[A] = {
        val promise = Promise[A]()
        a.addListener(new Runnable {
          def run() = {
            try {
              promise.success(a.get)
            } catch {
              case ex: ExecutionException => promise.failure(ex.getCause)
              case ex => promise.failure(ex)
            }
          }
        }, ec.asInstanceOf[Executor])
        promise.future
      } */
    
      case class CQLDate(year: Int, month: Int, day: Int)
      case object CQLTodayDate
      case class CQLDateTime(year: Int, Month: Int,
                             day: Int, hour: Int, minute: Int, second: Int, millisec: Int = 0)
      case object CQLDateTimeNow
    
      def cqlGetDate(dateToConvert: java.util.Date): java.time.LocalDate =
        dateToConvert.toInstant()
          .atZone(java.time.ZoneId.systemDefault())
          .toLocalDate()
    
      def cqlGetTime(dateToConvert: java.util.Date): java.time.LocalTime =
        dateToConvert.toInstant()
          .atZone(java.time.ZoneId.systemDefault())
          .toLocalTime()
    
      def cqlGetTimestamp(dateToConvert: java.util.Date): java.time.LocalDateTime=
        new java.sql.Timestamp(
          dateToConvert.getTime()
        ).toLocalDateTime()
    
      def processParameters(params: Seq[Object]): Seq[Object] = {
        import java.time.{Clock, ZoneId}
        log.info(s"[processParameters] input: ${params}")
        val outParams = params.map { obj =>
          obj match {
            case CQLDate(yy, mm, dd) => LocalDate.fromYearMonthDay(yy, mm, dd)
            case CQLTodayDate =>
              val today = java.time.LocalDate.now()
              LocalDate.fromYearMonthDay(today.getYear, today.getMonth.getValue, today.getDayOfMonth)
            case CQLDateTimeNow => Instant.now(Clock.system(ZoneId.of("EST", ZoneId.SHORT_IDS)))
            case CQLDateTime(yy, mm, dd, hr, ms, sc, mi) =>
              Instant.parse(f"$yy%4d-$mm%2d-$dd%2dT$hr%2d:$ms%2d:$sc%2d$mi%3d")
            case p@_ => p
          }
        }
        log.info(s"[processParameters] output: ${params}")
        outParams
      }
      class ByteBufferInputStream(buf: ByteBuffer) extends InputStream {
        override def read: Int = {
          if (!buf.hasRemaining) return -1
          buf.get
        }
    
        override def read(bytes: Array[Byte], off: Int, len: Int): Int = {
          val length: Int = Math.min(len, buf.remaining)
          buf.get(bytes, off, length)
          length
        }
      }
      object ByteBufferInputStream {
        def apply(buf: ByteBuffer): ByteBufferInputStream = {
          new ByteBufferInputStream(buf)
        }
      }
      class FixsizedByteBufferOutputStream(buf: ByteBuffer) extends OutputStream {
    
        override def write(b: Int): Unit = {
          buf.put(b.toByte)
        }
    
        override def write(bytes: Array[Byte], off: Int, len: Int): Unit = {
          buf.put(bytes, off, len)
        }
      }
      object FixsizedByteBufferOutputStream {
        def apply(buf: ByteBuffer) = new FixsizedByteBufferOutputStream(buf)
      }
      class ExpandingByteBufferOutputStream(var buf: ByteBuffer, onHeap: Boolean) extends OutputStream {
    
        private val increasing = ExpandingByteBufferOutputStream.DEFAULT_INCREASING_FACTOR
    
        override def write(b: Array[Byte], off: Int, len: Int): Unit = {
          val position = buf.position
          val limit = buf.limit
          val newTotal: Long = position + len
          if(newTotal > limit){
            var capacity = (buf.capacity * increasing)
            while(capacity <= newTotal){
              capacity = (capacity*increasing)
            }
            increase(capacity.toInt)
          }
    
          buf.put(b, 0, len)
        }
    
        override def write(b: Int): Unit= {
          if (!buf.hasRemaining) increase((buf.capacity * increasing).toInt)
          buf.put(b.toByte)
        }
        protected def increase(newCapacity: Int): Unit = {
          buf.limit(buf.position)
          buf.rewind
          val newBuffer =
            if (onHeap) ByteBuffer.allocate(newCapacity)
            else  ByteBuffer.allocateDirect(newCapacity)
          newBuffer.put(buf)
          buf.clear
          buf = newBuffer
        }
        def size: Long = buf.position
        def capacity: Long = buf.capacity
        def byteBuffer: ByteBuffer = buf
      }
      object ExpandingByteBufferOutputStream {
        val DEFAULT_INCREASING_FACTOR = 1.5f
        def apply(size: Int, increasingBy: Float, onHeap: Boolean) = {
          if (increasingBy <= 1) throw new IllegalArgumentException("Increasing Factor must be greater than 1.0")
          val buffer: ByteBuffer =
            if (onHeap) ByteBuffer.allocate(size)
            else ByteBuffer.allocateDirect(size)
          new ExpandingByteBufferOutputStream(buffer,onHeap)
        }
        def apply(size: Int): ExpandingByteBufferOutputStream = {
          apply(size, ExpandingByteBufferOutputStream.DEFAULT_INCREASING_FACTOR, false)
        }
    
        def apply(size: Int, onHeap: Boolean): ExpandingByteBufferOutputStream = {
          apply(size, ExpandingByteBufferOutputStream.DEFAULT_INCREASING_FACTOR, onHeap)
        }
    
        def apply(size: Int, increasingBy: Float): ExpandingByteBufferOutputStream = {
          apply(size, increasingBy, false)
        }
    
      }
      def cqlFileToBytes(fileName: String): ByteBuffer = {
        val fis = new FileInputStream(fileName)
        val b = new Array[Byte](fis.available + 1)
        val length = b.length
        fis.read(b)
        ByteBuffer.wrap(b)
      }
      def cqlBytesToFile(bytes: ByteBuffer, fileName: String)(
        implicit mat: Materializer): Future[IOResult] = {
        val source = StreamConverters.fromInputStream(() => ByteBufferInputStream(bytes))
        source.runWith(FileIO.toPath(Paths.get(fileName)))
      }
      def cqlDateTimeString(date: java.util.Date, fmt: String): String = {
        val outputFormat = new java.text.SimpleDateFormat(fmt)
        outputFormat.format(date)
      }
      def useJava8DateTime(cluster: Cluster) = {
        //for jdk8 datetime format
        cluster.getConfiguration().getCodecRegistry()
          .register(InstantCodec.instance)
      }
    }
    

    mgo/MGOProtoConversion.scala

    package sdp.mongo.engine
    import org.mongodb.scala.bson.collection.immutable.Document
    import org.bson.conversions.Bson
    import sdp.grpc.services._
    import protobuf.bytes.Converter._
    import MGOClasses._
    import MGOAdmins._
    import MGOCommands._
    import org.bson.BsonDocument
    import org.bson.codecs.configuration.CodecRegistry
    import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
    import org.mongodb.scala.FindObservable
    
    object MGOProtoConversion {
    
      type MGO_COMMAND_TYPE = Int
      val MGO_COMMAND_FIND            = 0
      val MGO_COMMAND_COUNT           = 20
      val MGO_COMMAND_DISTICT         = 21
      val MGO_COMMAND_DOCUMENTSTREAM  = 1
      val MGO_COMMAND_AGGREGATE       = 2
      val MGO_COMMAND_INSERT          = 3
      val MGO_COMMAND_DELETE          = 4
      val MGO_COMMAND_REPLACE         = 5
      val MGO_COMMAND_UPDATE          = 6
    
    
      val MGO_ADMIN_DROPCOLLECTION    = 8
      val MGO_ADMIN_CREATECOLLECTION  = 9
      val MGO_ADMIN_LISTCOLLECTION    = 10
      val MGO_ADMIN_CREATEVIEW        = 11
      val MGO_ADMIN_CREATEINDEX       = 12
      val MGO_ADMIN_DROPINDEXBYNAME   = 13
      val MGO_ADMIN_DROPINDEXBYKEY    = 14
      val MGO_ADMIN_DROPALLINDEXES    = 15
    
    
      case class AdminContext(
                               tarName: String = "",
                               bsonParam: Seq[Bson] = Nil,
                               options: Option[Any] = None,
                               objName: String = ""
                             ){
        def toProto = sdp.grpc.services.ProtoMGOAdmin(
          tarName = this.tarName,
          bsonParam = this.bsonParam.map {b => sdp.grpc.services.ProtoMGOBson(marshal(b))},
          objName = this.objName,
          options = this.options.map(b => ProtoAny(marshal(b)))
    
        )
      }
    
      object AdminContext {
        def fromProto(msg: sdp.grpc.services.ProtoMGOAdmin) = new AdminContext(
          tarName = msg.tarName,
          bsonParam = msg.bsonParam.map(b => unmarshal[Bson](b.bson)),
          objName = msg.objName,
          options = msg.options.map(b => unmarshal[Any](b.value))
        )
      }
    
      case class Context(
                          dbName: String = "",
                          collName: String = "",
                          commandType: MGO_COMMAND_TYPE,
                          bsonParam: Seq[Bson] = Nil,
                          resultOptions: Seq[ResultOptions] = Nil,
                          options: Option[Any] = None,
                          documents: Seq[Document] = Nil,
                          targets: Seq[String] = Nil,
                          only: Boolean = false,
                          adminOptions: Option[AdminContext] = None
                        ){
    
        def toProto = new sdp.grpc.services.ProtoMGOContext(
          dbName = this.dbName,
          collName = this.collName,
          commandType = this.commandType,
          bsonParam = this.bsonParam.map(bsonToProto),
          resultOptions = this.resultOptions.map(_.toProto),
          options = { if(this.options == None)
            None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
          else
            Some(ProtoAny(marshal(this.options.get))) },
          documents = this.documents.map(d => sdp.grpc.services.ProtoMGODocument(marshal(d))),
          targets = this.targets,
          only = Some(this.only),
          adminOptions = this.adminOptions.map(_.toProto)
        )
    
      }
    
      object MGODocument {
        def fromProto(msg: sdp.grpc.services.ProtoMGODocument): Document =
          unmarshal[Document](msg.document)
        def toProto(doc: Document): sdp.grpc.services.ProtoMGODocument =
          new ProtoMGODocument(marshal(doc))
      }
    
      object MGOProtoMsg {
        def fromProto(msg: sdp.grpc.services.ProtoMGOContext) = new Context(
          dbName = msg.dbName,
          collName = msg.collName,
          commandType = msg.commandType,
          bsonParam = msg.bsonParam.map(protoToBson),
          resultOptions = msg.resultOptions.map(r => ResultOptions.fromProto(r)),
          options = msg.options.map(a => unmarshal[Any](a.value)),
          documents = msg.documents.map(doc => unmarshal[Document](doc.document)),
          targets = msg.targets,
          adminOptions = msg.adminOptions.map(ado => AdminContext.fromProto(ado))
        )
      }
    
      def bsonToProto(bson: Bson) =
        ProtoMGOBson(marshal(bson.toBsonDocument(
          classOf[org.mongodb.scala.bson.collection.immutable.Document],DEFAULT_CODEC_REGISTRY)))
    
      def protoToBson(proto: ProtoMGOBson): Bson = new Bson {
        val bsdoc = unmarshal[BsonDocument](proto.bson)
        override def toBsonDocument[TDocument](documentClass: Class[TDocument], codecRegistry: CodecRegistry): BsonDocument = bsdoc
      }
    
      def ctxFromProto(proto: ProtoMGOContext): MGOContext = proto.commandType match {
        case MGO_COMMAND_FIND => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_QUERY,
            action = Some(Find())
          )
          def toResultOption(rts: Seq[ProtoMGOResultOption]): FindObservable[Document] => FindObservable[Document] = findObj =>
            rts.foldRight(findObj)((a,b) => ResultOptions.fromProto(a).toFindObservable(b))
    
          (proto.bsonParam, proto.resultOptions, proto.only) match {
            case (Nil, Nil, None) => ctx
            case (Nil, Nil, Some(b)) => ctx.setCommand(Find(firstOnly = b))
            case (bp,Nil,None) => ctx.setCommand(
              Find(filter = Some(protoToBson(bp.head))))
            case (bp,Nil,Some(b)) => ctx.setCommand(
              Find(filter = Some(protoToBson(bp.head)), firstOnly = b))
            case (bp,fo,None) => {
              ctx.setCommand(
                Find(filter = Some(protoToBson(bp.head)),
                  andThen = fo.map(ResultOptions.fromProto)
                ))
            }
            case (bp,fo,Some(b)) => {
              ctx.setCommand(
                Find(filter = Some(protoToBson(bp.head)),
                  andThen = fo.map(ResultOptions.fromProto),
                  firstOnly = b))
            }
            case _ => ctx
          }
        }
        case MGO_COMMAND_COUNT => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_QUERY,
            action = Some(Count())
          )
          (proto.bsonParam, proto.options) match {
            case (Nil, None) => ctx
            case (bp, None) => ctx.setCommand(
              Count(filter = Some(protoToBson(bp.head)))
            )
            case (Nil,Some(o)) => ctx.setCommand(
              Count(options = Some(unmarshal[Any](o.value)))
            )
            case _ => ctx
          }
        }
        case MGO_COMMAND_DISTICT => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_QUERY,
            action = Some(Distict(fieldName = proto.targets.head))
          )
          (proto.bsonParam) match {
            case Nil => ctx
            case bp: Seq[ProtoMGOBson] => ctx.setCommand(
              Distict(fieldName = proto.targets.head,filter = Some(protoToBson(bp.head)))
            )
            case _ => ctx
          }
        }
        case MGO_COMMAND_AGGREGATE => {
          new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_QUERY,
            action = Some(Aggregate(proto.bsonParam.map(p => protoToBson(p))))
          )
        }
        case MGO_ADMIN_LISTCOLLECTION => {
          new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_QUERY,
            action = Some(ListCollection(proto.dbName)))
        }
        case MGO_COMMAND_INSERT => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_UPDATE,
            action = Some(Insert(
              newdocs = proto.documents.map(doc => unmarshal[Document](doc.document))))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(Insert(
              newdocs = proto.documents.map(doc => unmarshal[Document](doc.document)),
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_COMMAND_DELETE => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_UPDATE,
            action = Some(Delete(
              filter = protoToBson(proto.bsonParam.head)))
          )
          (proto.options, proto.only) match {
            case (None,None) => ctx
            case (None,Some(b)) => ctx.setCommand(Delete(
              filter = protoToBson(proto.bsonParam.head),
              onlyOne = b))
            case (Some(o),None) => ctx.setCommand(Delete(
              filter = protoToBson(proto.bsonParam.head),
              options = Some(unmarshal[Any](o.value)))
            )
            case (Some(o),Some(b)) => ctx.setCommand(Delete(
              filter = protoToBson(proto.bsonParam.head),
              options = Some(unmarshal[Any](o.value)),
              onlyOne = b)
            )
          }
        }
        case MGO_COMMAND_REPLACE => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_UPDATE,
            action = Some(Replace(
              filter = protoToBson(proto.bsonParam.head),
              replacement = unmarshal[Document](proto.documents.head.document)))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(Replace(
              filter = protoToBson(proto.bsonParam.head),
              replacement = unmarshal[Document](proto.documents.head.document),
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_COMMAND_UPDATE => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_UPDATE,
            action = Some(Update(
              filter = protoToBson(proto.bsonParam.head),
              update = protoToBson(proto.bsonParam.tail.head)))
          )
          (proto.options, proto.only) match {
            case (None,None) => ctx
            case (None,Some(b)) => ctx.setCommand(Update(
              filter = protoToBson(proto.bsonParam.head),
              update = protoToBson(proto.bsonParam.tail.head),
              onlyOne = b))
            case (Some(o),None) => ctx.setCommand(Update(
              filter = protoToBson(proto.bsonParam.head),
              update = protoToBson(proto.bsonParam.tail.head),
              options = Some(unmarshal[Any](o.value)))
            )
            case (Some(o),Some(b)) => ctx.setCommand(Update(
              filter = protoToBson(proto.bsonParam.head),
              update = protoToBson(proto.bsonParam.tail.head),
              options = Some(unmarshal[Any](o.value)),
              onlyOne = b)
            )
          }
        }
        case MGO_ADMIN_DROPCOLLECTION =>
          new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(DropCollection(proto.collName))
          )
        case MGO_ADMIN_CREATECOLLECTION => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(CreateCollection(proto.collName))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(CreateCollection(proto.collName,
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_ADMIN_CREATEVIEW => {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(CreateView(viewName = proto.targets.head,
              viewOn = proto.targets.tail.head,
              pipeline = proto.bsonParam.map(p => protoToBson(p))))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(CreateView(viewName = proto.targets.head,
              viewOn = proto.targets.tail.head,
              pipeline = proto.bsonParam.map(p => protoToBson(p)),
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_ADMIN_CREATEINDEX=> {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(CreateIndex(key = protoToBson(proto.bsonParam.head)))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(CreateIndex(key = protoToBson(proto.bsonParam.head),
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_ADMIN_DROPINDEXBYNAME=> {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(DropIndexByName(indexName = proto.targets.head))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(DropIndexByName(indexName = proto.targets.head,
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_ADMIN_DROPINDEXBYKEY=> {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(DropIndexByKey(key = protoToBson(proto.bsonParam.head)))
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(DropIndexByKey(key = protoToBson(proto.bsonParam.head),
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
        case MGO_ADMIN_DROPALLINDEXES=> {
          var ctx = new MGOContext(
            dbName = proto.dbName,
            collName = proto.collName,
            actionType = MGO_ADMIN,
            action = Some(DropAllIndexes())
          )
          proto.options match {
            case None => ctx
            case Some(o) => ctx.setCommand(DropAllIndexes(
              options = Some(unmarshal[Any](o.value)))
            )
          }
        }
    
      }
    
      def ctxToProto(ctx: MGOContext): Option[sdp.grpc.services.ProtoMGOContext] = ctx.action match {
        case None => None
        case Some(act) => act match {
          case Count(filter, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_COUNT,
              bsonParam = { if (filter == None) Seq.empty[ProtoMGOBson]
                            else Seq(bsonToProto(filter.get))},
              options = { if(options == None) None  //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
                          else Some(ProtoAny(marshal(options.get))) }
          ))
          case Distict(fieldName, filter) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_DISTICT,
              bsonParam = { if (filter == None) Seq.empty[ProtoMGOBson]
                            else Seq(bsonToProto(filter.get))},
              targets = Seq(fieldName)
    
            ))
    
          case Find(filter, andThen, firstOnly) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_FIND,
              bsonParam = { if (filter == None) Seq.empty[ProtoMGOBson]
              else Seq(bsonToProto(filter.get))},
              resultOptions = andThen.map(_.toProto)
            ))
    
          case Aggregate(pipeLine) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_AGGREGATE,
              bsonParam = pipeLine.map(bsonToProto)
            ))
    
          case Insert(newdocs, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_INSERT,
              documents = newdocs.map(d => ProtoMGODocument(marshal(d))),
              options = { if(options == None) None      //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
          case Delete(filter, options, onlyOne) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_DELETE,
              bsonParam = Seq(bsonToProto(filter)),
              options = { if(options == None) None  //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) },
              only = Some(onlyOne)
            ))
    
          case Replace(filter, replacement, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_REPLACE,
              bsonParam = Seq(bsonToProto(filter)),
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) },
              documents = Seq(ProtoMGODocument(marshal(replacement)))
            ))
    
          case Update(filter, update, options, onlyOne) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_COMMAND_UPDATE,
              bsonParam = Seq(bsonToProto(filter),bsonToProto(update)),
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) },
              only = Some(onlyOne)
            ))
    
    
          case DropCollection(coll) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = coll,
              commandType = MGO_ADMIN_DROPCOLLECTION
            ))
    
          case CreateCollection(coll, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = coll,
              commandType = MGO_ADMIN_CREATECOLLECTION,
              options = { if(options == None) None  //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
          case ListCollection(dbName) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              commandType = MGO_ADMIN_LISTCOLLECTION
            ))
    
          case CreateView(viewName, viewOn, pipeline, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_ADMIN_CREATEVIEW,
              bsonParam = pipeline.map(bsonToProto),
              options = { if(options == None) None  //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) },
              targets = Seq(viewName,viewOn)
            ))
    
          case CreateIndex(key, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_ADMIN_CREATEINDEX,
              bsonParam = Seq(bsonToProto(key)),
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
    
          case DropIndexByName(indexName, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_ADMIN_DROPINDEXBYNAME,
              targets = Seq(indexName),
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
          case DropIndexByKey(key, options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_ADMIN_DROPINDEXBYKEY,
              bsonParam = Seq(bsonToProto(key)),
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
    
          case DropAllIndexes(options) =>
            Some(new sdp.grpc.services.ProtoMGOContext(
              dbName = ctx.dbName,
              collName = ctx.collName,
              commandType = MGO_ADMIN_DROPALLINDEXES,
              options = { if(options == None) None //Some(ProtoAny(com.google.protobuf.ByteString.EMPTY))
              else Some(ProtoAny(marshal(options.get))) }
            ))
    
        }
      }
    
    }
    

    mgo/ObservableToPublisher.scala

    package sdp.mongo.engine
    
    import java.util.concurrent.atomic.AtomicBoolean
    
    import org.mongodb.{scala => mongoDB}
    import org.{reactivestreams => rxStreams}
    
    final case class ObservableToPublisher[T](observable: mongoDB.Observable[T])
      extends rxStreams.Publisher[T] {
      def subscribe(subscriber: rxStreams.Subscriber[_ >: T]): Unit =
        observable.subscribe(new mongoDB.Observer[T]() {
          override def onSubscribe(subscription: mongoDB.Subscription): Unit =
            subscriber.onSubscribe(new rxStreams.Subscription() {
              private final val cancelled: AtomicBoolean = new AtomicBoolean
    
              override def request(n: Long): Unit =
                if (!subscription.isUnsubscribed && !cancelled.get() && n < 1) {
                  subscriber.onError(
                    new IllegalArgumentException(
                      s"Demand from publisher should be a positive amount. Current amount is:$n"
                    )
                  )
                } else {
                  subscription.request(n)
                }
    
              override def cancel(): Unit =
                if (!cancelled.getAndSet(true)) subscription.unsubscribe()
            })
    
          def onNext(result: T): Unit = subscriber.onNext(result)
    
          def onError(e: Throwable): Unit = subscriber.onError(e)
    
          def onComplete(): Unit = subscriber.onComplete()
        })
    }
    

    mgo/MongoEngine.scala

    package sdp.mongo.engine
    
    import java.text.SimpleDateFormat
    import java.util.Calendar
    
    import akka.NotUsed
    import akka.stream.Materializer
    import akka.stream.alpakka.mongodb.scaladsl._
    import akka.stream.scaladsl.{Flow, Source}
    import org.bson.conversions.Bson
    import org.mongodb.scala.bson.collection.immutable.Document
    import org.mongodb.scala.bson.{BsonArray, BsonBinary}
    import org.mongodb.scala.model._
    import org.mongodb.scala.{MongoClient, _}
    import protobuf.bytes.Converter._
    import sdp.file.Streaming._
    import sdp.logging.LogSupport
    
    import scala.collection.JavaConverters._
    import scala.concurrent._
    import scala.concurrent.duration._
    
    object MGOClasses {
      type MGO_ACTION_TYPE = Int
      val MGO_QUERY        = 0
      val MGO_UPDATE       = 1
      val MGO_ADMIN        = 2
    
      /*  org.mongodb.scala.FindObservable
        import com.mongodb.async.client.FindIterable
        val resultDocType = FindIterable[Document]
        val resultOption = FindObservable(resultDocType)
          .maxScan(...)
        .limit(...)
        .sort(...)
        .project(...) */
    
      type FOD_TYPE       = Int
      val FOD_FIRST       = 0  //def first(): SingleObservable[TResult], return the first item
      val FOD_FILTER      = 1  //def filter(filter: Bson): FindObservable[TResult]
      val FOD_LIMIT       = 2  //def limit(limit: Int): FindObservable[TResult]
      val FOD_SKIP        = 3  //def skip(skip: Int): FindObservable[TResult]
      val FOD_PROJECTION  = 4  //def projection(projection: Bson): FindObservable[TResult]
      //Sets a document describing the fields to return for all matching documents
      val FOD_SORT        = 5  //def sort(sort: Bson): FindObservable[TResult]
      val FOD_PARTIAL     = 6  //def partial(partial: Boolean): FindObservable[TResult]
      //Get partial results from a sharded cluster if one or more shards are unreachable (instead of throwing an error)
      val FOD_CURSORTYPE  = 7  //def cursorType(cursorType: CursorType): FindObservable[TResult]
      //Sets the cursor type
      val FOD_HINT        = 8  //def hint(hint: Bson): FindObservable[TResult]
      //Sets the hint for which index to use. A null value means no hint is set
      val FOD_MAX         = 9  //def max(max: Bson): FindObservable[TResult]
      //Sets the exclusive upper bound for a specific index. A null value means no max is set
      val FOD_MIN         = 10 //def min(min: Bson): FindObservable[TResult]
      //Sets the minimum inclusive lower bound for a specific index. A null value means no max is set
      val FOD_RETURNKEY   = 11 //def returnKey(returnKey: Boolean): FindObservable[TResult]
      //Sets the returnKey. If true the find operation will return only the index keys in the resulting documents
      val FOD_SHOWRECORDID=12  //def showRecordId(showRecordId: Boolean): FindObservable[TResult]
      //Sets the showRecordId. Set to true to add a field `$recordId` to the returned documents
    
      case class ResultOptions(
                                optType: FOD_TYPE,
                                bson: Option[Bson] = None,
                                value: Int = 0 ){
        def toProto = new sdp.grpc.services.ProtoMGOResultOption(
          optType = this.optType,
          bsonParam = this.bson.map {b => sdp.grpc.services.ProtoMGOBson(marshal(b))},
          valueParam = this.value
        )
        def toFindObservable: FindObservable[Document] => FindObservable[Document] = find => {
          optType match {
            case  FOD_FIRST        => find
            case  FOD_FILTER       => find.filter(bson.get)
            case  FOD_LIMIT        => find.limit(value)
            case  FOD_SKIP         => find.skip(value)
            case  FOD_PROJECTION   => find.projection(bson.get)
            case  FOD_SORT         => find.sort(bson.get)
            case  FOD_PARTIAL      => find.partial(value != 0)
            case  FOD_CURSORTYPE   => find
            case  FOD_HINT         => find.hint(bson.get)
            case  FOD_MAX          => find.max(bson.get)
            case  FOD_MIN          => find.min(bson.get)
            case  FOD_RETURNKEY    => find.returnKey(value != 0)
            case  FOD_SHOWRECORDID => find.showRecordId(value != 0)
    
          }
        }
      }
      object ResultOptions {
        def fromProto(msg: sdp.grpc.services.ProtoMGOResultOption) = new ResultOptions(
          optType = msg.optType,
          bson = msg.bsonParam.map(b => unmarshal[Bson](b.bson)),
          value = msg.valueParam
        )
    
      }
    
      trait MGOCommands
    
      object MGOCommands {
    
        case class Count(filter: Option[Bson] = None, options: Option[Any] = None) extends MGOCommands
    
        case class Distict(fieldName: String, filter: Option[Bson] = None) extends MGOCommands
    
        /*  org.mongodb.scala.FindObservable
        import com.mongodb.async.client.FindIterable
        val resultDocType = FindIterable[Document]
        val resultOption = FindObservable(resultDocType)
          .maxScan(...)
        .limit(...)
        .sort(...)
        .project(...) */
        case class Find(filter: Option[Bson] = None,
                           andThen: Seq[ResultOptions] = Seq.empty[ResultOptions],
                           firstOnly: Boolean = false) extends MGOCommands
    
        case class Aggregate(pipeLine: Seq[Bson]) extends MGOCommands
    
        case class MapReduce(mapFunction: String, reduceFunction: String) extends MGOCommands
    
        case class Insert(newdocs: Seq[Document], options: Option[Any] = None) extends MGOCommands
    
        case class Delete(filter: Bson, options: Option[Any] = None, onlyOne: Boolean = false) extends MGOCommands
    
        case class Replace(filter: Bson, replacement: Document, options: Option[Any] = None) extends MGOCommands
    
        case class Update(filter: Bson, update: Bson, options: Option[Any] = None, onlyOne: Boolean = false) extends MGOCommands
    
    
        case class BulkWrite(commands: List[WriteModel[Document]], options: Option[Any] = None) extends MGOCommands
    
      }
    
      object MGOAdmins {
    
        case class DropCollection(collName: String) extends MGOCommands
    
        case class CreateCollection(collName: String, options: Option[Any] = None) extends MGOCommands
    
        case class ListCollection(dbName: String) extends MGOCommands
    
        case class CreateView(viewName: String, viewOn: String, pipeline: Seq[Bson], options: Option[Any] = None) extends MGOCommands
    
        case class CreateIndex(key: Bson, options: Option[Any] = None) extends MGOCommands
    
        case class DropIndexByName(indexName: String, options: Option[Any] = None) extends MGOCommands
    
        case class DropIndexByKey(key: Bson, options: Option[Any] = None) extends MGOCommands
    
        case class DropAllIndexes(options: Option[Any] = None) extends MGOCommands
    
      }
    
      case class MGOContext(
                             dbName: String,
                             collName: String,
                             actionType: MGO_ACTION_TYPE = MGO_QUERY,
                             action: Option[MGOCommands] = None,
                             actionOptions: Option[Any] = None,
                             actionTargets: Seq[String] = Nil
                           ) {
        ctx =>
        def setDbName(name: String): MGOContext = ctx.copy(dbName = name)
    
        def setCollName(name: String): MGOContext = ctx.copy(collName = name)
    
        def setActionType(at: MGO_ACTION_TYPE): MGOContext = ctx.copy(actionType = at)
    
        def setCommand(cmd: MGOCommands): MGOContext  = ctx.copy(action = Some(cmd))
    
        def toSomeProto = MGOProtoConversion.ctxToProto(this)
    
      }
    
      object MGOContext {
        def apply(db: String, coll: String) = new MGOContext(db, coll)
        def fromProto(proto: sdp.grpc.services.ProtoMGOContext): MGOContext =
          MGOProtoConversion.ctxFromProto(proto)
      }
    
      case class MGOBatContext(contexts: Seq[MGOContext], tx: Boolean = false) {
        ctxs =>
        def setTx(txopt: Boolean): MGOBatContext = ctxs.copy(tx = txopt)
        def appendContext(ctx: MGOContext): MGOBatContext =
          ctxs.copy(contexts = contexts :+ ctx)
      }
    
      object MGOBatContext {
        def apply(ctxs: Seq[MGOContext], tx: Boolean = false) = new MGOBatContext(ctxs,tx)
      }
    
      type MGODate = java.util.Date
      def mgoDate(yyyy: Int, mm: Int, dd: Int): MGODate = {
        val ca = Calendar.getInstance()
        ca.set(yyyy,mm,dd)
        ca.getTime()
      }
      def mgoDateTime(yyyy: Int, mm: Int, dd: Int, hr: Int, min: Int, sec: Int): MGODate = {
        val ca = Calendar.getInstance()
        ca.set(yyyy,mm,dd,hr,min,sec)
        ca.getTime()
      }
      def mgoDateTimeNow: MGODate = {
        val ca = Calendar.getInstance()
        ca.getTime
      }
    
    
      def mgoDateToString(dt: MGODate, formatString: String): String = {
        val fmt= new SimpleDateFormat(formatString)
        fmt.format(dt)
      }
    
      type MGOBlob = BsonBinary
      type MGOArray = BsonArray
    
      def fileToMGOBlob(fileName: String, timeOut: FiniteDuration = 60 seconds)(
        implicit mat: Materializer) = FileToByteArray(fileName,timeOut)
    
      def mgoBlobToFile(blob: MGOBlob, fileName: String)(
        implicit mat: Materializer) =  ByteArrayToFile(blob.getData,fileName)
    
      def mgoGetStringOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getString(fieldName))
        else None
      }
      def mgoGetIntOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getInteger(fieldName))
        else None
      }
      def mgoGetLonggOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getLong(fieldName))
        else None
      }
      def mgoGetDoubleOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getDouble(fieldName))
        else None
      }
      def mgoGetBoolOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getBoolean(fieldName))
        else None
      }
      def mgoGetDateOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          Some(doc.getDate(fieldName))
        else None
      }
      def mgoGetBlobOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          doc.get(fieldName).asInstanceOf[Option[MGOBlob]]
        else None
      }
      def mgoGetArrayOrNone(doc: Document, fieldName: String) = {
        if (doc.keySet.contains(fieldName))
          doc.get(fieldName).asInstanceOf[Option[MGOArray]]
        else None
      }
    
      def mgoArrayToDocumentList(arr: MGOArray): scala.collection.immutable.List[org.bson.BsonDocument] = {
        (arr.getValues.asScala.toList)
          .asInstanceOf[scala.collection.immutable.List[org.bson.BsonDocument]]
      }
    
      type MGOFilterResult = FindObservable[Document] => FindObservable[Document]
    }
    
    
    object MGOEngine extends LogSupport {
    
      import MGOClasses._
      import MGOAdmins._
      import MGOCommands._
      import sdp.result.DBOResult._
      import com.mongodb.reactivestreams.client.MongoClients
    
      object TxUpdateMode {
        private def mgoTxUpdate(ctxs: MGOBatContext, observable: SingleObservable[ClientSession])(
                  implicit client: MongoClient, ec: ExecutionContext): SingleObservable[ClientSession] = {
          log.info(s"mgoTxUpdate> calling ...")
          observable.map(clientSession => {
    
            val transactionOptions =
              TransactionOptions.builder()
                .readConcern(ReadConcern.SNAPSHOT)
                .writeConcern(WriteConcern.MAJORITY).build()
    
            clientSession.startTransaction(transactionOptions)
    /*
            val fut = Future.traverse(ctxs.contexts) { ctx =>
              mgoUpdateObservable[Completed](ctx).map(identity).toFuture()
            }
            Await.ready(fut, 3 seconds) */
    
            ctxs.contexts.foreach { ctx =>
              mgoUpdateObservable[Completed](ctx).map(identity).toFuture()
            }
            clientSession
          })
        }
    
        private def commitAndRetry(observable: SingleObservable[Completed]): SingleObservable[Completed] = {
          log.info(s"commitAndRetry> calling ...")
          observable.recoverWith({
            case e: MongoException if e.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL) => {
              log.warn("commitAndRetry> UnknownTransactionCommitResult, retrying commit operation ...")
              commitAndRetry(observable)
            }
            case e: Exception => {
              log.error(s"commitAndRetry> Exception during commit ...: $e")
              throw e
            }
          })
        }
    
        private def runTransactionAndRetry(observable: SingleObservable[Completed]): SingleObservable[Completed] = {
          log.info(s"runTransactionAndRetry> calling ...")
          observable.recoverWith({
            case e: MongoException if e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL) => {
              log.warn("runTransactionAndRetry> TransientTransactionError, aborting transaction and retrying ...")
              runTransactionAndRetry(observable)
            }
          })
        }
    
        def mgoTxBatch(ctxs: MGOBatContext)(
                implicit client: MongoClient, ec: ExecutionContext): DBOResult[Completed] = {
    
          log.info(s"mgoTxBatch>  MGOBatContext: ${ctxs}")
    
          val updateObservable: Observable[ClientSession] = mgoTxUpdate(ctxs, client.startSession())
          val commitTransactionObservable: SingleObservable[Completed] =
            updateObservable.flatMap(clientSession => clientSession.commitTransaction())
          val commitAndRetryObservable: SingleObservable[Completed] = commitAndRetry(commitTransactionObservable)
    
          runTransactionAndRetry(commitAndRetryObservable)
    
          valueToDBOResult(Completed())
    
        }
      }
    
    
      def mgoUpdateBatch(ctxs: MGOBatContext)(implicit client: MongoClient, ec: ExecutionContext): DBOResult[Completed] = {
        log.info(s"mgoUpdateBatch>  MGOBatContext: ${ctxs}")
        if (ctxs.tx) {
            TxUpdateMode.mgoTxBatch(ctxs)
          } else {
    /*
            val fut = Future.traverse(ctxs.contexts) { ctx =>
              mgoUpdate[Completed](ctx).map(identity) }
    
            Await.ready(fut, 3 seconds)
            Future.successful(new Completed) */
            ctxs.contexts.foreach { ctx =>
              mgoUpdate[Completed](ctx).map(identity) }
    
             valueToDBOResult(Completed())
          }
    
      }
    
      def mongoStream(ctx: MGOContext)(
        implicit client: MongoClient, ec: ExecutionContextExecutor): Source[Document, NotUsed] = {
    
        log.info(s"mongoStream>  MGOContext: ${ctx}")
    
        ObservableToPublisher
    
        def toResultOption(rts: Seq[ResultOptions]): FindObservable[Document] => FindObservable[Document] = findObj =>
          rts.foldRight(findObj)((a,b) => a.toFindObservable(b))
    
        val db = client.getDatabase(ctx.dbName)
        val coll = db.getCollection(ctx.collName)
        if ( ctx.action == None) {
          log.error(s"mongoStream> uery action cannot be null!")
          throw new IllegalArgumentException("query action cannot be null!")
        }
        try {
          ctx.action.get match {
            case Find(None, Nil, false) => //FindObservable
              MongoSource(ObservableToPublisher(coll.find()))
            case Find(None, Nil, true) => //FindObservable
              MongoSource(ObservableToPublisher(coll.find().first()))
            case Find(Some(filter), Nil, false) => //FindObservable
              MongoSource(ObservableToPublisher(coll.find(filter)))
            case Find(Some(filter), Nil, true) => //FindObservable
              MongoSource(ObservableToPublisher(coll.find(filter).first()))
            case Find(None, sro, _) => //FindObservable
              val next = toResultOption(sro)
              MongoSource(ObservableToPublisher(next(coll.find[Document]())))
            case Find(Some(filter), sro, _) => //FindObservable
              val next = toResultOption(sro)
              MongoSource(ObservableToPublisher(next(coll.find[Document](filter))))
            case _ =>
              log.error(s"mongoStream> unsupported streaming query [${ctx.action.get}]")
              throw new RuntimeException(s"mongoStream> unsupported streaming query [${ctx.action.get}]")
    
          }
        }
        catch { case e: Exception =>
          log.error(s"mongoStream> runtime error: ${e.getMessage}")
          throw new RuntimeException(s"mongoStream> Error: ${e.getMessage}")
        }
    
      }
    
    
      // T => FindIterable  e.g List[Document]
      def mgoQuery[T](ctx: MGOContext, Converter: Option[Document => Any] = None)(implicit client: MongoClient): DBOResult[T] = {
        log.info(s"mgoQuery>  MGOContext: ${ctx}")
    
        val db = client.getDatabase(ctx.dbName)
        val coll = db.getCollection(ctx.collName)
    
        def toResultOption(rts: Seq[ResultOptions]): FindObservable[Document] => FindObservable[Document] = findObj =>
          rts.foldRight(findObj)((a,b) => a.toFindObservable(b))
    
    
        if ( ctx.action == None) {
          log.error(s"mgoQuery> uery action cannot be null!")
          Left(new IllegalArgumentException("query action cannot be null!"))
        }
        try {
          ctx.action.get match {
            /* count */
            case Count(Some(filter), Some(opt)) => //SingleObservable
              coll.countDocuments(filter, opt.asInstanceOf[CountOptions])
                .toFuture().asInstanceOf[Future[T]]
            case Count(Some(filter), None) => //SingleObservable
              coll.countDocuments(filter).toFuture()
                .asInstanceOf[Future[T]]
            case Count(None, None) => //SingleObservable
              coll.countDocuments().toFuture()
                .asInstanceOf[Future[T]]
            /* distinct */
            case Distict(field, Some(filter)) => //DistinctObservable
              coll.distinct(field, filter).toFuture()
                .asInstanceOf[Future[T]]
            case Distict(field, None) => //DistinctObservable
              coll.distinct((field)).toFuture()
                .asInstanceOf[Future[T]]
            /* find */
            case Find(None, Nil, false) => //FindObservable
              if (Converter == None) coll.find().toFuture().asInstanceOf[Future[T]]
              else coll.find().map(Converter.get).toFuture().asInstanceOf[Future[T]]
            case Find(None, Nil, true) => //FindObservable
              if (Converter == None) coll.find().first().head().asInstanceOf[Future[T]]
              else coll.find().first().map(Converter.get).head().asInstanceOf[Future[T]]
            case Find(Some(filter), Nil, false) => //FindObservable
              if (Converter == None) coll.find(filter).toFuture().asInstanceOf[Future[T]]
              else coll.find(filter).map(Converter.get).toFuture().asInstanceOf[Future[T]]
            case Find(Some(filter), Nil, true) => //FindObservable
              if (Converter == None) coll.find(filter).first().head().asInstanceOf[Future[T]]
              else coll.find(filter).first().map(Converter.get).head().asInstanceOf[Future[T]]
            case Find(None, sro, _) => //FindObservable
              val next = toResultOption(sro)
              if (Converter == None) next(coll.find[Document]()).toFuture().asInstanceOf[Future[T]]
              else next(coll.find[Document]()).map(Converter.get).toFuture().asInstanceOf[Future[T]]
            case Find(Some(filter), sro, _) => //FindObservable
              val next = toResultOption(sro)
              if (Converter == None) next(coll.find[Document](filter)).toFuture().asInstanceOf[Future[T]]
              else next(coll.find[Document](filter)).map(Converter.get).toFuture().asInstanceOf[Future[T]]
            /* aggregate AggregateObservable*/
            case Aggregate(pline) => coll.aggregate(pline).toFuture().asInstanceOf[Future[T]]
            /* mapReduce MapReduceObservable*/
            case MapReduce(mf, rf) => coll.mapReduce(mf, rf).toFuture().asInstanceOf[Future[T]]
            /* list collection */
            case ListCollection(dbName) => //ListConllectionObservable
              client.getDatabase(dbName).listCollections().toFuture().asInstanceOf[Future[T]]
    
          }
        }
        catch { case e: Exception =>
          log.error(s"mgoQuery> runtime error: ${e.getMessage}")
          Left(new RuntimeException(s"mgoQuery> Error: ${e.getMessage}"))
        }
      }
      //T => Completed, result.UpdateResult, result.DeleteResult
      def mgoUpdate[T](ctx: MGOContext)(implicit client: MongoClient): DBOResult[T] =
        try {
          mgoUpdateObservable[T](ctx).toFuture()
        }
        catch { case e: Exception =>
          log.error(s"mgoUpdate> runtime error: ${e.getMessage}")
          Left(new RuntimeException(s"mgoUpdate> Error: ${e.getMessage}"))
        }
    
      def mgoUpdateObservable[T](ctx: MGOContext)(implicit client: MongoClient): SingleObservable[T] = {
        log.info(s"mgoUpdateObservable>  MGOContext: ${ctx}")
    
        val db = client.getDatabase(ctx.dbName)
        val coll = db.getCollection(ctx.collName)
        if ( ctx.action == None) {
          log.error(s"mgoUpdateObservable> uery action cannot be null!")
          throw new IllegalArgumentException("mgoUpdateObservable> query action cannot be null!")
        }
        try {
          ctx.action.get match {
            /* insert */
            case Insert(docs, Some(opt)) => //SingleObservable[Completed]
              if (docs.size > 1)
                coll.insertMany(docs, opt.asInstanceOf[InsertManyOptions]).asInstanceOf[SingleObservable[T]]
              else coll.insertOne(docs.head, opt.asInstanceOf[InsertOneOptions]).asInstanceOf[SingleObservable[T]]
            case Insert(docs, None) => //SingleObservable
              if (docs.size > 1) coll.insertMany(docs).asInstanceOf[SingleObservable[T]]
              else coll.insertOne(docs.head).asInstanceOf[SingleObservable[T]]
            /* delete */
            case Delete(filter, None, onlyOne) => //SingleObservable
              if (onlyOne) coll.deleteOne(filter).asInstanceOf[SingleObservable[T]]
              else coll.deleteMany(filter).asInstanceOf[SingleObservable[T]]
            case Delete(filter, Some(opt), onlyOne) => //SingleObservable
              if (onlyOne) coll.deleteOne(filter, opt.asInstanceOf[DeleteOptions]).asInstanceOf[SingleObservable[T]]
              else coll.deleteMany(filter, opt.asInstanceOf[DeleteOptions]).asInstanceOf[SingleObservable[T]]
            /* replace */
            case Replace(filter, replacement, None) => //SingleObservable
              coll.replaceOne(filter, replacement).asInstanceOf[SingleObservable[T]]
            case Replace(filter, replacement, Some(opt)) => //SingleObservable
              coll.replaceOne(filter, replacement, opt.asInstanceOf[ReplaceOptions]).asInstanceOf[SingleObservable[T]]
            /* update */
            case Update(filter, update, None, onlyOne) => //SingleObservable
              if (onlyOne) coll.updateOne(filter, update).asInstanceOf[SingleObservable[T]]
              else coll.updateMany(filter, update).asInstanceOf[SingleObservable[T]]
            case Update(filter, update, Some(opt), onlyOne) => //SingleObservable
              if (onlyOne) coll.updateOne(filter, update, opt.asInstanceOf[UpdateOptions]).asInstanceOf[SingleObservable[T]]
              else coll.updateMany(filter, update, opt.asInstanceOf[UpdateOptions]).asInstanceOf[SingleObservable[T]]
            /* bulkWrite */
            case BulkWrite(commands, None) => //SingleObservable
              coll.bulkWrite(commands).asInstanceOf[SingleObservable[T]]
            case BulkWrite(commands, Some(opt)) => //SingleObservable
              coll.bulkWrite(commands, opt.asInstanceOf[BulkWriteOptions]).asInstanceOf[SingleObservable[T]]
          }
        }
        catch { case e: Exception =>
          log.error(s"mgoUpdateObservable> runtime error: ${e.getMessage}")
          throw new RuntimeException(s"mgoUpdateObservable> Error: ${e.getMessage}")
        }
      }
    
      def mgoAdmin(ctx: MGOContext)(implicit client: MongoClient): DBOResult[Completed] = {
        log.info(s"mgoAdmin>  MGOContext: ${ctx}")
    
        val db = client.getDatabase(ctx.dbName)
        val coll = db.getCollection(ctx.collName)
        if ( ctx.action == None) {
          log.error(s"mgoAdmin> uery action cannot be null!")
          Left(new IllegalArgumentException("mgoAdmin> query action cannot be null!"))
        }
        try {
          ctx.action.get match {
            /* drop collection */
            case DropCollection(collName) => //SingleObservable
              val coll = db.getCollection(collName)
              coll.drop().toFuture()
            /* create collection */
            case CreateCollection(collName, None) => //SingleObservable
              db.createCollection(collName).toFuture()
            case CreateCollection(collName, Some(opt)) => //SingleObservable
              db.createCollection(collName, opt.asInstanceOf[CreateCollectionOptions]).toFuture()
            /* list collection
          case ListCollection(dbName) =>   //ListConllectionObservable
            client.getDatabase(dbName).listCollections().toFuture().asInstanceOf[Future[T]]
            */
            /* create view */
            case CreateView(viewName, viewOn, pline, None) => //SingleObservable
              db.createView(viewName, viewOn, pline).toFuture()
            case CreateView(viewName, viewOn, pline, Some(opt)) => //SingleObservable
              db.createView(viewName, viewOn, pline, opt.asInstanceOf[CreateViewOptions]).toFuture()
            /* create index */
            case CreateIndex(key, None) => //SingleObservable
              coll.createIndex(key).toFuture().asInstanceOf[Future[Completed]] //   asInstanceOf[SingleObservable[Completed]]
            case CreateIndex(key, Some(opt)) => //SingleObservable
              coll.createIndex(key, opt.asInstanceOf[IndexOptions]).asInstanceOf[Future[Completed]] // asInstanceOf[SingleObservable[Completed]]
            /* drop index */
            case DropIndexByName(indexName, None) => //SingleObservable
              coll.dropIndex(indexName).toFuture()
            case DropIndexByName(indexName, Some(opt)) => //SingleObservable
              coll.dropIndex(indexName, opt.asInstanceOf[DropIndexOptions]).toFuture()
            case DropIndexByKey(key, None) => //SingleObservable
              coll.dropIndex(key).toFuture()
            case DropIndexByKey(key, Some(opt)) => //SingleObservable
              coll.dropIndex(key, opt.asInstanceOf[DropIndexOptions]).toFuture()
            case DropAllIndexes(None) => //SingleObservable
              coll.dropIndexes().toFuture()
            case DropAllIndexes(Some(opt)) => //SingleObservable
              coll.dropIndexes(opt.asInstanceOf[DropIndexOptions]).toFuture()
          }
        }
        catch { case e: Exception =>
          log.error(s"mgoAdmin> runtime error: ${e.getMessage}")
          throw new RuntimeException(s"mgoAdmin> Error: ${e.getMessage}")
        }
    
      }
    
     /*
        def mgoExecute[T](ctx: MGOContext)(implicit client: MongoClient): Future[T] = {
        val db = client.getDatabase(ctx.dbName)
        val coll = db.getCollection(ctx.collName)
        ctx.action match {
          /* count */
          case Count(Some(filter), Some(opt)) =>   //SingleObservable
            coll.countDocuments(filter, opt.asInstanceOf[CountOptions])
              .toFuture().asInstanceOf[Future[T]]
          case Count(Some(filter), None) =>        //SingleObservable
            coll.countDocuments(filter).toFuture()
              .asInstanceOf[Future[T]]
          case Count(None, None) =>                //SingleObservable
            coll.countDocuments().toFuture()
              .asInstanceOf[Future[T]]
          /* distinct */
          case Distict(field, Some(filter)) =>     //DistinctObservable
            coll.distinct(field, filter).toFuture()
              .asInstanceOf[Future[T]]
          case Distict(field, None) =>             //DistinctObservable
            coll.distinct((field)).toFuture()
              .asInstanceOf[Future[T]]
          /* find */
          case Find(None, None, optConv, false) =>  //FindObservable
            if (optConv == None) coll.find().toFuture().asInstanceOf[Future[T]]
            else coll.find().map(optConv.get).toFuture().asInstanceOf[Future[T]]
          case Find(None, None, optConv, true) =>   //FindObservable
            if (optConv == None) coll.find().first().head().asInstanceOf[Future[T]]
            else coll.find().first().map(optConv.get).head().asInstanceOf[Future[T]]
          case Find(Some(filter), None, optConv, false) =>   //FindObservable
            if (optConv == None) coll.find(filter).toFuture().asInstanceOf[Future[T]]
            else coll.find(filter).map(optConv.get).toFuture().asInstanceOf[Future[T]]
          case Find(Some(filter), None, optConv, true) =>   //FindObservable
            if (optConv == None) coll.find(filter).first().head().asInstanceOf[Future[T]]
            else coll.find(filter).first().map(optConv.get).head().asInstanceOf[Future[T]]
          case Find(None, Some(next), optConv, _) =>   //FindObservable
            if (optConv == None) next(coll.find[Document]()).toFuture().asInstanceOf[Future[T]]
            else next(coll.find[Document]()).map(optConv.get).toFuture().asInstanceOf[Future[T]]
          case Find(Some(filter), Some(next), optConv, _) =>  //FindObservable
            if (optConv == None) next(coll.find[Document](filter)).toFuture().asInstanceOf[Future[T]]
            else next(coll.find[Document](filter)).map(optConv.get).toFuture().asInstanceOf[Future[T]]
          /* aggregate AggregateObservable*/
          case Aggregate(pline) => coll.aggregate(pline).toFuture().asInstanceOf[Future[T]]
          /* mapReduce MapReduceObservable*/
          case MapReduce(mf, rf) => coll.mapReduce(mf, rf).toFuture().asInstanceOf[Future[T]]
          /* insert */
          case Insert(docs, Some(opt)) =>                  //SingleObservable[Completed]
            if (docs.size > 1) coll.insertMany(docs, opt.asInstanceOf[InsertManyOptions]).toFuture()
              .asInstanceOf[Future[T]]
            else coll.insertOne(docs.head, opt.asInstanceOf[InsertOneOptions]).toFuture()
              .asInstanceOf[Future[T]]
          case Insert(docs, None) =>                       //SingleObservable
            if (docs.size > 1) coll.insertMany(docs).toFuture().asInstanceOf[Future[T]]
            else coll.insertOne(docs.head).toFuture().asInstanceOf[Future[T]]
          /* delete */
          case Delete(filter, None, onlyOne) =>            //SingleObservable
            if (onlyOne) coll.deleteOne(filter).toFuture().asInstanceOf[Future[T]]
            else coll.deleteMany(filter).toFuture().asInstanceOf[Future[T]]
          case Delete(filter, Some(opt), onlyOne) =>       //SingleObservable
            if (onlyOne) coll.deleteOne(filter, opt.asInstanceOf[DeleteOptions]).toFuture().asInstanceOf[Future[T]]
            else coll.deleteMany(filter, opt.asInstanceOf[DeleteOptions]).toFuture().asInstanceOf[Future[T]]
          /* replace */
          case Replace(filter, replacement, None) =>        //SingleObservable
            coll.replaceOne(filter, replacement).toFuture().asInstanceOf[Future[T]]
          case Replace(filter, replacement, Some(opt)) =>    //SingleObservable
            coll.replaceOne(filter, replacement, opt.asInstanceOf[UpdateOptions]).toFuture().asInstanceOf[Future[T]]
          /* update */
          case Update(filter, update, None, onlyOne) =>      //SingleObservable
            if (onlyOne) coll.updateOne(filter, update).toFuture().asInstanceOf[Future[T]]
            else coll.updateMany(filter, update).toFuture().asInstanceOf[Future[T]]
          case Update(filter, update, Some(opt), onlyOne) => //SingleObservable
            if (onlyOne) coll.updateOne(filter, update, opt.asInstanceOf[UpdateOptions]).toFuture().asInstanceOf[Future[T]]
            else coll.updateMany(filter, update, opt.asInstanceOf[UpdateOptions]).toFuture().asInstanceOf[Future[T]]
          /* bulkWrite */
          case BulkWrite(commands, None) =>                  //SingleObservable
            coll.bulkWrite(commands).toFuture().asInstanceOf[Future[T]]
          case BulkWrite(commands, Some(opt)) =>             //SingleObservable
            coll.bulkWrite(commands, opt.asInstanceOf[BulkWriteOptions]).toFuture().asInstanceOf[Future[T]]
    
          /* drop collection */
          case DropCollection(collName) =>                   //SingleObservable
            val coll = db.getCollection(collName)
            coll.drop().toFuture().asInstanceOf[Future[T]]
          /* create collection */
          case CreateCollection(collName, None) =>           //SingleObservable
            db.createCollection(collName).toFuture().asInstanceOf[Future[T]]
          case CreateCollection(collName, Some(opt)) =>      //SingleObservable
            db.createCollection(collName, opt.asInstanceOf[CreateCollectionOptions]).toFuture().asInstanceOf[Future[T]]
          /* list collection */
          case ListCollection(dbName) =>   //ListConllectionObservable
            client.getDatabase(dbName).listCollections().toFuture().asInstanceOf[Future[T]]
          /* create view */
          case CreateView(viewName, viewOn, pline, None) =>       //SingleObservable
            db.createView(viewName, viewOn, pline).toFuture().asInstanceOf[Future[T]]
          case CreateView(viewName, viewOn, pline, Some(opt)) =>  //SingleObservable
            db.createView(viewName, viewOn, pline, opt.asInstanceOf[CreateViewOptions]).toFuture().asInstanceOf[Future[T]]
          /* create index */
          case CreateIndex(key, None) =>                     //SingleObservable
            coll.createIndex(key).toFuture().asInstanceOf[Future[T]]
          case CreateIndex(key, Some(opt)) =>                //SingleObservable
            coll.createIndex(key, opt.asInstanceOf[IndexOptions]).toFuture().asInstanceOf[Future[T]]
          /* drop index */
          case DropIndexByName(indexName, None) =>           //SingleObservable
            coll.dropIndex(indexName).toFuture().asInstanceOf[Future[T]]
          case DropIndexByName(indexName, Some(opt)) =>      //SingleObservable
            coll.dropIndex(indexName, opt.asInstanceOf[DropIndexOptions]).toFuture().asInstanceOf[Future[T]]
          case DropIndexByKey(key, None) =>                  //SingleObservable
            coll.dropIndex(key).toFuture().asInstanceOf[Future[T]]
          case DropIndexByKey(key, Some(opt)) =>             //SingleObservable
            coll.dropIndex(key, opt.asInstanceOf[DropIndexOptions]).toFuture().asInstanceOf[Future[T]]
          case DropAllIndexes(None) =>                       //SingleObservable
            coll.dropIndexes().toFuture().asInstanceOf[Future[T]]
          case DropAllIndexes(Some(opt)) =>                  //SingleObservable
            coll.dropIndexes(opt.asInstanceOf[DropIndexOptions]).toFuture().asInstanceOf[Future[T]]
        }
      }
    */
    
    
    }
    
    
    object MongoActionStream {
    
      import MGOClasses._
    
      case class StreamingInsert[A](dbName: String,
                                    collName: String,
                                    converter: A => Document,
                                    parallelism: Int = 1
                                   ) extends MGOCommands
    
      case class StreamingDelete[A](dbName: String,
                                    collName: String,
                                    toFilter: A => Bson,
                                    parallelism: Int = 1,
                                    justOne: Boolean = false
                                   ) extends MGOCommands
    
      case class StreamingUpdate[A](dbName: String,
                                    collName: String,
                                    toFilter: A => Bson,
                                    toUpdate: A => Bson,
                                    parallelism: Int = 1,
                                    justOne: Boolean = false
                                   ) extends MGOCommands
    
    
      case class InsertAction[A](ctx: StreamingInsert[A])(
        implicit mongoClient: MongoClient) {
    
        val database = mongoClient.getDatabase(ctx.dbName)
        val collection = database.getCollection(ctx.collName)
    
        def performOnRow(implicit ec: ExecutionContext): Flow[A, Document, NotUsed] =
          Flow[A].map(ctx.converter)
            .mapAsync(ctx.parallelism)(doc => collection.insertOne(doc).toFuture().map(_ => doc))
      }
    
      case class UpdateAction[A](ctx: StreamingUpdate[A])(
        implicit mongoClient: MongoClient) {
    
        val database = mongoClient.getDatabase(ctx.dbName)
        val collection = database.getCollection(ctx.collName)
    
        def performOnRow(implicit ec: ExecutionContext): Flow[A, A, NotUsed] =
          if (ctx.justOne) {
            Flow[A]
              .mapAsync(ctx.parallelism)(a =>
                collection.updateOne(ctx.toFilter(a), ctx.toUpdate(a)).toFuture().map(_ => a))
          } else
            Flow[A]
              .mapAsync(ctx.parallelism)(a =>
                collection.updateMany(ctx.toFilter(a), ctx.toUpdate(a)).toFuture().map(_ => a))
      }
    
    
      case class DeleteAction[A](ctx: StreamingDelete[A])(
        implicit mongoClient: MongoClient) {
    
        val database = mongoClient.getDatabase(ctx.dbName)
        val collection = database.getCollection(ctx.collName)
    
        def performOnRow(implicit ec: ExecutionContext): Flow[A, A, NotUsed] =
          if (ctx.justOne) {
            Flow[A]
              .mapAsync(ctx.parallelism)(a =>
                collection.deleteOne(ctx.toFilter(a)).toFuture().map(_ => a))
          } else
            Flow[A]
              .mapAsync(ctx.parallelism)(a =>
                collection.deleteMany(ctx.toFilter(a)).toFuture().map(_ => a))
      }
    
    }
    
    object MGOHelpers {
    
      implicit class DocumentObservable[C](val observable: Observable[Document]) extends ImplicitObservable[Document] {
        override val converter: (Document) => String = (doc) => doc.toJson
      }
    
      implicit class GenericObservable[C](val observable: Observable[C]) extends ImplicitObservable[C] {
        override val converter: (C) => String = (doc) => doc.toString
      }
    
      trait ImplicitObservable[C] {
        val observable: Observable[C]
        val converter: (C) => String
    
        def results(): Seq[C] = Await.result(observable.toFuture(), 10 seconds)
    
        def headResult() = Await.result(observable.head(), 10 seconds)
    
        def printResults(initial: String = ""): Unit = {
          if (initial.length > 0) print(initial)
          results().foreach(res => println(converter(res)))
        }
    
        def printHeadResult(initial: String = ""): Unit = println(s"${initial}${converter(headResult())}")
      }
    
      def getResult[T](fut: Future[T], timeOut: Duration = 1 second): T = {
        Await.result(fut, timeOut)
      }
    
      def getResults[T](fut: Future[Iterable[T]], timeOut: Duration = 1 second): Iterable[T] = {
        Await.result(fut, timeOut)
      }
    
      import monix.eval.Task
      import monix.execution.Scheduler.Implicits.global
    
      final class FutureToTask[A](x: => Future[A]) {
        def asTask: Task[A] = Task.deferFuture[A](x)
      }
    
      final class TaskToFuture[A](x: => Task[A]) {
        def asFuture: Future[A] = x.runToFuture
      }
    
    }
    

     

  • 相关阅读:
    pycharm 社区版运行flask app相关配置
    飞冰框架学习记录
    从上一次到现在总结2
    从上一次到今天的总结1
    mybatis 遇到空串无法判断
    Shell 脚本入门
    数据库批量插入数据
    Navicat for mysql 实现数据库自动备份
    自定义校验注解
    C++ 提高编程
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/10948321.html
Copyright © 2011-2022 走看看