mount manages stateful components.
mount-up let's you know whenever any of these components are "managed".
There are three types of events you can listen to:
Whenever any state / component is
- started
(on-up [k f when])
- stopped
(on-down [k f when])
- started and/or stopped
(on-upndown [k f when])
where:
k
: key / name of the listner
f
: function / listener
when
: when to apply f
. possible values :before
, :after
or :wrap-in
As anything good in Clojure, listener is just a function.
This function will be passed a map with :name
and :action
keys.
:name
will have a component's name
:action
will have an action taked: i.e. :up
or :down
mount-up comes with one such listener that logs whenever any of the states / components are started or stopped:
(defn log [{:keys [name action]}]
(case action
:up (log/info ">> starting.." name)
:down (log/info "<< stopping.." name)))
Let's use the log
function above as an example.
$ boot dev
Creating a server component, starting it and stopping it as usual:
boot.user=> (require '[mount.core :as mount :refer [defstate]])
nil
boot.user=> (defstate server :start 42 :stop -42)
#'boot.user/server
boot.user=> (mount/start)
{:started ["#'boot.user/server"]}
boot.user=> (mount/stop)
{:stopped ["#'boot.user/server"]}
Now let's listen whenever this component is started and log :before it happens:
boot.user=> (require '[mount-up.core :as mu])
nil
boot.user=> (mu/on-up :info mu/log :before)
{:info #object[clojure.core$partial$fn__4761 0x703ef68c "clojure.core$partial$fn__4761@703ef68c"]}
boot.user=> (mount/start)
INFO mount-up.core - >> starting.. #'boot.user/server
{:started ["#'boot.user/server"]}
boot.user=> (mount/stop)
{:stopped ["#'boot.user/server"]}
We can also clear all the listeners by all-clear
:
boot.user=> (mu/all-clear)
nil
boot.user=> (mount/start)
{:started ["#'boot.user/server"]}
boot.user=> (mount/stop)
{:stopped ["#'boot.user/server"]}
boot.user=> (mu/on-upndown :info mu/log :before)
{:info #object[clojure.core$partial$fn__4761 0x75fda4b5 "clojure.core$partial$fn__4761@75fda4b5"]}
boot.user=> (mount/start)
INFO mount-up.core - >> starting.. #'boot.user/server
{:started ["#'boot.user/server"]}
boot.user=> (mount/stop)
INFO mount-up.core - << stopping.. #'boot.user/server
{:stopped ["#'boot.user/server"]}
mu/log
function is just an example of course: any function(s) can be used as a listener.
Besides :before
and :after
, mount-up knows how to wrap ups and downs with a custom function via :wrap-in
.
This is really useful in case you need to be in charge of calling start or stop for each individual state.
For example to guard ups and downs of each state with a try/catch
.
A "wrapper" function takes two arguments:
f
: a function that is going to bring state up or down
state-name
: a name of the state (i.e. "#'app/db"
)
Function f
will be provided by mount and will just need to be invoked as (f)
to start/stop the state. The rest is up to you.
It is a lot simpler to demo than to explain.
mount-up comes with a generic try-catch
function:
(defn try-catch [on-error]
(fn [f state]
(try (f)
(catch Throwable t
(on-error t state)))))
which returns a function that takes f
and state
(name) and wraps calling (f)
in a try/catch
. It takes an on-error
function
that will decide what will happen if starting or stopping state results in a Throwable
.
Let's define a sample on-error
function that will eat (ouch!) the exception and will log what happened:
boot.user=> (defn log-exception [ex _]
(let [root (.getMessage (.getCause ex))]
(log/error (str (.getMessage ex) " \"" root \"))))
#'boot.user/log-exception
Let's define three states, one of which throws an exception:
boot.user=> (defstate server :start 42 :stop -42)
#'boot.user/server
boot.user=> (defstate db :start (/ 1 0) :stop -42)
#'boot.user/db
boot.user=> (defstate pi :start 3.14 :stop 14.3)
#'boot.user/pi
Let's start these without wrapping anything:
boot.user=> (mount/start)
INFO mount-up.core - >> starting.. #'boot.user/server
INFO mount-up.core - >> starting.. #'boot.user/db
java.lang.ArithmeticException: Divide by zero
java.lang.RuntimeException: could not start [#'boot.user/db] due to
As expected #'boot.user/db
throws an exception and we have no control over it. Also notice that system failed
(which in most cases is the right behavior), so #'boot.user/pi
was not even attempted to start.
Let's plug in a sample try-catcher
"on-up" and see what it does:
boot.user=> (mu/on-up :guard (mu/try-catch log-exception) :wrap-in)
{:guard
#object[clojure.core$partial$fn__4761 0x7fbb46f2 "clojure.core$partial$fn__4761@7fbb46f2"],
:info
#object[clojure.core$partial$fn__4761 0x656ab49 "clojure.core$partial$fn__4761@656ab49"]}
(we still have the :info
logger from the above section to help with a visual)
Notice the :wrap-in
instead of :after
or :before
.
Let's stop and start it again:
boot.user=> (mount/stop)
{:stopped ["#'boot.user/server"]}
boot.user=> (mount/start)
INFO mount-up.core - >> starting.. #'boot.user/server
INFO mount-up.core - >> starting.. #'boot.user/db
ERROR boot.user - could not start [#'boot.user/db] due to "Divide by zero"
INFO mount-up.core - >> starting.. #'boot.user/pi
{:started ["#'boot.user/server" "#'boot.user/pi"]}
this time we "controlled" the exception, reported the problem and decided the system may start without a database.
Let's check what all these state look like:
boot.user=> (require '[mount.tools.graph :as graph])
boot.user=> (graph/states-with-deps)
({:name "#'boot.user/server", :order 1, :status #{:started}, :deps #{}}
{:name "#'boot.user/db", :order 2, :status #{:stopped}, :deps #{}}
{:name "#'boot.user/pi", :order 3, :status #{:started}, :deps #{}})
again, a built in try-catch
is just an example of a custom wrapper function.
Copyright © 2018 tolitius
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.