no.kodeworks.kvarg.actor.BootService.scala Maven / Gradle / Ivy
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