An Architect's View

CFML, Clojure, Software Design, Frameworks and more...

An Architect's View

Real World Clojure - environment control

October 26, 2011 ·

Our platform uses some minimal per-environment configuration to determine on which tier it is running: dev, CI, QA, production. We have a fairly large number of application settings which vary by tier, such as data sources, ports and hostnames for external services, email addresses to be used for error reporting and so on. We also toggle features on and off in different environments so we can always have working builds for deployment to production (feature toggles allow us to develop and test features as they are developed over a period of time, without letting the feature "leak" into production before it is completed). This was actually the first part of our system to be moved to Clojure, because everything else would depend on it.

Initially we stored configuration in two XML files. One contained the "safe" defaults for all settings (a basic development template), the other contained overrides for settings that varied in specific environments. That second XML file also included the tags that allowed our environment control code to select the appropriate set of overrides. Whilst we could easily have continued with the XML files, the move to Clojure allowed us to use nested maps instead which meant that configuration no longer had to be static - it could contain executable code. We don't currently leverage that but at least we have the option now.

The default configuration is a simple map of settings. For each environment, we have a map with a :name, a list of patterns matching :hostnames, a pattern matching the application :webroot and then the :settings map containing the overrides. The settings for the current environment can then be computed as follows:

(defn settings []
  (merge default-configuration
         (:settings
           (match-environment
             environment-configurations
             (hostname) (webroot)))))

(hostname) returns the current servers name using Java interop - (.getHostName (java.net.InetAddress/getLocalHost)) - and (webroot) returns the application's installed webroot. Depending on how the code is being called, this is either derived from the web container itself, for the dating platform, or provided as a command line argument for processes that run from cron or as daemons. The match-environment function just walks the vector of maps, looking for the first map whose :hostnames patterns and/or :webroot pattern match the specified values.

In order to avoid computing that every time, we have define a variable with delayed execution:

(def my-settings (delay (settings)))

Then other code depends on the environment settings like this:

(ns worldsingles.mail
  (:require [worldsingles.environment :as env])
  ...)

(def ^:private mail-properties
  (delay (let [s @env/my-settings]
           (doto (java.util.Properties.)
             (.put "mail.host" (:mail-host s))
             (.put "mail.debug" (:mail-debug s))
             (.put "mail.smtp.from" (:bounce-email s))))))

Note that mail-properties is also a delayed execution so that we only need to compute it once, the first time we call (javax.mail.Session/getDefaultInstance @mail-properties) as part of our code to send email.

There are a few other convenience functions in the environment namespace, such as (env/production?) - in non-production environments, the code that sends email uses a :development-email setting as the "To:" address, so we don't accidentally send emails to real users because of test data (or, in QA, partial snapshots of production data).

Tags: clojure

2 responses

  • 1 Ken // Oct 26, 2011 at 10:02 AM

    I'm finding this series of posts very interesting. Thanks for putting it together.
  • 2 Sean Corfield // Oct 26, 2011 at 5:53 PM

    @Ken, thanx! Three posted so far, seven more queued up so far...