managing Clojure and ClojureScript app state since (reset)
Find a file
2015-11-14 16:45:38 -05:00
dev start/stop return :started/:stopped 2015-10-25 22:27:14 -04:00
doc [diff from component]: link to start/stop parts 2015-11-14 11:17:43 -05:00
src/mount bringing start-with-args into master 2015-11-14 03:10:18 -05:00
test testing with parts of an app 2015-11-14 11:05:22 -05:00
.gitignore welcome mount 2015-10-19 21:33:56 -04:00
.hgignore welcome mount 2015-10-19 21:33:56 -04:00
LICENSE welcome mount 2015-10-19 21:33:56 -04:00
project.clj releasing 0.1.1 2015-11-14 16:45:38 -05:00
README.md plugin in Circle CI 2015-11-14 16:37:39 -05:00

I think that it's extraordinarily important that we in computer science keep fun in computing

Alan J. Perlis from Structure and Interpretation of Computer Programs

mount

module branch status
mount master Circle CI

Clojars Project

Table of Contents generated with DocToc

Why?

Clojure is

  • powerful
  • simple
  • and fun

Depending on how application state is managed during development, the above three superpowers can either stay, go somewhat, or go completely.

If Clojure REPL (i.e. lein repl, boot repl) fired up instantly, the need to reload application state inside the REPL would go away. But at the moment, and for some time in the future, managing state by making it reloadable within the same REPL session is important to retain all the Clojure superpowers.

Here is a good breakdown on the Clojure REPL startup time, and it is not because of JVM.

mount is here to preserve all the Clojure superpowers while making the application state enjoyably reloadable.

There is another Clojure superpower that mount is made to retain: Clojure community. Pull request away, let's solve this thing!

Differences from Component

mount is an alternative to the component approach with notable differences.

How

(require '[mount :refer [defstate]])

Creating State

Creating state is easy:

(defstate conn :start (create-conn))

where (create-conn) is defined elsewhere, can be right above it.

In case this state needs to be cleaned / destryed between reloads, there is also :stop

(defstate conn :start (create-conn)
               :stop (disconnect conn))

That is pretty much it. But wait, there is more.. this state is a top level being, which means it can be simply required by other namespaces or in REPL:

dev=> (require '[app.nyse :refer [conn]])
nil
dev=> conn
#object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"]

Using State

For example let's say an app needs a connection above. No problem:

(ns app
  (:require [above :refer [conn]]))

where above is an arbitrary namespace that defines the above state / connection.

Dependencies

If the whole app is one big application context (or system), cross dependencies with a solid dependency graph is an integral part of the system.

But if a state is a simple top level being, these beings can coexist with each other and with other namespaces by being required instead.

If a managing state library requires a whole app buy-in, where everything is a bean or a component, it is a framework, and dependency graph is usually quite large and complex, since it has everything (every piece of the application) in it.

But if stateful things are kept lean and low level (i.e. I/O, queues, etc.), dependency graphs are simple and small, and everything else is just namespaces and functions: the way it should be.

Talking States

There are of course direct dependecies that mount respects:

(ns app.config
  (:require [mount :refer [defstate]]))

(defstate app-config
  :start (load-config "test/resources/config.edn"))

this app-config, being top level, can be used in other namespaces, including the ones that create states:

(ns app.database
  (:require [mount :refer [defstate]]
            [app.config :refer [app-config]]))

(defstate conn :start (create-connection app-config))

here is an example of a Datomic connection that "depends" on a similar app-config.

The Importance of Being Reloadable

mount has start and stop functions that will walk all the states created with defstate and start / stop them accordingly: i.e. will call their :start and :stop defined functions. Hence the whole applicatoin state can be reloaded in REPL e.g.:

dev=> (mount/stop)
dev=> (mount/start)

This can be easily hooked up to tools.namespace, to make the whole application reloadable with refreshing the app namespaces. Here is a dev.clj as an example, that sums up to:

(defn go []
  (start)
  :ready)

(defn reset []
  (stop)
  (tn/refresh :after 'dev/go))

the (reset) is then used in REPL to restart / reload application state without the need to restart the REPL itself.

Start and Stop Order

Since dependencies are "injected" by requireing on the namespace level, mount trusts the Clojure compiler to maintain the start and stop order for all the defstates.

The "start" order is then recorded and replayed on each (reset).

The "stop" order is simply (reverse "start order"):

dev=> (reset)
08:21:39.430 [nREPL-worker-1] DEBUG mount - << stopping..  nrepl
08:21:39.431 [nREPL-worker-1] DEBUG mount - << stopping..  conn
08:21:39.432 [nREPL-worker-1] DEBUG mount - << stopping..  app-config

:reloading (app.config app.nyse app.utils.datomic app)

08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting..  app-config
08:21:39.463 [nREPL-worker-1] DEBUG mount - >> starting..  conn
08:21:39.481 [nREPL-worker-1] DEBUG mount - >> starting..  nrepl
:ready

You can see examples of start and stop flows in the example app.

Start and Stop Parts of Application

In REPL or during testing it is often very useful to work with / start / stop only a part of an application, i.e. "only these two states".

mount start/stop functions optionally take namespaces to start/stop:

(mount/start #'app.config/app-config #'app.nyse/conn)
...
(mount/stop #'app.config/app-config #'app.nyse/conn)

which will only start/stop app-config and conn (won't start any other states).

Here is an example test that uses only two namespaces checking that the third one is not started.

Mount and Develop!

mount comes with an example app that has 3 states:

  • config, loaded from the files and refreshed on each (reset)
  • datomic connection that uses the config to create itself
  • nrepl that uses config to bind to host/port

Running New York Stock Exchange

To try it out, clone mount, get to REPL and switch to (dev):

$ lein repl

user=> (dev)
#object[clojure.lang.Namespace 0xcf1a0cc "dev"]

start/restart/reset everything using (reset):

dev=> (reset)

:reloading (app.config app.nyse app.utils.datomic app dev)
15:30:32.412 [nREPL-worker-1] DEBUG mount - >> starting..  app-config
15:30:32.414 [nREPL-worker-1] INFO  app.config - loading config from test/resources/config.edn
15:30:32.422 [nREPL-worker-1] DEBUG mount - >> starting..  conn
15:30:32.430 [nREPL-worker-1] INFO  app.nyse - conf:  {:datomic {:uri datomic:mem://mount}, :h2 {:classname org.h2.Driver, :subprotocol h2, :subname jdbc:h2:mem:mount, :user sa, :password }, :rabbit {:api-port 15672, :password guest, :queue r-queue, :username guest, :port 5672, :node jabit, :exchange-type direct, :host 192.168.1.1, :vhost /captoman, :auto-delete-q? true, :routing-key , :exchange foo}}
15:30:32.430 [nREPL-worker-1] INFO  app.nyse - creating a connection to datomic: datomic:mem://mount
15:30:32.430 [nREPL-worker-1] DEBUG mount - >> starting..  nrepl
dev=>

everything is started and can be played with:

dev=> (create-nyse-schema)
dev=> (add-order "GOOG" 665.51M 665.59M 100)
dev=> (add-order "GOOG" 665.50M 665.58M 300)

dev=> (find-orders "GOOG")
({:db/id 17592186045418, :order/symbol "GOOG", :order/bid 665.51M, :order/qty 100, :order/offer 665.59M}
 {:db/id 17592186045420, :order/symbol "GOOG", :order/bid 665.50M, :order/qty 300, :order/offer 665.58M})

once something is changed in the code, or you just need to reload everything, do (reset):

dev=> (reset)
15:32:44.342 [nREPL-worker-2] DEBUG mount - << stopping..  nrepl
15:32:44.343 [nREPL-worker-2] DEBUG mount - << stopping..  conn
15:32:44.343 [nREPL-worker-2] INFO  app.nyse - disconnecting from  datomic:mem://mount
15:32:44.344 [nREPL-worker-2] DEBUG mount - << stopping..  app-config

:reloading (app.config app.nyse app.utils.datomic app dev)

15:32:44.371 [nREPL-worker-2] DEBUG mount - >> starting..  app-config
15:32:44.372 [nREPL-worker-2] INFO  app.config - loading config from test/resources/config.edn
15:32:44.380 [nREPL-worker-2] DEBUG mount - >> starting..  conn
15:32:44.382 [nREPL-worker-2] INFO  app.nyse - conf:  {:datomic {:uri datomic:mem://mount}, :h2 {:classname org.h2.Driver, :subprotocol h2, :subname jdbc:h2:mem:mount, :user sa, :password }, :rabbit {:api-port 15672, :password guest, :queue r-queue, :username guest, :port 5672, :node jabit, :exchange-type direct, :host 192.168.1.1, :vhost /captoman, :auto-delete-q? true, :routing-key , :exchange foo}}
15:32:44.382 [nREPL-worker-2] INFO  app.nyse - creating a connection to datomic: datomic:mem://mount
15:32:44.387 [nREPL-worker-2] DEBUG mount - >> starting..  nrepl
:ready

notice that it stopped and started again.

In nyse's connection :stop function database is deleted. Hence after (reset) was called the app was brought its starting point: database was created by the :start function, but no schema again:

dev=> (find-orders "GOOG")

IllegalArgumentExceptionInfo :db.error/not-an-entity Unable to resolve entity: :order/symbol  datomic.error/arg (error.clj:57)

hence the app is in its "clean" state, and ready to rock and roll as right after the REPL started:

dev=> (create-nyse-schema)
dev=> (find-orders "GOOG")
()

dev=> (add-order "AAPL" 111.712M 111.811M 250)

dev=> (find-orders "AAPL")
({:db/id 17592186045418, :order/symbol "AAPL", :order/bid 111.712M, :order/qty 250, :order/offer 111.811M})

Web and Uberjar

There is an uberjar branch with an example webapp and it's uberjar sibling. Before trying it:

$ git checkout uberjar
Switched to branch 'uberjar'

The documentation is here.

Runtime Arguments

There is an with-args branch with an example app that takes command line params

$ git checkout with-args
Switched to branch 'with-args'

The documentation is here.

License

Copyright © 2015 tolitius

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.