前面我们已经讨论了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://[email protected]: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
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
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 } }