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

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

There is a newer version: 0.7
Show newest version
package no.kodeworks.kvarg.actor

import akka.actor._
import akka.cluster.Cluster
import akka.cluster.ClusterEvent._
import akka.pattern._
import akka.util.Timeout
import cats.syntax.option._
import no.kodeworks.kvarg.actor.Booter.Stable
import no.kodeworks.kvarg.message.{InitFailure, InitMessage, InitSuccess}
import no.kodeworks.kvarg.util.Nulls
import shapeless.ops.nat.ToInt

import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import no.kodeworks.kvarg.util._

import scala.language.postfixOps
import scala.util.{Failure, Success, Try}

/*
  * Boot service makes sure actors are initialized in the correct order -
  * that is, they dont ask each other stuff before their dependent actors are in the right state
  */

case class Boot[Booted](name: String, booted: Booted, bootable: Bootable[Booted])

case class BootResult(boot: Boot[_], service: ActorRef, initMessage: InitMessage)

case class Bootable[Booted](
                             boot: (ActorRef, Booted) => Actor,
                             local: Boolean = true,
                             preBoot: Booted => Unit = (_:Booted) => (),
                             dispatcher: Option[String] = None
                           )

trait Booter[Booted] {
  def boot: Future[Booted]
}

object Booter {

  import shapeless._
  import shapeless.ops.hlist.{Fill, Length, Mapper, Zip, ZipWithKeys}
  import shapeless.ops.record.UnzipFields

  import scala.language.dynamics
  import scala.language.experimental.macros

  def apply[Booted](
                     ac: ActorSystem,
                     dispatcher: ExecutionContext = null,
                     timeout: Timeout = 30 seconds,
                     cluster: Option[Cluster] = None,
                     clusterHost: String = "127.0.0.1",
                     clusterPort: Int = 2552,
                     stablePeriod: Timeout = 5 seconds
                   ): MkBooter[Booted] = new MkBooter[Booted](ac, dispatcher, timeout, cluster, clusterHost, clusterPort)


  final class MkBooter[Booted](
                                ac: ActorSystem,
                                dispatcher: ExecutionContext = null,
                                timeout: Timeout = 30 seconds,
                                cluster: Option[Cluster] = None,
                                clusterHost: String = "127.0.0.1",
                                clusterPort: Int = 2552,
                                stablePeriod: Timeout = 5 seconds
                              ) extends Dynamic {
    def applyRecord[
    Bootables <: HList
    , BootedKeys <: HList
    , BootablesValues <: HList
    , BootablesLength <: Nat
    , BootableBooleans <: HList
    , BootableLocals <: HList
    , BootedFields <: HList
    , BootedValues <: HList
    , BootedLength <: Nat
    , ActorSystems <: HList
    , BootBoxes <: HList
    , BootableTimeouts <: HList
    , BootableDispatchers <: HList
    , Booteds <: HList
    , BootedTasks <: HList
    , KeysValuesTasks <: HList
    ](bootables: Bootables)
     (implicit
      bootablesKeyValues: UnzipFields.Aux[Bootables, BootedKeys, BootablesValues]
      , bootedFields: LabelledGeneric.Aux[Booted, BootedFields]
      , bootedLength: Length.Aux[BootedFields, BootedLength]
      , fillBoolean: Fill.Aux[BootedLength, Boolean, BootableBooleans]
      , zipBootableLocals: Zip.Aux[BootablesValues :: BootableBooleans :: HNil, BootableLocals]
      , mapBootablesLocal0: Mapper.Aux[mapBootablesLocal.type, BootableLocals, BootablesValues]
      , bootedLengthToInt: ToInt[BootedLength]
      , actorSystemFill: Fill.Aux[BootedLength, ActorSystem, ActorSystems]
      , bootboxFill: Fill.Aux[BootedLength, ActorRef, BootBoxes]
      , timeoutFill: Fill.Aux[BootedLength, Timeout, BootableTimeouts]
      , dispatcherFill: Fill.Aux[BootedLength, ExecutionContext, BootableDispatchers]
      , bootedKeyValues: UnzipFields.Aux[BootedFields, BootedKeys, BootedValues]
      , bootedValuesAreBootables: LUBConstraint[BootedValues, ActorRef]
      , bootedNulls: Nulls[BootedValues]
      , bootedGen: Generic.Aux[Booted, BootedValues]
      , bootedZip: ZipWithKeys.Aux[BootedKeys, BootedValues, BootedFields]
      , bootedFill: Fill.Aux[BootedLength, Booted, Booteds]
      , bootedTaskNulls: Nulls[Booteds]
      , bootedTasksZip: Zip.Aux[BootedKeys :: BootablesValues :: ActorSystems :: BootBoxes :: BootableTimeouts :: BootableDispatchers :: Booteds :: HNil, KeysValuesTasks]
      , bootedTasks0: Mapper.Aux[bootedTasks.type, KeysValuesTasks, BootedTasks]
     )
    : Booter[Booted] = {
      import ac.log
      val keys = bootablesKeyValues.keys()
      val values = bootablesKeyValues.values(bootables)
      val locals = fillBoolean(cluster.isEmpty)
      val bootableLocals = zipBootableLocals(values :: locals :: HNil)
      val valuesLocals = mapBootablesLocal0(bootableLocals)
      val length = bootedLengthToInt()
      val actorSystems = actorSystemFill(ac)
      val timeoutPerService =
        timeout.duration.toMillis /
          ((length - cluster.toList.length).max(1).toDouble / 2.5).max(1d).toLong millis
      //triple timeout for first elem if cluster active, need time to wait for other nodes to boot
      val timeouts =
        (timeoutFill(timeoutPerService), cluster) match {
          case (to, Some(_)) =>
            listToHList[BootableTimeouts](hlistToList[Timeout](to) match {
              case collection.immutable.::(h, t) => collection.immutable.::(
                Timeout(h.duration.length * 3L, h.duration.unit), t)
              case _ => Nil
            })
          case (to, _) => to
        }

      val dispatchers = dispatcherFill(Option(dispatcher).getOrElse(ac.dispatcher))
      val tasks = bootedTaskNulls()
      val bootedNulls0 = bootedGen.from(bootedNulls())

      new Booter[Booted] {
        override def boot: Future[Booted] = {
          val bootbox = ac.actorOf(Props(new BootBox[Booted](ac, dispatcher, timeout, cluster, clusterHost, clusterPort, stablePeriod)), "bootService")
          val bootboxes = bootboxFill(bootbox)
          val zip = bootedTasksZip(keys :: valuesLocals :: actorSystems :: bootboxes :: timeouts :: dispatchers :: tasks :: HNil)
          val bootedTasks: BootedTasks = bootedTasks0(zip)
          val list = hlistToList[Booted => Future[Booted]](bootedTasks)
          Util.process(list, bootedNulls0)(ac.dispatcher).map { services =>
            ac.stop(bootbox)
            services
          }(ac.dispatcher)
        }
      }
    }

    def applyDynamicNamed(method: String)(rec: Any*): Booter[Booted] = macro RecordMacros.forwardNamedImpl
  }


  object bootedTasks extends Poly1 {
    implicit def apply[
    Key
    , Booted
    , BootedValues <: HList
    ]
    (implicit
     keyLens: MkFieldLens.Aux[Booted, Witness.Aux[Key]#T, ActorRef]
     , bootedGen: Generic.Aux[Booted, BootedValues]
     , bootedNulls: Nulls[BootedValues]
     , nullMerger: NullMerger[BootedValues]
    )
    : Case.Aux[(Key, Bootable[Booted], ActorSystem, ActorRef, Timeout, ExecutionContext, Booted), Booted => Future[Booted]] =
      at[(Key, Bootable[Booted], ActorSystem, ActorRef, Timeout, ExecutionContext, Booted)] {
        case (key, bootable, ac, bootbox, timeout, dispatcher, _) =>
          import ac.log
          prevBooted =>
            bootbox.?(Boot(key.asInstanceOf[Symbol].name, prevBooted, bootable))(timeout.duration).transform {
              case Success(BootResult(_, svc, InitSuccess)) =>
                log.info(s"InitSuccess $key")
                val nextBooted: Booted = keyLens().set(bootedGen.from(bootedNulls()))(svc)
                val booted = bootedGen.from(nullMerger(bootedGen.to(prevBooted), bootedGen.to(nextBooted)))
                Success(booted)
              case Success(x) => Failure[Booted](throw new IllegalStateException(s"Unexpected result from bootService: $x"))
              case f: Failure[Booted] => f
            }(dispatcher)
      }
  }

  object mapBootablesLocal extends Poly1 {
    implicit def apply[Booted]: Case.Aux[(Bootable[Booted], Boolean), Bootable[Booted]] = at[(Bootable[Booted], Boolean)] {
      case (bootable, true) => bootable.copy(local = true)
      case (b, _) => b
    }
  }

  sealed trait NullMerger[Mergeable <: HList] {
    def apply(m0: Mergeable, m1: Mergeable): Mergeable
  }

  object NullMerger {
    def apply[Mergeable <: HList](implicit mergeable: NullMerger[Mergeable]): mergeable.type = mergeable

    implicit def nullMergerHNil: NullMerger[HNil] = new NullMerger[HNil] {
      override def apply(m0: HNil, m1: HNil): HNil = HNil
    }

    implicit def nullMergeHCons[H, T <: HList](implicit t: NullMerger[T]): NullMerger[H :: T] =
      new NullMerger[H :: T] {
        override def apply(m0: H :: T, m1: H :: T): H :: T = {
          val head =
            if (null == m0.head) m1.head
            else if (null == m1.head) m0.head
            else m1.head
          val tail = t(m0.tail, m1.tail)
          head :: tail
        }
      }
  }

  case object Stable

}

class BootBox[Booted]
(
  ac: ActorSystem,
  dispatcher: ExecutionContext = null,
  timeout: Timeout = 30 seconds,
  cluster: Option[Cluster] = None,
  clusterHost: String = "127.0.0.1",
  clusterPort: Int = 2552,
  stablePeriod: Timeout = 5 seconds
)
  extends Actor with ActorLogging {
  val startedAt = System.currentTimeMillis
  var asker: ActorRef = _
  var terminator: Cancellable = _
  var boot: Boot[Booted] = _
  var svc: ActorRef = _
  var stableTimer: Cancellable = _
  var stable = false
  var amUp = false
  var leaderSet = false
  var amLeader = false
  var multipleLeaderChange = false

  override def preStart {
    log.info("BootService born")
    cluster.foreach { c =>
      context.become(clusterBehavior)
      c.subscribe(self, initialStateMode = InitialStateAsSnapshot, classOf[ClusterDomainEvent])
    }
    terminator = context.system.scheduler.scheduleOnce(timeout.duration, self, PoisonPill)(context.dispatcher, self)
  }

  override def postStop {
    log.info("BootService Died")
    //we need to clean up because services may hold references to bootservice which will be serialized upon failover
    Option(stableTimer).foreach(_.cancel)
    stableTimer = null
    Option(terminator).foreach(_.cancel)
    terminator = null
    Option(asker).foreach(_ ! BootResult(boot, svc, InitFailure))
    asker = null
    boot = null
    svc = null
  }

  def doLeader(host: String, port: Int) {
    leaderSet = true
    amLeader = false
    if (host == clusterHost && port == clusterPort) {
      log.info("\n\n----==== THIS NODE IS LEADER ====----\n")
      amLeader = true
    } else {
      log.info("\n\n----==== THIS NODE IS SLAVE ====----\n")
    }
    doStableTimer()
  }

  def clusterBehavior: Receive = {
    case c@CurrentClusterState(_, _, _, Some(Address(_, _, Some(host), Some(port))), _) =>
      log.debug("Cluster state: {}", c)
      doLeader(host, port)
    case c: CurrentClusterState =>
      log.debug("Cluster state: {}", c)
    case MemberUp(member) =>
      log.info(s"Got memberup after ${System.currentTimeMillis - startedAt} millis")
      member.address match {
        case Address(_, _, Some(host), Some(port))
          if null == stableTimer &&
            host == clusterHost &&
            port == clusterPort =>
          amUp = true
        case x =>
          log.info("Got irrelevant memberup: {}", x)
      }
      doStableTimer()
    case m@(_: MemberLeft | _: MemberExited | _: MemberRemoved) =>
      log.error("Illegal member left/exited/removed during boot: {}", m)
    case l: LeaderChanged if stable =>
      log.info(s"Got leaderchanged after becoming stable, after ${System.currentTimeMillis - startedAt} millis. This is not supported, exiting.")
      context.stop(self)
    case LeaderChanged(Some(Address(_, _, Some(host), Some(port)))) =>
      log.info(s"Got leaderchanged after ${System.currentTimeMillis - startedAt} millis")
      if (leaderSet) {
        log.info("Getting multiple leader changes. This usually means there's already a cluster up and running.")
        multipleLeaderChange = true
      }
      doLeader(host, port)
    case c: ClusterDomainEvent =>
      log.info("Got other clusterdomainevent, TODO eval: " + c)
    case Stable =>
      log.info(s"Got stable after ${System.currentTimeMillis - startedAt} millis")
      stable = true
      doBoot()
    //stable can only be set once during boot, or else we self terminate. This is to preserve simplicity during boot.
    case b: Boot[Booted] =>
      log.info(s"Got boot ${b.name} after ${System.currentTimeMillis - startedAt} millis")
      boot = b
      asker = sender
      doBoot()
    case i: InitMessage
      if amUp
        || boot.bootable.local =>
      asker forward BootResult(boot, sender, i)
      asker = null
      svc = null
    case x =>
      log.error("Unknown error from {}, terminating: {}", sender, x)
      context.stop(self)
  }

  override def receive = {
    case b: Boot[Booted] =>
      boot = b
      asker = sender
      boot.bootable.preBoot(b.booted)
      svc = service(ac, boot.bootable.boot(self, b.booted), boot.bootable.local, boot.name.some, boot.bootable.dispatcher)
    case i: InitMessage if sender.path.name == boot.name =>
      asker forward BootResult(boot, sender, i)
      asker = null
      svc = null
    case x =>
      log.error("Unknown error from {}, terminating: {}", sender, x)
      context.stop(self)
  }

  def doBoot() {
    if (stable && null != boot) {
      log.info(s"Attempting boot of ${boot.name}")
      val curBoot = boot // for failovers
      curBoot.bootable.preBoot(curBoot.booted)
      svc = service(ac, curBoot.bootable.boot(self, curBoot.booted), curBoot.bootable.local, curBoot.name.some, curBoot.bootable.dispatcher)
      if ((multipleLeaderChange
        || !amLeader)
        && !boot.bootable.local) {
        asker ! BootResult(boot, svc, InitSuccess)
        asker = null
        svc = null
      }
    } else {
      log.info("Ignoring boot until stable state is reached: {}", boot)
    }
  }

  def doStableTimer(): Unit = {
    if (null != stableTimer) {
      stableTimer.cancel()
    }
    if (amUp && leaderSet) {
      stableTimer = ac.scheduler.scheduleOnce(stablePeriod.duration, self, Stable)(context.dispatcher)
    }
  }
}

object Util {
  def process[A](list: List[A => Future[A]], acc: A)(implicit ec: ExecutionContext): Future[A] =
    list match {
      case Nil => Future.successful(acc)
      case head :: tail =>
        head(acc).flatMap(
          process(tail, _))
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy