10

In a Scala application that is using Typesafe Config, I want to add the possibility to reload a Config at runtime. A Config instance is immutable. Here is what I have so far:

package config

trait Settings {
  private[config] var config: Config = ConfigFactory.empty()
  def engine: EngineSettings
}

trait EngineSettings {
  def weight: Int
  def offset: Int
}

class AppSettings {
  override def engine = new EngineSettings {
    override def weight = config.getInt("engine.weight")
    override def offset = config.getInt("engine.offset")
  }
}

object Settings {
  private val namedSettings = new TrieMap[String, AppSettings]
  def load(configName: String = "local"): Settings = {
    // load config
    // create or update AppSettings
    // add to map and return
  }
}

Initially a Settings instance is created using Settings.load. That instance reference is handed to other classes. Then a second thread can reload the underlying Config by calling Settings.load again. Here is how you access it:

class Engine(settings: Settings) {
  def calculate() = {
    val weight = settings.engine.weight
    // do some stuff
    val offset = settings.engine.offset
  }
}

There are two problems:

  1. someone might reload the underlying Config while calculate() is at line: // do some stuff (consistency)
  2. don't like using a var in the Settings trait

How can I improve this design :)

Havoc P
  • 8,101
  • 1
  • 28
  • 45
reikje
  • 2,474
  • 2
  • 19
  • 35

1 Answers1

4

You could turn config into a method with support for config cache invalidation (and with sensible defaults), so you can choose between dynamic (default in the following sample) and performance.

In general I suggest you use a good Scala typesafe wrapper of TypeSafe's Config such as Ficus (e.g. Gradle-stype artifact dependency net.ceedubs:ficus_2.11:1.1.1)

package config

import scala.collection.concurrent.TrieMap

import com.typesafe.config.{Config, ConfigFactory}
import net.ceedubs.ficus.Ficus._

trait Settings {
  protected[config] def config (
    name: String = "local",
    invalidateCache: Boolean = false
  ): Config = {
    if (invalidateCache) { ConfigFactory invalidateCaches }
    ConfigFactory load name
  }
  def engine: EngineSettings
}

trait EngineSettings {
  def weight: Int
  def offset: Int
}

class AppSettings(val name: String = "local") extends Settings {
  val c = config()

  override def engine = new EngineSettings {
    override def weight = c.as[Int]("engine.weight")
    override def offset = c.as[Int]("engine.offset")
  }
}

object Settings {
  private val namedSettings = new TrieMap[String, AppSettings]
  def load(configName: String = "local"): Settings = {
    // e.g.
    val loadedUpToDate = new AppSettings
    namedSettings +=
      ((configName + "." + System.currentTimeMillis, loadedUpToDate))
    new Settings {
      override def engine = loadedUpToDate.engine
    }
  }
}

I think this solves your issues because:

  1. Configuration retrieval is dynamic by default through reload
  2. By using a method you don't resort to mutable state
circlespainter
  • 791
  • 4
  • 8
  • 2
    https://github.com/typesafehub/config/issues/57 has some thoughts on this issue. I think the general approach to take is that you don't want a reference to a Config (via method or anything else) to switch to another Config in between reading different values, because then the config might become inconsistent. So that points to getting a whole new Config sometimes and then recreating everything that depends on it. So the problem kind of comes down to "how do I restart the configured bits of my program" - which is why reloading config isn't supported by Akka for example, because it's pretty hard – Havoc P Aug 14 '14 at 13:32