313 lines
11 KiB
Scala
313 lines
11 KiB
Scala
/*
|
|
* Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com>
|
|
*/
|
|
|
|
package akka.actor
|
|
|
|
import java.util.concurrent.atomic.AtomicReference
|
|
|
|
import scala.annotation.tailrec
|
|
|
|
import com.github.ghik.silencer.silent
|
|
import com.typesafe.config._
|
|
|
|
import akka.annotation.InternalApi
|
|
import akka.routing._
|
|
import akka.util.WildcardIndex
|
|
|
|
object Deploy {
|
|
final val NoDispatcherGiven = ""
|
|
final val NoMailboxGiven = ""
|
|
val local = Deploy(scope = LocalScope)
|
|
|
|
/**
|
|
* INTERNAL API
|
|
*/
|
|
@InternalApi private[akka] final val DispatcherSameAsParent = ".."
|
|
|
|
def apply(
|
|
path: String = "",
|
|
config: Config = ConfigFactory.empty,
|
|
routerConfig: RouterConfig = NoRouter,
|
|
scope: Scope = NoScopeGiven,
|
|
dispatcher: String = Deploy.NoDispatcherGiven,
|
|
mailbox: String = Deploy.NoMailboxGiven): Deploy =
|
|
new Deploy(path, config, routerConfig, scope, dispatcher, mailbox, Set.empty)
|
|
|
|
// for bincomp, pre 2.6 was case class
|
|
def unapply(deploy: Deploy): Option[(String, Config, RouterConfig, Scope, String, String)] =
|
|
Some((deploy.path, deploy.config, deploy.routerConfig, deploy.scope, deploy.dispatcher, deploy.mailbox))
|
|
}
|
|
|
|
/**
|
|
* This class represents deployment configuration for a given actor path. It is
|
|
* marked final in order to guarantee stable merge semantics (i.e. what
|
|
* overrides what in case multiple configuration sources are available) and is
|
|
* fully extensible via its Scope argument, and by the fact that an arbitrary
|
|
* Config section can be passed along with it (which will be merged when merging
|
|
* two Deploys).
|
|
*
|
|
* The path field is used only when inserting the Deploy into a deployer and
|
|
* not needed when just doing deploy-as-you-go:
|
|
*
|
|
* {{{
|
|
* val remoteProps = someProps.withDeploy(Deploy(scope = RemoteScope("someOtherNodeName")))
|
|
* }}}
|
|
*/
|
|
@SerialVersionUID(3L)
|
|
final class Deploy(
|
|
val path: String = "",
|
|
val config: Config = ConfigFactory.empty,
|
|
val routerConfig: RouterConfig = NoRouter,
|
|
val scope: Scope = NoScopeGiven,
|
|
val dispatcher: String = Deploy.NoDispatcherGiven,
|
|
val mailbox: String = Deploy.NoMailboxGiven,
|
|
val tags: Set[String] = Set.empty)
|
|
extends Serializable
|
|
with Product
|
|
with Equals {
|
|
|
|
// for bincomp, pre 2.6 did not have tags
|
|
def this(
|
|
path: String,
|
|
config: Config,
|
|
routerConfig: RouterConfig,
|
|
scope: Scope,
|
|
dispatcher: String,
|
|
mailbox: String) = this(path, config, routerConfig, scope, dispatcher, mailbox, Set.empty)
|
|
|
|
/**
|
|
* Java API to create a Deploy with the given RouterConfig
|
|
*/
|
|
def this(routing: RouterConfig) = this("", ConfigFactory.empty, routing)
|
|
|
|
/**
|
|
* Java API to create a Deploy with the given RouterConfig with Scope
|
|
*/
|
|
def this(routing: RouterConfig, scope: Scope) = this("", ConfigFactory.empty, routing, scope)
|
|
|
|
/**
|
|
* Java API to create a Deploy with the given Scope
|
|
*/
|
|
def this(scope: Scope) = this("", ConfigFactory.empty, NoRouter, scope)
|
|
|
|
/**
|
|
* Do a merge between this and the other Deploy, where values from “this” take
|
|
* precedence. The “path” of the other Deploy is not taken into account. All
|
|
* other members are merged using `X.withFallback(other.X)`.
|
|
*/
|
|
def withFallback(other: Deploy): Deploy = {
|
|
Deploy(
|
|
path,
|
|
config.withFallback(other.config),
|
|
routerConfig.withFallback(other.routerConfig),
|
|
scope.withFallback(other.scope),
|
|
if (dispatcher == Deploy.NoDispatcherGiven) other.dispatcher else dispatcher,
|
|
if (mailbox == Deploy.NoMailboxGiven) other.mailbox else mailbox)
|
|
}
|
|
|
|
def withTags(tags: Set[String]): Deploy =
|
|
new Deploy(path, config, routerConfig, scope, dispatcher, mailbox, tags)
|
|
|
|
// below are for bincomp, pre 2.6 was case class
|
|
def copy(
|
|
path: String = path,
|
|
config: Config = config,
|
|
routerConfig: RouterConfig = routerConfig,
|
|
scope: Scope = scope,
|
|
dispatcher: String = dispatcher,
|
|
mailbox: String = mailbox): Deploy =
|
|
new Deploy(path, config, routerConfig, scope, dispatcher, mailbox, tags)
|
|
|
|
override def productElement(n: Int): Any = n match {
|
|
case 0 => path
|
|
case 1 => config
|
|
case 2 => routerConfig
|
|
case 3 => scope
|
|
case 4 => dispatcher
|
|
case 5 => mailbox
|
|
case 6 => tags
|
|
}
|
|
|
|
override def productArity: Int = 7
|
|
|
|
override def canEqual(that: Any): Boolean = that.isInstanceOf[Deploy]
|
|
|
|
override def equals(other: Any): Boolean = other match {
|
|
case that: Deploy =>
|
|
path == that.path &&
|
|
config == that.config &&
|
|
routerConfig == that.routerConfig &&
|
|
scope == that.scope &&
|
|
dispatcher == that.dispatcher &&
|
|
mailbox == that.mailbox &&
|
|
tags == that.tags
|
|
case _ => false
|
|
}
|
|
|
|
override def hashCode(): Int = {
|
|
val state = Seq[AnyRef](path, config, routerConfig, scope, dispatcher, mailbox, tags)
|
|
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
|
|
}
|
|
|
|
override def toString = s"Deploy($path, $config, $routerConfig, $scope, $dispatcher, $mailbox, $tags)"
|
|
}
|
|
|
|
/**
|
|
* The scope of a [[akka.actor.Deploy]] serves two purposes: as a marker for
|
|
* pattern matching the “scope” (i.e. local/remote/cluster) as well as for
|
|
* extending the information carried by the final Deploy class. Scopes can be
|
|
* used in conjunction with a custom [[akka.actor.ActorRefProvider]], making
|
|
* Akka actors fully extensible.
|
|
*/
|
|
trait Scope {
|
|
|
|
/**
|
|
* When merging [[akka.actor.Deploy]] instances using ``withFallback()`` on
|
|
* the left one, this is propagated to “merging” scopes in the same way.
|
|
* The setup is biased towards preferring the callee over the argument, i.e.
|
|
* ``a.withFallback(b)`` is called expecting that ``a`` should in general take
|
|
* precedence.
|
|
*/
|
|
def withFallback(other: Scope): Scope
|
|
}
|
|
|
|
@silent("@SerialVersionUID has no effect")
|
|
@SerialVersionUID(1L)
|
|
abstract class LocalScope extends Scope
|
|
|
|
/**
|
|
* The Local Scope is the default one, which is assumed on all deployments
|
|
* which do not set a different scope. It is also the only scope handled by
|
|
* the LocalActorRefProvider.
|
|
*/
|
|
@silent("@SerialVersionUID has no effect")
|
|
@SerialVersionUID(1L)
|
|
case object LocalScope extends LocalScope {
|
|
|
|
/**
|
|
* Java API: get the singleton instance
|
|
*/
|
|
def getInstance = this
|
|
|
|
def withFallback(other: Scope): Scope = this
|
|
}
|
|
|
|
/**
|
|
* This is the default value and as such allows overrides.
|
|
*/
|
|
@silent("@SerialVersionUID has no effect")
|
|
@SerialVersionUID(1L)
|
|
abstract class NoScopeGiven extends Scope
|
|
@SerialVersionUID(1L)
|
|
case object NoScopeGiven extends NoScopeGiven {
|
|
def withFallback(other: Scope): Scope = other
|
|
|
|
/**
|
|
* Java API: get the singleton instance
|
|
*/
|
|
def getInstance = this
|
|
}
|
|
|
|
/**
|
|
* Deployer maps actor paths to actor deployments.
|
|
*/
|
|
private[akka] class Deployer(val settings: ActorSystem.Settings, val dynamicAccess: DynamicAccess) {
|
|
|
|
import akka.util.ccompat.JavaConverters._
|
|
|
|
private val resizerEnabled: Config = ConfigFactory.parseString("resizer.enabled=on")
|
|
private val deployments = new AtomicReference(WildcardIndex[Deploy]())
|
|
private val config = settings.config.getConfig("akka.actor.deployment")
|
|
protected val default = config.getConfig("default")
|
|
val routerTypeMapping: Map[String, String] =
|
|
settings.config
|
|
.getConfig("akka.actor.router.type-mapping")
|
|
.root
|
|
.unwrapped
|
|
.asScala
|
|
.collect {
|
|
case (key, value: String) => (key -> value)
|
|
}
|
|
.toMap
|
|
|
|
config.root.asScala
|
|
.flatMap {
|
|
case ("default", _) => None
|
|
case (key, value: ConfigObject) => parseConfig(key, value.toConfig)
|
|
case _ => None
|
|
}
|
|
.foreach(deploy)
|
|
|
|
def lookup(path: ActorPath): Option[Deploy] = lookup(path.elements.drop(1))
|
|
|
|
def lookup(path: Iterable[String]): Option[Deploy] = deployments.get().find(path)
|
|
|
|
def deploy(d: Deploy): Unit = {
|
|
@tailrec def add(path: Array[String], d: Deploy): Unit = {
|
|
val w: WildcardIndex[Deploy] = deployments.get
|
|
for (i <- path.indices) path(i) match {
|
|
case "" => throw InvalidActorNameException(s"Actor name in deployment [${d.path}] must not be empty")
|
|
case el => ActorPath.validatePathElement(el, fullPath = d.path)
|
|
}
|
|
|
|
if (!deployments.compareAndSet(w, w.insert(path, d))) add(path, d)
|
|
}
|
|
|
|
add(d.path.split("/").drop(1), d)
|
|
}
|
|
|
|
def parseConfig(key: String, config: Config): Option[Deploy] = {
|
|
val deployment = config.withFallback(default)
|
|
val router = createRouterConfig(deployment.getString("router"), key, config, deployment)
|
|
val dispatcher = deployment.getString("dispatcher")
|
|
val mailbox = deployment.getString("mailbox")
|
|
Some(Deploy(key, deployment, router, NoScopeGiven, dispatcher, mailbox))
|
|
}
|
|
|
|
/**
|
|
* Factory method for creating `RouterConfig`
|
|
* @param routerType the configured name of the router, or FQCN
|
|
* @param key the full configuration key of the deployment section
|
|
* @param config the user defined config of the deployment, without defaults
|
|
* @param deployment the deployment config, with defaults
|
|
*/
|
|
protected def createRouterConfig(routerType: String, key: String, config: Config, deployment: Config): RouterConfig =
|
|
if (routerType == "from-code") NoRouter
|
|
else {
|
|
// need this for backwards compatibility, resizer enabled when including (parts of) resizer section in the deployment
|
|
val deployment2 =
|
|
if (config.hasPath("resizer") && !deployment.getBoolean("resizer.enabled"))
|
|
resizerEnabled.withFallback(deployment)
|
|
else deployment
|
|
|
|
val fqn = routerTypeMapping.getOrElse(routerType, routerType)
|
|
|
|
def throwCannotInstantiateRouter(args: Seq[(Class[_], AnyRef)], cause: Throwable) =
|
|
throw new IllegalArgumentException(
|
|
s"Cannot instantiate router [$fqn], defined in [$key], " +
|
|
s"make sure it extends [${classOf[RouterConfig]}] and has constructor with " +
|
|
s"[${args(0)._1.getName}] and optional [${args(1)._1.getName}] parameter",
|
|
cause)
|
|
|
|
// first try with Config param, and then with Config and DynamicAccess parameters
|
|
val args1 = List(classOf[Config] -> deployment2)
|
|
val args2 = List(classOf[Config] -> deployment2, classOf[DynamicAccess] -> dynamicAccess)
|
|
dynamicAccess
|
|
.createInstanceFor[RouterConfig](fqn, args1)
|
|
.recover {
|
|
case e @ (_: IllegalArgumentException | _: ConfigException) => throw e
|
|
case e: NoSuchMethodException =>
|
|
dynamicAccess
|
|
.createInstanceFor[RouterConfig](fqn, args2)
|
|
.recover {
|
|
case e @ (_: IllegalArgumentException | _: ConfigException) => throw e
|
|
case _ => throwCannotInstantiateRouter(args2, e)
|
|
}
|
|
.get
|
|
case e => throwCannotInstantiateRouter(args2, e)
|
|
}
|
|
.get
|
|
}
|
|
|
|
}
|