diff --git a/akka-actor-tests/src/test/resources/reference.conf b/akka-actor-tests/src/test/resources/reference.conf index 2553b0e032..5d9918e535 100644 --- a/akka-actor-tests/src/test/resources/reference.conf +++ b/akka-actor-tests/src/test/resources/reference.conf @@ -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 diff --git a/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala index 371f21d02c..933dd207e6 100644 --- a/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/actor/ActorSystemSpec.scala @@ -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 { diff --git a/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala b/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala new file mode 100644 index 0000000000..83f13faac5 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/actor/ExtensionSpec.scala @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +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())) + } + } + + + } + +} diff --git a/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala b/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala index ac2bd11a33..a89760aeff 100644 --- a/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/config/ConfigSpec.scala @@ -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 { diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index 289ffcacc2..b3aaf7eafd 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -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 diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index 7bce538df3..aa1577058d 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -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 diff --git a/akka-docs/rst/java/extending-akka.rst b/akka-docs/rst/java/extending-akka.rst index 1c86007567..5d3a3b7170 100644 --- a/akka-docs/rst/java/extending-akka.rst +++ b/akka-docs/rst/java/extending-akka.rst @@ -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. diff --git a/akka-docs/rst/scala/extending-akka.rst b/akka-docs/rst/scala/extending-akka.rst index 5f1b75edf7..e2d36980eb 100644 --- a/akka-docs/rst/scala/extending-akka.rst +++ b/akka-docs/rst/scala/extending-akka.rst @@ -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. \ No newline at end of file