-
Notifications
You must be signed in to change notification settings - Fork 6
Configuration
#Configuration
Typical Akka applications require configurable elements. And by those, I mean trivial things such as javax.sql.DataSource
s, javax.mail.Session
s 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.