Clojure, Leiningen 2 and Heroku: AOT Compilation Gotchas
Recently I upgraded the clojure project I’m working on to Leiningen 2 in order to start using nrepl - since swank-clojure is now deprecated. Little did I know this would lead me to a small debugging adventure.
I use Heroku as my deployment platform and my project had been running on it for a few weeks without any issues. I also use Heroku’s PostgreSQL solution.
However, by upgrading to Leiningen 2, my project started throwing some weird exceptions during deployment - it couldn’t connect to my database any longer. Everything was fine on my local environment though.
This means that after receiving a git push Heroku runs this command:
At first this might seem harmless. But step back for a second and think about what this means for code that depends on environment variables.
Case in point: Lobos
The real-world example on their github page recommends this code snippet for configuring a database connection:
1 2 3 4 5 6 7 8 9 10 11
More often than not this isn’t ideal. You’ll probably want to customise your database configuration based on environment variables. Something along these lines:
1 2 3 4 5 6 7 8 9 10 11
And you’re good to go - until Heroku decides to AOT compile your code.
The Clojure compilation process caught me by surprise. As stated by Rich Hickey on this IRC log entry, “a side effect of compiling Clojure code is loading the namespaces in order to make macros and functions they use available”.
This means that during compilation, any top level function calls - such as
(open-global db) from above - will get executed. The same applies to macros that expand to top-level function calls.
If said function call has no side effects, you’re probably ok.
The trouble with open-global though is that it effectively attempts a connection to the database!
On Heroku the environment variables you rely on - such as DATABASE_URL - aren’t available on compile time meaning a connection will be attempted to an empty url. All sorts of badness.
After having understood why the compilation process was effectively executing top level function calls, I then wrapped that call to open-global in a function declaration:
This will prevent eager evaulation during compilation since all
defn does is bind the given function body to a var.
Then, in the code that needs migrations to happen - such as in my midje tests setup - I initialize the database like this:
1 2 3 4
I spent some time in the #clojure IRC channel on freenode and it is accepted that top level function calls should be avoided anyway - especially if you’re dealing with side effects.
I also applied the same principle to other initialization code I have such as the configuration for Korma.
Not quite. After having spent quite some time to get to this point, this was the new error that was stealing my sleep:
1 2 3 4 5 6 7 8 9
This isn’t an unknown error. It basically means you’re trying to find a namespace that doesn’t exist. Or does it?
Once again this only happened if the code got compiled Ahead Of Time. Puzzling.
A couple of debugging hours later and I tracked it down to be a problem with clj-logging-config - a logging configuration utility for clojure. This is the offending code:
1 2 3 4 5
set-logger! is a macro that makes use of the *ns* var. *ns* contains the current namespace and, due to what I’m assuming to be special semantics regarding this particular var, it shouldn’t be unquoted within a macro - which is why it was failing in AOT mode.
My code now works perfectly - regardless of AOT compilation.
This was a nice little journey and I’m actually glad I went through it. I have a much better understanding of Clojure’s compilation process, worked out some quirks on Heroku and even got my first Clojure related open-source contribution accepted. Life is good.
Hopefully this will save other people a lot of time and effort debugging similar issues.