act #20332 Functionality to automatically load library extensions
This commit is contained in:
parent
2418e610ab
commit
14c08df74a
8 changed files with 209 additions and 53 deletions
|
|
@ -1,4 +1,7 @@
|
||||||
akka {
|
akka {
|
||||||
|
# for the akka.actor.ExtensionSpec
|
||||||
|
library-extensions += "akka.actor.InstanceCountingExtension"
|
||||||
|
|
||||||
actor {
|
actor {
|
||||||
serialize-messages = on
|
serialize-messages = on
|
||||||
warn-about-java-serializer-usage = off
|
warn-about-java-serializer-usage = off
|
||||||
|
|
|
||||||
|
|
@ -19,33 +19,6 @@ import akka.util.Switch
|
||||||
import akka.util.Helpers.ConfigOps
|
import akka.util.Helpers.ConfigOps
|
||||||
import scala.util.control.NoStackTrace
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
class JavaExtensionSpec extends JavaExtension with JUnitSuiteLike
|
|
||||||
|
|
||||||
object TestExtension extends ExtensionId[TestExtension] with ExtensionIdProvider {
|
|
||||||
def lookup = this
|
|
||||||
def createExtension(s: ExtendedActorSystem) = new TestExtension(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains
|
|
||||||
class TestExtension(val system: ExtendedActorSystem) extends Extension
|
|
||||||
|
|
||||||
object FailingTestExtension extends ExtensionId[FailingTestExtension] with ExtensionIdProvider {
|
|
||||||
def lookup = this
|
|
||||||
def createExtension(s: ExtendedActorSystem) = new FailingTestExtension(s)
|
|
||||||
|
|
||||||
class TestException extends IllegalArgumentException("ERR") with NoStackTrace
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains
|
|
||||||
class FailingTestExtension(val system: ExtendedActorSystem) extends Extension {
|
|
||||||
// first time the actor is created
|
|
||||||
val ref = system.actorOf(Props.empty, "uniqueName")
|
|
||||||
// but the extension initialization fails
|
|
||||||
// second time it will throw exception when trying to create actor with same name,
|
|
||||||
// but we want to see the first exception every time
|
|
||||||
throw new FailingTestExtension.TestException
|
|
||||||
}
|
|
||||||
|
|
||||||
object ActorSystemSpec {
|
object ActorSystemSpec {
|
||||||
|
|
||||||
class Waves extends Actor {
|
class Waves extends Actor {
|
||||||
|
|
@ -140,7 +113,6 @@ object ActorSystemSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = s"""
|
val config = s"""
|
||||||
akka.extensions = ["akka.actor.TestExtension"]
|
|
||||||
slow {
|
slow {
|
||||||
type="${classOf[SlowDispatcher].getName}"
|
type="${classOf[SlowDispatcher].getName}"
|
||||||
}"""
|
}"""
|
||||||
|
|
@ -177,23 +149,6 @@ class ActorSystemSpec extends AkkaSpec(ActorSystemSpec.config) with ImplicitSend
|
||||||
shutdown(ActorSystem("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"))
|
shutdown(ActorSystem("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"))
|
||||||
}
|
}
|
||||||
|
|
||||||
"support extensions" in {
|
|
||||||
// TestExtension is configured and should be loaded at startup
|
|
||||||
system.hasExtension(TestExtension) should ===(true)
|
|
||||||
TestExtension(system).system should ===(system)
|
|
||||||
system.extension(TestExtension).system should ===(system)
|
|
||||||
}
|
|
||||||
|
|
||||||
"handle extensions that fail to initialize" in {
|
|
||||||
intercept[FailingTestExtension.TestException] {
|
|
||||||
FailingTestExtension(system)
|
|
||||||
}
|
|
||||||
// same exception should be reported next time
|
|
||||||
intercept[FailingTestExtension.TestException] {
|
|
||||||
FailingTestExtension(system)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"log dead letters" in {
|
"log dead letters" in {
|
||||||
val sys = ActorSystem("LogDeadLetters", ConfigFactory.parseString("akka.loglevel=INFO").withFallback(AkkaSpec.testConf))
|
val sys = ActorSystem("LogDeadLetters", ConfigFactory.parseString("akka.loglevel=INFO").withFallback(AkkaSpec.testConf))
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
140
akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala
Normal file
140
akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2016 Lightbend Inc. <http://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
package akka.actor
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
import akka.testkit.EventFilter
|
||||||
|
import akka.testkit.TestKit._
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import org.scalatest.{Matchers, WordSpec}
|
||||||
|
import org.scalatest.junit.JUnitSuiteLike
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
|
||||||
|
class JavaExtensionSpec extends JavaExtension with JUnitSuiteLike
|
||||||
|
|
||||||
|
object TestExtension extends ExtensionId[TestExtension] with ExtensionIdProvider {
|
||||||
|
def lookup = this
|
||||||
|
def createExtension(s: ExtendedActorSystem) = new TestExtension(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains
|
||||||
|
class TestExtension(val system: ExtendedActorSystem) extends Extension
|
||||||
|
|
||||||
|
object FailingTestExtension extends ExtensionId[FailingTestExtension] with ExtensionIdProvider {
|
||||||
|
def lookup = this
|
||||||
|
def createExtension(s: ExtendedActorSystem) = new FailingTestExtension(s)
|
||||||
|
|
||||||
|
class TestException extends IllegalArgumentException("ERR") with NoStackTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
object InstanceCountingExtension extends ExtensionId[DummyExtensionImpl] with ExtensionIdProvider {
|
||||||
|
val createCount = new AtomicInteger(0)
|
||||||
|
override def createExtension(system: ExtendedActorSystem): DummyExtensionImpl = {
|
||||||
|
createCount.addAndGet(1)
|
||||||
|
new DummyExtensionImpl
|
||||||
|
}
|
||||||
|
override def lookup(): ExtensionId[_ <: Extension] = this
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyExtensionImpl extends Extension
|
||||||
|
|
||||||
|
// Dont't place inside ActorSystemSpec object, since it will not be garbage collected and reference to system remains
|
||||||
|
class FailingTestExtension(val system: ExtendedActorSystem) extends Extension {
|
||||||
|
// first time the actor is created
|
||||||
|
val ref = system.actorOf(Props.empty, "uniqueName")
|
||||||
|
// but the extension initialization fails
|
||||||
|
// second time it will throw exception when trying to create actor with same name,
|
||||||
|
// but we want to see the first exception every time
|
||||||
|
throw new FailingTestExtension.TestException
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionSpec extends WordSpec with Matchers {
|
||||||
|
|
||||||
|
"The ActorSystem extensions support" should {
|
||||||
|
|
||||||
|
"support extensions" in {
|
||||||
|
val config = ConfigFactory.parseString("""akka.extensions = ["akka.actor.TestExtension"]""")
|
||||||
|
val system = ActorSystem("extensions", config)
|
||||||
|
|
||||||
|
// TestExtension is configured and should be loaded at startup
|
||||||
|
system.hasExtension(TestExtension) should ===(true)
|
||||||
|
TestExtension(system).system should ===(system)
|
||||||
|
system.extension(TestExtension).system should ===(system)
|
||||||
|
|
||||||
|
shutdownActorSystem(system)
|
||||||
|
}
|
||||||
|
|
||||||
|
"handle extensions that fail to initialize" in {
|
||||||
|
val system = ActorSystem("extensions")
|
||||||
|
|
||||||
|
intercept[FailingTestExtension.TestException] {
|
||||||
|
FailingTestExtension(system)
|
||||||
|
}
|
||||||
|
// same exception should be reported next time
|
||||||
|
intercept[FailingTestExtension.TestException] {
|
||||||
|
FailingTestExtension(system)
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownActorSystem(system)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
"fail the actor system if an extension listed in akka.extensions fails to start" in {
|
||||||
|
intercept[RuntimeException]{
|
||||||
|
val system = ActorSystem("failing", ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
akka.extensions = ["akka.actor.FailingTestExtension"]
|
||||||
|
"""))
|
||||||
|
|
||||||
|
shutdownActorSystem(system)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"log an error if an extension listed in akka.extensions cannot be loaded" in {
|
||||||
|
val system = ActorSystem("failing", ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
akka.extensions = ["akka.actor.MissingExtension"]
|
||||||
|
"""))
|
||||||
|
EventFilter.error("While trying to load extension [akka.actor.MissingExtension], skipping...").intercept()(system)
|
||||||
|
shutdownActorSystem(system)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"allow for auto-loading of library-extensions" in {
|
||||||
|
val system = ActorSystem("extensions")
|
||||||
|
val listedExtensions = system.settings.config.getStringList("akka.library-extensions")
|
||||||
|
listedExtensions.size should be > 0
|
||||||
|
// could be initalized by other tests, so at least once
|
||||||
|
InstanceCountingExtension.createCount.get() should be > 0
|
||||||
|
|
||||||
|
shutdownActorSystem(system)
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail the actor system if a library-extension fails to start" in {
|
||||||
|
intercept[FailingTestExtension.TestException] {
|
||||||
|
ActorSystem("failing", ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
akka.library-extensions += "akka.actor.FailingTestExtension"
|
||||||
|
""").withFallback(ConfigFactory.load()).resolve())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail the actor system if a library-extension cannot be loaded" in {
|
||||||
|
intercept[RuntimeException] {
|
||||||
|
ActorSystem("failing", ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
akka.library-extensions += "akka.actor.MissingExtension"
|
||||||
|
""").withFallback(ConfigFactory.load()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,14 +7,15 @@ package akka.config
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.ActorSystem
|
||||||
|
import akka.event.DefaultLoggingFilter
|
||||||
import akka.event.Logging.DefaultLogger
|
import akka.event.Logging.DefaultLogger
|
||||||
import akka.testkit.AkkaSpec
|
import akka.testkit.AkkaSpec
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import org.scalatest.Assertions
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import akka.event.DefaultLoggingFilter
|
|
||||||
|
|
||||||
class ConfigSpec extends AkkaSpec(ConfigFactory.defaultReference(ActorSystem.findClassLoader())) {
|
class ConfigSpec extends AkkaSpec(ConfigFactory.defaultReference(ActorSystem.findClassLoader())) with Assertions {
|
||||||
|
|
||||||
"The default configuration file (i.e. reference.conf)" must {
|
"The default configuration file (i.e. reference.conf)" must {
|
||||||
"contain all configuration properties for akka-actor that are used in code with their correct defaults" in {
|
"contain all configuration properties for akka-actor that are used in code with their correct defaults" in {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,16 @@ akka {
|
||||||
# setting.
|
# setting.
|
||||||
log-dead-letters-during-shutdown = on
|
log-dead-letters-during-shutdown = on
|
||||||
|
|
||||||
|
# List FQCN of extensions which shall be loaded at actor system startup.
|
||||||
|
# Library extensions are regular extensions that are loaded at startup and are
|
||||||
|
# available for third party library authors to enable auto-loading of extensions when
|
||||||
|
# present on the classpath. This is done by appending entries:
|
||||||
|
# 'library-extensions += "Extension"' in the library `reference.conf`.
|
||||||
|
#
|
||||||
|
# Should not be set by end user applications in 'application.conf', use the extensions property for that
|
||||||
|
#
|
||||||
|
library-extensions = ${?akka.library-extensions} []
|
||||||
|
|
||||||
# List FQCN of extensions which shall be loaded at actor system startup.
|
# List FQCN of extensions which shall be loaded at actor system startup.
|
||||||
# Should be on the format: 'extensions = ["foo", "bar"]' etc.
|
# Should be on the format: 'extensions = ["foo", "bar"]' etc.
|
||||||
# See the Akka Documentation for more info about Extensions
|
# See the Akka Documentation for more info about Extensions
|
||||||
|
|
|
||||||
|
|
@ -779,14 +779,26 @@ private[akka] class ActorSystemImpl(
|
||||||
def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = findExtension(ext) != null
|
def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = findExtension(ext) != null
|
||||||
|
|
||||||
private def loadExtensions() {
|
private def loadExtensions() {
|
||||||
immutableSeq(settings.config.getStringList("akka.extensions")) foreach { fqcn ⇒
|
/**
|
||||||
dynamicAccess.getObjectFor[AnyRef](fqcn) recoverWith { case _ ⇒ dynamicAccess.createInstanceFor[AnyRef](fqcn, Nil) } match {
|
* @param throwOnLoadFail Throw exception when an extension fails to load (needed for backwards compatibility)
|
||||||
case Success(p: ExtensionIdProvider) ⇒ registerExtension(p.lookup())
|
*/
|
||||||
case Success(p: ExtensionId[_]) ⇒ registerExtension(p)
|
def loadExtensions(key: String, throwOnLoadFail: Boolean): Unit = {
|
||||||
case Success(other) ⇒ log.error("[{}] is not an 'ExtensionIdProvider' or 'ExtensionId', skipping...", fqcn)
|
immutableSeq(settings.config.getStringList(key)) foreach { fqcn ⇒
|
||||||
case Failure(problem) ⇒ log.error(problem, "While trying to load extension [{}], skipping...", fqcn)
|
dynamicAccess.getObjectFor[AnyRef](fqcn) recoverWith { case _ ⇒ dynamicAccess.createInstanceFor[AnyRef](fqcn, Nil) } match {
|
||||||
|
case Success(p: ExtensionIdProvider) ⇒ registerExtension(p.lookup())
|
||||||
|
case Success(p: ExtensionId[_]) ⇒ registerExtension(p)
|
||||||
|
case Success(other)⇒
|
||||||
|
if (!throwOnLoadFail) log.error("[{}] is not an 'ExtensionIdProvider' or 'ExtensionId', skipping...", fqcn)
|
||||||
|
else throw new RuntimeException(s"[$fqcn] is not an 'ExtensionIdProvider' or 'ExtensionId'")
|
||||||
|
case Failure(problem) ⇒
|
||||||
|
if (!throwOnLoadFail) log.error(problem, "While trying to load extension [{}], skipping...", fqcn)
|
||||||
|
else throw new RuntimeException(s"While trying to load extension [$fqcn]", problem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadExtensions("akka.library-extensions", throwOnLoadFail = true)
|
||||||
|
loadExtensions("akka.extensions", throwOnLoadFail = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def toString: String = lookupRoot.path.root.address.toString
|
override def toString: String = lookupRoot.path.root.address.toString
|
||||||
|
|
|
||||||
|
|
@ -94,3 +94,20 @@ Use it:
|
||||||
.. includecode:: code/docs/extension/SettingsExtensionDocTest.java
|
.. includecode:: code/docs/extension/SettingsExtensionDocTest.java
|
||||||
:include: extension-usage-actor
|
:include: extension-usage-actor
|
||||||
|
|
||||||
|
Library extensions
|
||||||
|
==================
|
||||||
|
A third part library may register it's extension for auto-loading on actor system startup by appending it to
|
||||||
|
``akka.library-extensions`` in its ``reference.conf``.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
akka.library-extensions += "docs.extension.ExampleExtension"
|
||||||
|
|
||||||
|
|
||||||
|
As there is no way to selectively remove such extensions, it should be used with care and only when there is no case
|
||||||
|
where the user would ever want it disabled or have specific support for disabling such sub-features. One example where
|
||||||
|
this could be important is in tests.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
The``akka.library-extensions`` must never be assigned (``= ["Extension"]``) instead of appending as this will break
|
||||||
|
the library-extension mechanism and make behavior depend on class path ordering.
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,21 @@ Use it:
|
||||||
.. includecode:: code/docs/extension/SettingsExtensionDocSpec.scala
|
.. includecode:: code/docs/extension/SettingsExtensionDocSpec.scala
|
||||||
:include: extension-usage-actor
|
:include: extension-usage-actor
|
||||||
|
|
||||||
|
|
||||||
|
Library extensions
|
||||||
|
==================
|
||||||
|
A third part library may register it's extension for auto-loading on actor system startup by appending it to
|
||||||
|
``akka.library-extensions`` in its ``reference.conf``.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
akka.library-extensions += "docs.extension.ExampleExtension"
|
||||||
|
|
||||||
|
|
||||||
|
As there is no way to selectively remove such extensions, it should be used with care and only when there is no case
|
||||||
|
where the user would ever want it disabled or have specific support for disabling such sub-features. One example where
|
||||||
|
this could be important is in tests.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
The``akka.library-extensions`` must never be assigned (``= ["Extension"]``) instead of appending as this will break
|
||||||
|
the library-extension mechanism and make behavior depend on class path ordering.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue