I'm writing a Compojure based application and can't get the hang of how to handle cleaning up state when the code is reloaded. Specifically, my app has an open handle to a LevelDB database, and this needs to be either reused or closed properly before it is opened again.
I've looked around for different suggestions on how to deal with state in Compojure applications, and found two patterns. The first one, which can be seen in this SO question ("Using LevelDB in a Ring Compojure webapp") suggests using a global var referencing an atom/ref/delay (I've seen a few different suggestions). This solution doesn't seem ideal to me, it's essentially global mutable state, and it looks like it is strongly discouraged (for example in this presentation). It's also unclear if it works with code reloading (maybe with defonce
instead of def
?)
The second pattern is to inject state using a piece of middleware, like in this SO question ("Passing state as parameter to a ring handler"). This pattern looks much better since it avoids the mutable state, but it doesn't work with code reloading. Here is my code that uses that pattern:
(defn create-initial-state []
{:db (delay (storage/create "data.leveldb"))})
(defn add-app-state [handler state]
(fn [request]
(handler (assoc request :app-state state))))
(defroutes app-routes
(GET "/signals" request
(handle-signals (:app-state request))))
(def app
(-> app-routes
(add-app-state (create-initial-state))
(handler/site)))
The implementation of handle-signals
isn't important so I've left that out. storage/create
opens a LevelDB database and returns a handle. The database is wrapped in a delay
to avoid it being created when the code loads in my tests. The division between create-initial-state
and add-app-state
is just to make it easier to reuse add-app-state
in the tests without marrying it to the code that opens the database.
When this code is run with lein ring server-headless
it works fine until I edit the code, then it blows up on the next request since app
has been recreated, and create-initial-state
has run again. There is no longer any reference to the old database handle, but since LevelDB keeps lock files around to make sure that there is only one process using the database, and my code never closes the handle, opening a new handle is an error.
- How do people generally handle things like database connections, open files and sockets when code is reloaded in a Ring app, without resorting to global mutable state? I've seen Stuart Sierra's "Clojure in the Large" presentation and it gives some good suggestions, but they feel way to complex for a tiny app like mine.
- Is there a way to register to get a callback when the code is about to reload, so that I can close the open database handle?