All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.actor.DbService.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
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