act #20332 Functionality to automatically load library extensions

This commit is contained in:
Johan Andrén 2016-04-15 15:12:35 +02:00
parent 2418e610ab
commit 14c08df74a
8 changed files with 209 additions and 53 deletions

View file

@ -1,4 +1,7 @@
akka {
# for the akka.actor.ExtensionSpec
library-extensions += "akka.actor.InstanceCountingExtension"
actor {
serialize-messages = on
warn-about-java-serializer-usage = off

View file

@ -19,33 +19,6 @@ import akka.util.Switch
import akka.util.Helpers.ConfigOps
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 {
class Waves extends Actor {
@ -140,7 +113,6 @@ object ActorSystemSpec {
}
val config = s"""
akka.extensions = ["akka.actor.TestExtension"]
slow {
type="${classOf[SlowDispatcher].getName}"
}"""
@ -177,23 +149,6 @@ class ActorSystemSpec extends AkkaSpec(ActorSystemSpec.config) with ImplicitSend
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 {
val sys = ActorSystem("LogDeadLetters", ConfigFactory.parseString("akka.loglevel=INFO").withFallback(AkkaSpec.testConf))
try {

View 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()))
}
}
}
}

View file

@ -7,14 +7,15 @@ package akka.config
import java.util.concurrent.TimeUnit
import akka.actor.ActorSystem
import akka.event.DefaultLoggingFilter
import akka.event.Logging.DefaultLogger
import akka.testkit.AkkaSpec
import com.typesafe.config.ConfigFactory
import org.scalatest.Assertions
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 {
"contain all configuration properties for akka-actor that are used in code with their correct defaults" in {

View file

@ -59,6 +59,16 @@ akka {
# setting.
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.
# Should be on the format: 'extensions = ["foo", "bar"]' etc.
# See the Akka Documentation for more info about Extensions

View file

@ -779,14 +779,26 @@ private[akka] class ActorSystemImpl(
def hasExtension(ext: ExtensionId[_ <: Extension]): Boolean = findExtension(ext) != null
private def loadExtensions() {
immutableSeq(settings.config.getStringList("akka.extensions")) foreach { 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) log.error("[{}] is not an 'ExtensionIdProvider' or 'ExtensionId', skipping...", fqcn)
case Failure(problem) log.error(problem, "While trying to load extension [{}], skipping...", fqcn)
/**
* @param throwOnLoadFail Throw exception when an extension fails to load (needed for backwards compatibility)
*/
def loadExtensions(key: String, throwOnLoadFail: Boolean): Unit = {
immutableSeq(settings.config.getStringList(key)) foreach { 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

View file

@ -94,3 +94,20 @@ Use it:
.. includecode:: code/docs/extension/SettingsExtensionDocTest.java
: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.

View file

@ -87,3 +87,21 @@ Use it:
.. includecode:: code/docs/extension/SettingsExtensionDocSpec.scala
: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.