fix #23618 : Support for persistence dynamic configuration at runtime (#23841)

This commit is contained in:
Prada Souvanlasy 2018-03-26 13:52:31 +02:00 committed by Patrik Nordwall
parent 0a426a7ab0
commit 46c662965f
16 changed files with 918 additions and 144 deletions

View file

@ -5,11 +5,13 @@
package akka.persistence.query
import java.util.concurrent.atomic.AtomicReference
import akka.actor._
import akka.event.Logging
import scala.annotation.tailrec
import scala.util.Failure
import com.typesafe.config.Config
import com.typesafe.config.{ Config, ConfigFactory }
/**
* Persistence extension for queries.
@ -39,21 +41,33 @@ class PersistenceQuery(system: ExtendedActorSystem) extends Extension {
/** Discovered query plugins. */
private val readJournalPluginExtensionIds = new AtomicReference[Map[String, ExtensionId[PluginHolder]]](Map.empty)
/**
* Scala API: Returns the [[akka.persistence.query.scaladsl.ReadJournal]] specified by the given
* read journal configuration entry.
*
* The provided readJournalPluginConfig will be used to configure the journal plugin instead of the actor system
* config.
*/
final def readJournalFor[T <: scaladsl.ReadJournal](readJournalPluginId: String, readJournalPluginConfig: Config): T =
readJournalPluginFor(readJournalPluginId, readJournalPluginConfig).scaladslPlugin.asInstanceOf[T]
/**
* Scala API: Returns the [[akka.persistence.query.scaladsl.ReadJournal]] specified by the given
* read journal configuration entry.
*/
final def readJournalFor[T <: scaladsl.ReadJournal](readJournalPluginId: String): T =
readJournalPluginFor(readJournalPluginId).scaladslPlugin.asInstanceOf[T]
readJournalFor(readJournalPluginId, ConfigFactory.empty)
/**
* Java API: Returns the [[akka.persistence.query.javadsl.ReadJournal]] specified by the given
* read journal configuration entry.
*/
final def getReadJournalFor[T <: javadsl.ReadJournal](clazz: Class[T], readJournalPluginId: String): T =
readJournalPluginFor(readJournalPluginId).javadslPlugin.asInstanceOf[T]
final def getReadJournalFor[T <: javadsl.ReadJournal](clazz: Class[T], readJournalPluginId: String, readJournalPluginConfig: Config): T =
readJournalPluginFor(readJournalPluginId, readJournalPluginConfig).javadslPlugin.asInstanceOf[T]
@tailrec private def readJournalPluginFor(readJournalPluginId: String): PluginHolder = {
final def getReadJournalFor[T <: javadsl.ReadJournal](clazz: Class[T], readJournalPluginId: String): T = getReadJournalFor[T](clazz, readJournalPluginId, ConfigFactory.empty())
@tailrec private def readJournalPluginFor(readJournalPluginId: String, readJournalPluginConfig: Config): PluginHolder = {
val configPath = readJournalPluginId
val extensionIdMap = readJournalPluginExtensionIds.get
extensionIdMap.get(configPath) match {
@ -62,20 +76,21 @@ class PersistenceQuery(system: ExtendedActorSystem) extends Extension {
case None
val extensionId = new ExtensionId[PluginHolder] {
override def createExtension(system: ExtendedActorSystem): PluginHolder = {
val provider = createPlugin(configPath)
val provider = createPlugin(configPath, readJournalPluginConfig)
PluginHolder(provider.scaladslReadJournal(), provider.javadslReadJournal())
}
}
readJournalPluginExtensionIds.compareAndSet(extensionIdMap, extensionIdMap.updated(configPath, extensionId))
readJournalPluginFor(readJournalPluginId) // Recursive invocation.
readJournalPluginFor(readJournalPluginId, readJournalPluginConfig) // Recursive invocation.
}
}
private def createPlugin(configPath: String): ReadJournalProvider = {
private def createPlugin(configPath: String, readJournalPluginConfig: Config): ReadJournalProvider = {
val mergedConfig = readJournalPluginConfig.withFallback(system.settings.config)
require(
!isEmpty(configPath) && system.settings.config.hasPath(configPath),
!isEmpty(configPath) && mergedConfig.hasPath(configPath),
s"'reference.conf' is missing persistence read journal plugin config path: '${configPath}'")
val pluginConfig = system.settings.config.getConfig(configPath)
val pluginConfig = mergedConfig.getConfig(configPath)
val pluginClassName = pluginConfig.getString("class")
log.debug(s"Create plugin: ${configPath} ${pluginClassName}")
val pluginClass = system.dynamicAccess.getClassFor[AnyRef](pluginClassName).get

View file

@ -7,6 +7,7 @@ package akka.persistence.query;
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.testkit.AkkaJUnitActorSystemResource;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;

View file

@ -13,7 +13,7 @@ import akka.actor.ExtendedActorSystem
* Use for tests only!
* Emits infinite stream of strings (representing queried for events).
*/
class DummyReadJournal extends scaladsl.ReadJournal with scaladsl.PersistenceIdsQuery {
class DummyReadJournal(val dummyValue: String) extends scaladsl.ReadJournal with scaladsl.PersistenceIdsQuery {
override def persistenceIds(): Source[String, NotUsed] =
Source.fromIterator(() Iterator.from(0)).map(_.toString)
}
@ -42,13 +42,19 @@ object DummyReadJournalProvider {
${DummyReadJournal.Identifier}4 {
class = "${classOf[DummyReadJournalProvider4].getCanonicalName}"
}
${DummyReadJournal.Identifier}5 {
class = "${classOf[DummyReadJournalProvider5].getCanonicalName}"
}
""")
}
class DummyReadJournalProvider extends ReadJournalProvider {
class DummyReadJournalProvider(dummyValue: String) extends ReadJournalProvider {
// mandatory zero-arg constructor
def this() = this("dummy")
override val scaladslReadJournal: DummyReadJournal =
new DummyReadJournal
new DummyReadJournal(dummyValue)
override val javadslReadJournal: DummyReadJournalForJava =
new DummyReadJournalForJava(scaladslReadJournal)
@ -60,3 +66,7 @@ class DummyReadJournalProvider3(sys: ExtendedActorSystem, conf: Config) extends
class DummyReadJournalProvider4(sys: ExtendedActorSystem, conf: Config, confPath: String) extends DummyReadJournalProvider
class DummyReadJournalProvider5(sys: ExtendedActorSystem) extends DummyReadJournalProvider
class CustomDummyReadJournalProvider5(sys: ExtendedActorSystem) extends DummyReadJournalProvider("custom")

View file

@ -5,11 +5,12 @@
package akka.persistence.query
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.ActorSystem
import akka.persistence.journal.EventSeq
import akka.persistence.journal.ReadEventAdapter
import com.typesafe.config.ConfigFactory
import akka.persistence.journal.{ EventSeq, ReadEventAdapter }
import com.typesafe.config.{ Config, ConfigFactory }
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpecLike }
import scala.concurrent.Await
import scala.concurrent.duration._
@ -17,21 +18,45 @@ class PersistenceQuerySpec extends WordSpecLike with Matchers with BeforeAndAfte
val eventAdaptersConfig =
s"""
|akka.persistence.query.journal.dummy {
| event-adapters {
| adapt = ${classOf[PrefixStringWithPAdapter].getCanonicalName}
| }
|}
|akka.persistence.query.journal.dummy {
| event-adapters {
| adapt = ${classOf[PrefixStringWithPAdapter].getCanonicalName}
| }
|}
""".stripMargin
val customReadJournalPluginConfig =
s"""
|${DummyReadJournal.Identifier}5 {
| class = "${classOf[CustomDummyReadJournalProvider5].getCanonicalName}"
|}
|${DummyReadJournal.Identifier}6 {
| class = "${classOf[DummyReadJournalProvider].getCanonicalName}"
|}
""".stripMargin
"ReadJournal" must {
"be found by full config key" in {
withActorSystem() { system
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](DummyReadJournal.Identifier)
val readJournalPluginConfig: Config = ConfigFactory.parseString(customReadJournalPluginConfig)
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier, readJournalPluginConfig)
// other combinations of constructor parameters
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](DummyReadJournal.Identifier + "2")
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](DummyReadJournal.Identifier + "3")
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](DummyReadJournal.Identifier + "4")
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier + "2", readJournalPluginConfig)
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier + "3", readJournalPluginConfig)
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier + "4", readJournalPluginConfig)
// config key existing within both the provided readJournalPluginConfig
// and the actorSystem config. The journal must be created from the provided config then.
val dummyReadJournal5 = PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier + "5", readJournalPluginConfig)
dummyReadJournal5.dummyValue should equal("custom")
// config key directly coming from the provided readJournalPluginConfig,
// and does not exist within the actorSystem config
PersistenceQuery.get(system).readJournalFor[DummyReadJournal](
DummyReadJournal.Identifier + "6", readJournalPluginConfig)
}
}
@ -46,6 +71,7 @@ class PersistenceQuerySpec extends WordSpecLike with Matchers with BeforeAndAfte
}
private val systemCounter = new AtomicInteger()
private def withActorSystem(conf: String = "")(block: ActorSystem Unit): Unit = {
val config =
DummyReadJournalProvider.config
@ -60,8 +86,13 @@ class PersistenceQuerySpec extends WordSpecLike with Matchers with BeforeAndAfte
}
object ExampleQueryModels {
case class OldModel(value: String) { def promote = NewModel(value) }
case class OldModel(value: String) {
def promote = NewModel(value)
}
case class NewModel(value: String)
}
class PrefixStringWithPAdapter extends ReadEventAdapter {

View file

@ -60,7 +60,7 @@ class EventsByPersistenceIdSpec extends AkkaSpec(EventsByPersistenceIdSpec.confi
src.map(_.event).runWith(TestSink.probe[Any])
.request(2)
.expectNext("a-1", "a-2")
.expectNoMsg(500.millis)
.expectNoMessage(500.millis)
.request(2)
.expectNext("a-3")
.expectComplete()
@ -81,13 +81,13 @@ class EventsByPersistenceIdSpec extends AkkaSpec(EventsByPersistenceIdSpec.confi
val probe = src.map(_.event).runWith(TestSink.probe[Any])
.request(2)
.expectNext("f-1", "f-2")
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
ref ! "f-4"
expectMsg("f-4-done")
probe
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
.request(5)
.expectNext("f-3")
.expectComplete() // f-4 not seen
@ -186,13 +186,13 @@ class EventsByPersistenceIdSpec extends AkkaSpec(EventsByPersistenceIdSpec.confi
val probe = src.map(_.event).runWith(TestSink.probe[Any])
.request(2)
.expectNext("e-1", "e-2")
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
ref ! "e-4"
expectMsg("e-4-done")
probe
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
.request(5)
.expectNext("e-3")
.expectNext("e-4")

View file

@ -79,7 +79,7 @@ class EventsByTagSpec extends AkkaSpec(EventsByTagSpec.config)
.request(2)
.expectNext(EventEnvelope(Sequence(1L), "a", 2L, "a green apple"))
.expectNext(EventEnvelope(Sequence(2L), "a", 3L, "a green banana"))
.expectNoMsg(500.millis)
.expectNoMessage(500.millis)
.request(2)
.expectNext(EventEnvelope(Sequence(3L), "b", 2L, "a green leaf"))
.expectComplete()
@ -99,13 +99,13 @@ class EventsByTagSpec extends AkkaSpec(EventsByTagSpec.config)
.request(2)
.expectNext(EventEnvelope(Sequence(1L), "a", 2L, "a green apple"))
.expectNext(EventEnvelope(Sequence(2L), "a", 3L, "a green banana"))
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
c ! "a green cucumber"
expectMsg(s"a green cucumber-done")
probe
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
.request(5)
.expectNext(EventEnvelope(Sequence(3L), "b", 2L, "a green leaf"))
.expectComplete() // green cucumber not seen
@ -130,7 +130,7 @@ class EventsByTagSpec extends AkkaSpec(EventsByTagSpec.config)
val probe = blackSrc.runWith(TestSink.probe[Any])
.request(2)
.expectNext(EventEnvelope(Sequence(1L), "b", 1L, "a black car"))
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
d ! "a black dog"
expectMsg(s"a black dog-done")
@ -139,7 +139,7 @@ class EventsByTagSpec extends AkkaSpec(EventsByTagSpec.config)
probe
.expectNext(EventEnvelope(Sequence(2L), "d", 1L, "a black dog"))
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
.request(10)
.expectNext(EventEnvelope(Sequence(3L), "d", 2L, "a black night"))
}
@ -151,7 +151,7 @@ class EventsByTagSpec extends AkkaSpec(EventsByTagSpec.config)
// note that banana is not included, since exclusive offset
.expectNext(EventEnvelope(Sequence(3L), "b", 2L, "a green leaf"))
.expectNext(EventEnvelope(Sequence(4L), "c", 1L, "a green cucumber"))
.expectNoMsg(100.millis)
.expectNoMessage(100.millis)
}
}