no.kodeworks.kvarg.actor.DbService.scala Maven / Gradle / Ivy
package no.kodeworks.kvarg.actor
import java.net.ConnectException
import akka.actor._
import akka.dispatch.PriorityGenerator
import akka.pattern.pipe
import cats.implicits._
import no.kodeworks.kvarg.actor.DbService._
import no.kodeworks.kvarg.mailbox.UnboundedStablePriorityDequeBasedMailbox
import no.kodeworks.kvarg.message.{InitFailure, InitSuccess}
import no.kodeworks.kvarg.util.{AtLeastOnceDeliveryDefault, Db, _}
import com.typesafe.config.Config
import scala.collection.mutable.ListBuffer
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
//TODO need a way to genereate diff between releases
//TODO might not need AtLeastOnceDelivery after all - it "always" run on the same node as the other singleton services
class DbService(
db0: Db
, dbType: String
, config: Config
, bootService: ActorRef
, dbSchemaCreate: Boolean = true
) extends AtLeastOnceDeliveryDefault with Stash {
import context.dispatcher
val dbCfg = db0.dbCfg
import dbCfg.db
import dbCfg.profile.api._
var upCheck: Cancellable = null
override def preStart {
log.info("Born")
context.become(initing)
val inits = ListBuffer[Future[Any]]()
if (dbSchemaCreate) {
inits += db.run(db0.tableQuerys.map(_.schema).reduce(_ ++ _).create).map { res =>
log.info("Schema created")
res
}
}
//Other db stuff?
val zelf = self
val kontext = context
def suc() {
bootService.!(InitSuccess)(zelf)
kontext.unbecome
}
Future.sequence(inits).mapAll {
case Success(_) =>
log.info("Init done")
suc()
case Failure(x)
if null != x.getCause
&& x.getCause.getClass.isAssignableFrom(classOf[ConnectException]) =>
log.error("Critical database error. Error connecting to database during boot.")
bootService.!(InitFailure)(zelf)
case Failure(x) =>
log.warning("Non-critical db error: {}", x)
suc()
}
}
override def postStop() {
db.close
log.info("Died")
}
def initing() = Actor.emptyBehavior
def down(): Receive = {
case c: DbCommand =>
log.info("down - stashing db command")
stash
case UpCheck =>
doUpCheck
case GoDown => //ignore, multiples may arrive
case GoUp => goUp
case x =>
log.error("Down - unknown message: {} with sender {}", x, sender)
}
def insert(persistables: Seq[Any]): Future[Inserted] = {
log.info("Insert")
val zelf = self
Future.sequence(persistables
.flatMap(per => table(per)
.map(table =>
db.run(table += per)
.mapAll {
case Success(res) => Right(per)
case Failure(x) => Left(per -> x)
}
)))
.map(_.toList.separate)
.map { res =>
if (res._1.nonEmpty) {
log.error(res._1.asInstanceOf[Iterable[(_, Throwable)]].head._2, "Insert db error")
zelf ! GoDown
zelf ! Insert(res._1: _*)
}
res
}
.map(res => Inserted(res._2.toSet, res._1.toMap))
}
override def receive = (super.receive orElse {
case GoDown => goDown
case Load(persistables@_*) =>
val zelf = self
val zender = sender
log.info("Load {}", persistables.mkString(", "))
val queries: Seq[Future[Either[(String, Throwable), (String, Seq[Any])]]] =
persistables
.map(per => Try(db0.tables(per)) match {
case Success(table) => db.run(table.result)
.mapAll {
case Success(res) => Right(per -> res)
case Failure(x) => Left(per -> x)
}
case Failure(x) => Future.successful(Left(per -> x))
})
Future.sequence(queries)
.map(_.toList.separate)
.map { res =>
if (res._1.nonEmpty) {
// zelf ! Load(res._1.map(_._1): _*)
log.error(res._1.asInstanceOf[Iterable[(_, Throwable)]].head._2, "Load db error")
zelf ! GoDown
}
res
}
.map(res => Loaded(res._2.toMap, res._1.toMap))
.pipeTo(zender)
case Insert(persistables@_ *) =>
val zender = sender
insert(persistables)
.pipeTo(zender)
case Upsert(persistable@_ *) =>
val zelf = self
val zender = sender
log.info("Upsert")
Future.sequence(persistable
.flatMap(per => table(per)
.map(table =>
db.run(table.insertOrUpdate(per))
.mapAll {
case Success(res) => Right(per)
case Failure(x) => Left(per -> x)
}
)))
.map(_.toList.separate)
.map { res =>
if (res._1.nonEmpty) {
log.error(res._1.asInstanceOf[Iterable[(_, Throwable)]].head._2, "Upsert db error")
zelf ! GoDown
zelf ! Upsert(res._1: _*)
}
res
}
.map(res => Upserted(res._2.toSet, res._1.toMap))
.pipeTo(zender)
//TODO case delete - will require exposing ids on table map
})
def goDown() {
log.info("Going down, upcheck every 5 seconds")
scheduleUpCheck
context.become(down)
}
def scheduleUpCheck() {
upCheck = context.system.scheduler.scheduleOnce(5 seconds, self, UpCheck)
}
def doUpCheck() {
val zelf = self
db.run(sql"show tables".as[String]).mapAll {
case Success(res) =>
zelf ! GoUp
case Failure(x) =>
log.info("still down, retry in 5 seconds")
scheduleUpCheck
}
}
def goUp() {
log.info("up check ok, going up")
context.unbecome
unstashAll
}
private def table(per: Any) =
db0.tables.get(lowerFirstChar(per.getClass.getSimpleName)).map(tableCast(_, per))
private def tableCast(table: TableQuery[_], per: Any) =
table.asInstanceOf[TableQuery[Table[Any] {
val id: Rep[Option[Long]]
}]]
}
object DbService {
sealed trait DbMessage
sealed trait DbInMessage extends DbMessage
sealed trait DbOutMessage extends DbMessage
sealed trait DbQuery extends DbInMessage
sealed trait DbCommand extends DbInMessage
sealed trait DbControl extends DbMessage
case class Insert(persistables: Any*) extends DbCommand
case class Inserted(
persistables: Set[Any],
errors: Map[Any, Throwable] = Map.empty
) extends DbOutMessage
case class Upsert(persistables: Any*) extends DbCommand
case class Upserted(
persistableIds: Set[Any],
errors: Map[Any, Throwable] = Map.empty
) extends DbOutMessage
case class Load(persistables: String*) extends DbQuery
case class Loaded(
data: Map[String, Seq[Any]],
errors: Map[String, Throwable] = Map.empty
) extends DbOutMessage
case object GoDown extends DbControl
case object GoUp extends DbControl
case object UpCheck extends DbControl
}
class DbMailbox(settings: ActorSystem.Settings, config: Config) extends UnboundedStablePriorityDequeBasedMailbox(
PriorityGenerator {
case x: DbControl => 0
case _ => 1
}, config.getInt("mailbox-capacity"))
© 2015 - 2025 Weber Informatics LLC | Privacy Policy