Skip to content
Jan Machacek edited this page Jul 13, 2012 · 3 revisions

#Configuration

Typical Akka applications require configurable elements. And by those, I mean trivial things such as javax.sql.DataSources, javax.mail.Sessions and many others. We would like to keep our codebase the same accross environments; and by codebase, I also mean the configuration files. The main reason is that I can guarantee that at some point, someone will forget to update the configuration file on the production servers. (Go anecdotal evidence!) Now, in typical Java EE applications deployed in fat application servers (ugh!), we would use JNDI. Our code would lookup the JNDI elements, and we would configure the container with the right entries. Let's explore the configuration options in Akka applications.

We would like to somehow configure our Akka applications; keeping in mind that we will certainly want different configuration for our tests (and possibly different configuration for different tests!) and different configuration for the real running code. We would also like to be able to set up the configuration from our code and mix in the appropriate configuration. The word mix in should be a big hint. The actors (and other components) that want to access the configuration will need to mix in a trait that gives them the read only access; and the component that builds the configuration will need to mix in another trait that gives the write only access.

Turning to simple actor that uses a DataSource, we would have

import akka.actor.{Address, Actor}
import org.cakesolutions.akkapatterns.core.Configured
import javax.sql.DataSource

case class GetAddresses(person: String)

class AddressBookActor extends Actor with Configured {

  protected def receive = {
    case GetAddresses(person) =>
      val dataSource = configured[DataSource]

      // use the dataSource

      sender ! Address("Robert Robinson Avenue", "Oxford") ::
               Address("Houldsworth Mill", "Reddish") ::
               Nil
  }
}

Notice the configured[DataSource] function from the Configured trait: it looks for a configured instance that is assignable to DataSource and, if one exists, it returns it.

Now, to set up the environment, let's take a look at how the main module would look like:

object Main extends App {

  implicit val system = ActorSystem("AkkaPatterns")

  class Application(val actorSystem: ActorSystem) extends Core with Api with Web with Configuration {
    configure {
      val ds = new BasicDataSource()
      ds.setDriverClassName(classOf[JDBCDriver].getCanonicalName)
      ds.setUsername("sa")
      ds.setUrl("jdbc:hsqldb:mem:test")

      ds
    }
  }

  new Application(system)

  sys.addShutdownHook {
    system.shutdown()
  }

}

We say that the application is composed of Core, Api, Web and its DataSource is configured using the configure function from the Configuration trait. When we run this application (by executing the Main object), the AddressBookActor will use the HSQLDB DataSource at jdbc:hsqldb:mem:test. (Real applications will obviously want to use a DataSource to a more powerful RDBMS, but the pattern will remain.)

##Testing

To perform an integration test of the AddressBookActor, we must build configuration for the tests; and nothing could be simpler. We will define a SpecConfiguration trait, which configures the test environment.

trait SpecConfiguration extends Configuration {

  configure {
    val ds = new BasicDataSource()
    ds.setDriverClassName(classOf[JDBCDriver].getCanonicalName)
    ds.setUsername("sa")
    ds.setUrl("jdbc:hsqldb:mem:test")

    ds
  }

}

To use it, we simply have to mix it into our specifications.

class AddressBookActorSpec extends TestKit(ActorSystem()) with Specification with ImplicitSender with Core with SpecConfiguration {

  val actor = TestActorRef[AddressBookActor]

  "GetAddresses(p) replies with the person's addresses" in {
    actor ! GetAddresses("Jan")

    expectMsg(Address("Robert Robinson Avenue", "Oxford") ::
                   Address("Houldsworth Mill", "Reddish") ::
                   Nil)

    success
  }


  implicit def actorSystem = system
}

##Behind the scenes I have left out the bofies of the Configured and Configuration traits and their supporting code. Focussing on the essence of the pattern, the simplest approach is to have:

private object ConfigurationStore {
  val entries = mutable.Map[String, AnyRef]()

  def put(key: String, value: AnyRef) {
    entries += ((key, value))
  }

  def get[A] =
    entries.values.find(_.isInstanceOf[A]).asInstanceOf[Option[A]]

}

trait Configured {

  def configured[A](implicit actorContext: ActorContext): A =
    ConfigurationStore.get[A].get


}

trait Configuration {

  def configure[R <: AnyRef](f: => R) = {
    val a = f
    ConfigurationStore.put(a.getClass.getName, a)
    a
  }

}

You may want to modify the singleton ConfigurationStore in your applications if you wish, but I found it to be perfectly reasonable in my production code, too.

Clone this wiki locally