Giter Site home page Giter Site logo

mount's Introduction

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

<! release <! clojars

any questions or feedback: #mount clojurians slack channel (or just open an issue)

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.core :refer [defstate]])

Creating State

Creating state is easy:

(defstate conn :start (create-conn))

where the create-conn function creates a connection (for example to a database) and is defined elsewhere, can be right above it.

In case this state needs to be cleaned / destroyed 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.db :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.

Documentation String

As in any definition (i.e. def, defn) a documentation string can be added to better describe a state:

(defstate answer
  "answer to the ultimate question of life universe and everything"
  :start (+ 1 41))
(doc answer)
-------------------------
dev/answer
  answer to the ultimate question of life universe and everything

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, threads, connections, 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 dependencies that mount respects:

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

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

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

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

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

here is an example of a web server that "depends" on a similar config.

(the example load-config function above comes from cprop, but could of course be a custom function that loads configuration from a file)

Value of values

Lifecycle functions start/stop can take both functions and values. This is "valuable" and also works:

(defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)

While it would be useful in REPL and for testing, real application states would usually have start / stop logic, in other words, the real lifecycle.

Besides scalar values, lifecycle functions can take anonymous functions, partial functions, function references, etc.. Here are some examples:

(defn f [n]
  (fn [m]
    (+ n m)))

(defn g [a b]
  (+ a b))

(defn- pf [n]
  (+ 41 n))

(defn fna []
  42)

(defstate scalar :start 42)
(defstate fun :start #(inc 41))
(defstate with-fun :start (inc 41))
(defstate with-partial :start (partial g 41))
(defstate f-in-f :start (f 41))
(defstate f-no-args-value :start (fna))
(defstate f-no-args :start fna)
(defstate f-args :start g)
(defstate f-value :start (g 41 1))
(defstate private-f :start pf)

Check out fun-with-values-test for more details.

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 application state can be reloaded in REPL e.g.:

dev=> (require '[mount.core :as mount])

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

While it is not always necessary, mount lifecycle 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..  config

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

08:21:39.462 [nREPL-worker-1] DEBUG mount - >> starting..  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.

Composing States

Besides calling (mount/start) there are other useful ways to start an application:

While all of these are great by themselves, sometimes it is really handy to compose these super powers. For example to start an application with only certain states, swapping a couple of them for new values, while passing runtime arguments.

Composer's Toolbox

Each "tool" has a single responsibility and can be composed with other tools in any combination and order.

  • only will return only states that it is given + exist (seen by mount) in the application
  • except will return all the states that it is given except a given set
  • swap will take a map with keys as states and values as their substitute values
  • swap-states will take a map with keys as states and values with {:start fn :stop fn} as their substitute states
  • with-args will take a map that could later be accessed by (mount/args)

All these functions take one or two arguments. If called with two arguments, the first one will be treated as the universe of states to work with. If called with one argument, it will work with all known to mount states.

None of these functions start or stop the application states, they merely serve as transformations from the initial set of states to the one that will later be passed to (mount/start).

Be Composing

All of the above is much easier to understand by looking at examples:

(-> (only #{#'foo/a
            #'foo/b
            #'foo/c
            #'bar/d
            #'baz/e})
    (except [#'foo/c
             #'bar/d])
    (with-args {:a 42})
    mount/start)

This would start off from 5 states, even though the whole application may have many more states available. It would then exclude two states (i.e. #'foo/c and #'bar/d), then it will pass runtime arguments {:a 42}, and finally it will start the remaining three states: #'foo/a, #'foo/b, #'baz/e.

You may notice that only takes a set, while except takes a vector in this example. This is done intentionally to demonstrate that both these functions can take any collection of states. set would make more sense for most cases though.

Here is a more "involved" example:

(-> (only #{#'foo/a
            #'foo/b
            #'foo/c
            #'bar/d
            #'baz/e})
    (with-args {:a 42})
    (except [#'foo/c
             #'bar/d])
    (swap-states {#'foo/a {:start #(create-connection test-conf)
                           :stop #(disconnect a)}})
    (swap {#'baz/e {:datomic {:uri "datomic:mem://composable-mount"}}})
    mount/start)

This will do the same thing as the previous example plus it would swap #'foo/a with alternative :start and :stop functions and #'baz/e with {:datomic {:uri "datomic:mem://composable-mount"}} value before starting the application.

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's lifecycle functions, i.e. start/stop, can optionally take states as vars (i.e. prefixed with their namespaces):

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

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

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

Start an Application Without Certain States

Whether it is in REPL or during testing, it is often useful to start an application without certain states. These can be queue listeners that are not needed at REPL time, or a subset of an application to test.

The start-without function can do just that:

(mount/start-without #'app.feeds/feed-listener
                     #'app/nrepl)

which will start an application without starting feed-listener and nrepl states.

Here is an example test that excludes Datomic connection and nREPL from an application on start.

Swapping Alternate Implementations

During testing it is often very useful to mock/stub certain states. For example running a test against an in memory database vs. the real one, running with a publisher that publishes to a test core.async channel vs. the real remote queue, etc.

Swapping States with Values

The start-with function takes values as substitutes.

Say we have a send-sms state:

(ns app.sms)
;; ...
(defstate send-sms :start (create-sms-sender
                            (:sms config)))

When running tests it would be great not to send the real text messages, but rather send them all to a local core.async channel instead:

(let [sms-ch (chan)
      send-sms (fn [sms] (go (>! sms-ch sms)))]
  (mount/start-with {#'app.sms/send-sms send-sms})   ;; <<<< swapping the "send-sms" state with a test function
  ;; testing.. checking "sms-ch" channel
  (mount/stop))

start-with takes a map of states with their substitutes. For example #'app.sms/send-sms here is the real deal SMS sender that is being substituted with a send-sms test function.

Swapping States with States

The start-with-states function takes values in a form of {:start fn :stop fn} as substitutes:

(mount/start-with-states {#'app.neo/db        {:start #(connect test-config)
                                               :stop #(disconnect db)}
                          #'app.neo/publisher {:start #(create-pub test-config)
                                               :stop #(close-pub publisher)}})

start-with-states takes a map of states with their substitutes. For example #'app.nyse/db here is the real deal (remote) DB that is being substituted with #(connect test-config) function, which could end up being anything, a map, an in memory DB, etc.

The :stop functions of substitutes can be anything, and could refer to the original state references. As in the example above: db and publisher are real references. They would need to be accessible from the namespace of course, so you might need to (:require [app.neo :refer [db]]) in order to use db in :stop #(disconnect db) example above.

--

One thing to note is whenever

(mount/stop)

is run after start-with/start-with-states, it rolls back to an original "state of states", i.e. #'app.neo/db is #'app.neo/db again. So subsequent calls to (mount/start) or even to (mount/start-with {something else}) will start from a clean slate.

Here is an example test that starts an app with mocking Datomic connection and nREPL.

Stop an Application Except Certain States

Calling (mount/stop) will stop all the application states. In case everything needs to be stopped besides certain ones, it can be done with (mount/stop-except).

Here is an example of restarting the application without bringing down #'app.www/nyse-app:

dev=> (mount/start)
14:34:10.813 [nREPL-worker-0] INFO  mount.core - >> starting..  config
14:34:10.814 [nREPL-worker-0] INFO  mount.core - >> starting..  conn
14:34:10.814 [nREPL-worker-0] INFO  app.db - creating a connection to datomic: datomic:mem://mount
14:34:10.838 [nREPL-worker-0] INFO  mount.core - >> starting..  nyse-app
14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:4242
14:34:10.843 [nREPL-worker-0] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED org.eclipse.jetty.server.Server@194f37af
14:34:10.844 [nREPL-worker-0] INFO  mount.core - >> starting..  nrepl
:started

dev=> (mount/stop-except #'app.www/nyse-app)
14:34:47.766 [nREPL-worker-0] INFO  mount.core - << stopping..  nrepl
14:34:47.766 [nREPL-worker-0] INFO  mount.core - << stopping..  conn
14:34:47.766 [nREPL-worker-0] INFO  app.db - disconnecting from  datomic:mem://mount
14:34:47.766 [nREPL-worker-0] INFO  mount.core - << stopping..  config
:stopped
dev=>

dev=> (mount/start)
14:34:58.673 [nREPL-worker-0] INFO  mount.core - >> starting..  config
14:34:58.674 [nREPL-worker-0] INFO  app.config - loading config from test/resources/config.edn
14:34:58.674 [nREPL-worker-0] INFO  mount.core - >> starting..  conn
14:34:58.674 [nREPL-worker-0] INFO  app.db - creating a connection to datomic: datomic:mem://mount
14:34:58.693 [nREPL-worker-0] INFO  mount.core - >> starting..  nrepl
:started

Notice that the nyse-app is not started the second time (hence no more accidental java.net.BindException: Address already in use). It is already up and running.

Recompiling Namespaces with Running States

Mount will detect when a namespace with states (i.e. with (defstate ...)) was reloaded/recompiled, and will check every state in this namespace whether it was running at the point of recompilation. If it was, it will restart it:

  • if a state has a :stop function, mount will invoke it on the old version of state (i.e. cleanup)
  • it will call a "new" :start function after this state is recompiled/redefined

Mount won't keep it a secret, it'll tell you about all the states that had to be restarted during namespace reload/recompilation:

same is true for recompiling and reloading (figwheel, boot-reload, etc.) namespaces in ClojureScript:

Providing a :stop function is optional, but in case a state needs to be cleaned between restarts or on a system shutdown, :stop is highly recommended.

:on-reload

By default a state will be restarted on its redefinition or a namespace recompilation. However it is not always a desired behavior. Sometimes it's ok to have stale references during REPL sessions / development, other times all that is needed is not a "restart", but just a "stop".

This behavior could be controlled with an optional :on-reload meta attribute when defining a state.

In case nothing needs to be done to a running state on reload / recompile / redef, set :on-reload to :noop:

(defstate ^{:on-reload :noop}
          mem-db :start (connect config)
                 :stop (disconnect mem-db))

When a running state needs to be just "stopped" on reload, set :on-reload to :stop:

(defstate ^{:on-reload :stop}
          mem-db :start (connect config)
                 :stop (disconnect mem-db))

Again, by default, if no :on-reload meta is added, internally it would be set to :restart, in which case a running state will be restarted on a redef / a namespace reload.

Note that ^{:on-reload :noop} will disable stopping or starting the state on namespace recompilation but it will still obey (mount/start) / (mount/stop) calls. This means that if any of the namespaces with (mount/start) / (mount/stop) calls are reloaded or these calls are explicitely executed (i.e. somewhere in the dev namespace or in an :after clause), the state's start/stop functions will still be called.

Cleaning up Deleted States

Mount will detect when a state was renamed/deleted from a namespace, and will do two things:

  • if a state had a :stop function, mount will invoke it on the old version of state (i.e. cleanup)
  • will remove any knowledge of this state internally

Here is an example:

dev=> (defstate won't-be-here-long :start (println "I am starting... ")
                                   :stop (println "I am stopping... "))
#'dev/won't-be-here-long
dev=>

dev=> (mount/start #'dev/won't-be-here-long)
INFO  app.utils.logging - >> starting..  #'dev/won't-be-here-long
I am starting...
{:started ["#'dev/won't-be-here-long"]}
dev=>

"deleting" it from REPL, and starting all the states:

dev=> (ns-unmap 'dev 'won't-be-here-long)
nil
dev=> (mount/start)

"<< stopping.. #'dev/won't-be-here-long (it was deleted)"
I am stopping...

INFO  app.utils.logging - >> starting..  #'app.conf/config
INFO  app.utils.logging - >> starting..  #'app.db/conn
INFO  app.utils.logging - >> starting..  #'app.www/nyse-app
INFO  app.utils.logging - >> starting..  #'app.example/nrepl
{:started ["#'app.conf/config" "#'app.db/conn" "#'app.www/nyse-app" "#'app.example/nrepl"]}

Mount detected that #'dev/won't-be-here-long was deleted, hence:

<< stopping.. #'dev/won't-be-here-long (it was deleted)

cljc mode

By default mount states are kept under var references. While it works for Clojure, it falls short in the land of ClojureScript since, especially during an :advanced compilation, var names get compressed + ClojureScript does not support reified vars.

To support both Clojure and ClojureScript mount has a cljc mode which is well documented in here, and can be enabled by (mount/in-cljc-mode).

Disable Lazy Start

When in cljc mode, mount states that are not started by (mount/start a b c), or that are not transitive states: i.e. not :required at the time (mount/start) is called, will start lazily whenever they are dereferenced:

=> (mount/in-cljc-mode)
:cljc

=> (defstate db-connection :start (println "connecting")
                           :stop (println "disconnecting..."))

=> db-connection
#object[mount.core.DerefableState 0x546b9d51 {:status :pending, :val nil}]

dev=> (mount/running-states)
#{}

dev=> @db-connection   ;;   db-connection will start here when deref'ed even though it was not started explicitly
connecting

dev=> (mount/running-states)
#{"#'dev/db-connection"}

This can be quite handy as it allows certain app states to start lazily.

However there are cases when it is best to fail in case a certain state is deref'ed while it was not yet started. This is possible by marking such states with ^{:on-lazy-start :throw} metadata:

=> (defstate ^{:on-lazy-start :throw} db-connection :start (do (println "connecting") 42)
                                                    :stop (println "disconnecting..."))

=> @db-connection   ;;   this will throw since db connection is deref'ed before it was started

java.lang.RuntimeException: :on-lazy-start is set to :throw i.e. (defstate {:on-lazy-start :throw} #'dev/db-connection...) and #'dev/db-connection state was not explicitly started before it was deref'ed (i.e. @#'dev/db-connection)

=> (mount/start #'dev/db-connection)
connecting
{:started ["#'dev/db-connection"]}

=> @db-connection
42

Packaging

Since mount relies on the Clojure/Script Compiler to learn about all the application states, before mount/start is called all the namespaces that have defstates need to be compiled.

At the development time this requirement is mostly transparent, since these namespaces are compiled with nREPL, or refreshed with "tools.namespace", etc. But it becomes important when packaging an application or when starting a web application via lein-ring's or boot-http's :init hooks.

Depending on a structure and a kind of an application, this means that these namespaces need to be :required prior to a call to mount/start when packaging the app as a stand alone JAR or a WAR.

This can be easily done with choosing an application entry point, which could be a web handler namespace with routes or just an arbitrary app namespace (i.e. my.app). In this app entry point namespace all other namespaces that have defstate would be :required and a call to the mount/start function would be defined:

(ns my.app
  (:require [a]
            [b]
            [c]
            [mount.core :as mount]))

(defn rock-n-roll []                   ;; or (defn -main [args].. )
  (mount/start))

this would ensure that at the time (rock-n-roll) is called, all the namespaces with states were compiled (i.e. mount knows about them). (rock-n-roll) can be used in/as a -main function or as a web hook such as :init.

In practice only a few namespaces need to be :required, since others will be brought in transitively (i.e. by already required namespaces). From the my.app example above, say we had namespaces d, e and f that are required by a, and g and h that are required by b. They (d, e, f, g and h) won't need to be required by my.app, since a and b would "bring" them in.

Affected States

Every time a lifecycle function (start/stop) is called mount will return all the states that were affected:

dev=> (mount/start)
{:started [#'app.config/config
           #'app.nyse/conn
           #'app/nrepl]}
dev=> (mount/stop)
{:stopped [#'app/nrepl
           #'app.nyse/conn
           #'app.config/config]}

An interesting bit here is a vector vs. a set: all the states are returned in the order they were affected.

Logging

All the mount examples have >> starting.. / << stopping.. logging messages, but when I develop an application with mount I don't see them.

Valid question. It was a conscious choice not to depend on any particular logging library, since there are few to select from, and this decision is best left to the developer who may choose to use mount.

Since mount is a library it should not bring any dependencies unless its functionality directly depends on them.

But I still these logging statements in the examples...

mount-up

One way to do that would be using "mount-up" that "watches mount's ups and downs":

=> (require '[mount-up.core :as mu])

=> (mu/on-upndown :info mu/log :before)

=> (mount/start)
INFO  mount-up.core - >> starting.. #'boot.user/server
{:started ["#'boot.user/server"]}

=> (mount/stop)
INFO  mount-up.core - << stopping.. #'boot.user/server
{:stopped ["#'boot.user/server"]}

Manual AOP

Another, a more manual way, would be to do it via an excellent robert hooke. Example applications live in test, so does the utility that adds logging to all the mount's lifecycle functions on start in dev.clj.

Exception Handling

One way to handle exceptions on start/stop would be to simply wrap start/stop functions in try/catch.

Another way would be to use a custom mount-up wrapper.

Clojure Version

Since mount supports both Clojure and ClojureScript, it relies on Reader Conditionals that were introduced in Clojure 1.7. mount's code is not precompiled (i.e. AOT) and distributed in .cljc sources, hence it currently requires Clojure 1.7 and above.

Mount and Develop!

Besides a a collection of sample mount applications, mount sources come with two sample apps:

  • Clojure app
  • ClojureScript app

You can clone mount, jump into a REPL and start playing with these built in apps.

Below is an example of the Clojure app that comes with mount.

The app has 4 states:

  • config, loaded from the files and refreshed on each (reset)
  • datomic connection that uses the config to create itself
  • nyse web app which is a web server with compojure routes (i.e. the actual app)
  • nrepl that uses config to bind to host/port

Running New York Stock Exchange

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

$ boot repl

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

start/restart/reset everything using (reset):

dev=> (reset)

:reloading (mount.tools.macro mount.core app.utils.logging app.conf app.db app.utils.datomic app.nyse app.www app.example dev)
INFO  app.utils.logging - >> starting..  #'app.conf/config
INFO  app.conf - loading config from dev/resources/config.edn
INFO  app.utils.logging - >> starting..  #'app.db/conn
INFO  app.db - conf:  {:datomic {:uri datomic:mem://mount}, :www {:port 4242}, :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}, :nrepl {:host 0.0.0.0, :port 7878}}
INFO  app.db - creating a connection to datomic: datomic:mem://mount
INFO  app.utils.logging - >> starting..  #'app.www/nyse-app
INFO  app.utils.logging - >> starting..  #'app.example/nrepl
dev=>

everything is started and can be played with:

dev=> (add-order conn {:ticker "GOOG" :bid 665.51M :offer 665.59M :qty 100})
dev=> (add-order conn {:ticker "GOOG" :bid 665.50M :offer 665.58M :qty 300})

dev=> (find-orders conn "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})

since there is also a web server running, we can add orders with HTTP POST (from a different terminal window):

$ curl -X POST -d "ticker=TSLA&qty=100&bid=232.38&offer=232.43" "http://localhost:4242/nyse/orders"

{"added":{"ticker":"TSLA","qty":"100","bid":"232.38","offer":"232.43"}}
dev=> (find-orders conn "TSLA")
({:db/id 17592186045422, :order/symbol "TSLA", :order/bid 232.38M, :order/qty 100, :order/offer 232.43M})

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

note: a simple (mount/stop) / (mount/start) will also work, (reset) is for "convenience + ns refresh":

dev=> (reset)
INFO  app.utils.logging - << stopping..  #'app.example/nrepl
INFO  app.utils.logging - << stopping..  #'app.www/nyse-app
INFO  app.utils.logging - << stopping..  #'app.db/conn
INFO  app.db - disconnecting from  datomic:mem://mount
INFO  app.utils.logging - << stopping..  #'app.conf/config

:reloading (app.conf app.db app.nyse app.www app.example dev)

INFO  app.utils.logging - >> starting..  #'app.conf/config
INFO  app.conf - loading config from dev/resources/config.edn
INFO  app.utils.logging - >> starting..  #'app.db/conn
INFO  app.db - conf:  {:datomic {:uri datomic:mem://mount}, :www {:port 4242}, :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}, :nrepl {:host 0.0.0.0, :port 7878}}
INFO  app.db - creating a connection to datomic: datomic:mem://mount
INFO  app.utils.logging - >> starting..  #'app.www/nyse-app
INFO  app.utils.logging - >> starting..  #'app.example/nrepl
:ready

notice that it stopped and started again.

In app.db connection :stop calls a disconnect function where a database is deleted. Hence after (reset) was called the app was brought its starting point: database was created by the :start that calls a new-connection function, and db schema is created by nyse.app.

But again no orders:

dev=> (find-orders conn "GOOG")
()
dev=> (find-orders conn "TSLA")
()

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

dev=> (add-order conn {:ticker "TSLA" :bid 232.381M :offer 232.436M :qty 250})

dev=> (find-orders conn "TSLA")
({:db/id 17592186045418, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M})

New York Stock Exchange Maintenance

Say we want to leave the exchange functioning, but would like to make sure that no one can hit it from the web. Easy, just stop the web server:

dev=> (mount/stop #'app.www/nyse-app)
INFO  app.utils.logging - << stopping..  #'app.www/nyse-app
{:stopped ["#'app.www/nyse-app"]}
dev=>
$ curl localhost:4242
curl: (7) Failed to connect to localhost port 4242: Connection refused

everything but the web server works as before:

dev=> (find-orders conn "TSLA")
({:db/id 17592186045420, :order/symbol "TSLA", :order/bid 232.381M, :order/qty 250, :order/offer 232.436M})
dev=>

once we found who DDoSed us on :4242, and punished them, we can restart the web server:

dev=> (mount/start #'app.www/nyse-app)
INFO  app.utils.logging - >> starting..  #'app.www/nyse-app
{:started ["#'app.www/nyse-app"]}
dev=>
$ curl localhost:4242
welcome to the mount sample app!

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 © 2020 tolitius

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

mount's People

Contributors

achengs avatar arichiardi avatar au-phiware avatar ckampfe avatar cloojure avatar dgr avatar dijonkitchen avatar dm3 avatar doglooksgood avatar edvorg avatar frankhenderson avatar grammati avatar hipitihop avatar ideal-knee avatar jmarca avatar jstepien avatar jumarko avatar krajj7 avatar malchmih avatar manenko avatar paulrd avatar rajcspsg avatar rgkirch avatar runejuhl avatar ryfow avatar tolitius avatar wambat avatar yatesco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mount's Issues

remove tools.logging dependency

Logging Choice

There are logging alternatives such as unilog or timbre, and mount should not bring tools.logging with it: while it can be excluded, it's just an extra step that should be avoided.

Visual Help

The way mount currently logging states while bringing them up and down is quite helpful to resolve any exceptions during start/stop/suspend/resume, since it is visible which state caused it:

22:13:06.591 [nREPL-worker-0] INFO  mount.core - >> starting..  app-config
22:13:06.592 [nREPL-worker-0] INFO  mount.core - >> starting..  conn
22:13:06.767 [nREPL-worker-0] INFO  mount.core - >> starting..  web-server

BindException Address already in use  java.net.PlainSocketImpl.socketBind (PlainSocketImpl.java:-2)

so it is obvious that the answer to the problem is in starting.. web-server.

Exception Details

On the other hand, mount adds details to the exception to make is clear when/where it occurred. For example if the above exception is looked at in REPL *e, it would have the details about web-server:

#error {
 :cause "Address already in use"
 :via
 [{:type java.lang.RuntimeException
   :message "could not start [web-server] due to"                 <<<<<<<<<<<<<<
   :at [mount.core$up$fn__4959 invoke "core.clj" 55]}
  {:type java.net.BindException
   :message "Address already in use"
   :at [java.net.PlainSocketImpl socketBind "PlainSocketImpl.java" -2]}]
 :trace
 [[java.net.PlainSocketImpl socketBind "PlainSocketImpl.java" -2]
  [java.net.AbstractPlainSocketImpl bind "AbstractPlainSocketImpl.java" 382]

Therefore I believe it is safe to remove mount logging all together.

Needs Thinking

If logging is removed, and there are no exceptions during start/stop, it is not obvious which states have started / stopped / suspended / resumed.

Feels like all lifecycle functions need to simply either return all the states that they changed OR return the current app status:

({:name conn, :started? true, :suspended? false}
 {:name nrepl, :started? true, :suspended? false}
 {:name q-listener, :started? true}
 {:name web-server, :started? false}
 {:name test-conn, :started? false}
 {:name test-nrepl, :started? false}
 {:name app-config, :started? true, :suspended? false})

With simply returning a set of states a particular lifecycle method affected, there is a caveat: start can also resume.

visualizing dependency graph

it would be a nice feature to be able to print out the dependency graph that ends up being generated based on the namespaces

question about fun-with-values test/example

Not really an issue; probably more a lack of understanding due to my newness to clojure.

I'm trying to understand your fun-with-value example.

lein test on your code repo works fine, so clearly there isn't a bug.

However, when I try to use the approach in my code, I get errors like:

... .mount_test$g cannot be cast to java.util.concurrent.Future

I copied the test file as is into my project. I'm using boot, not lein, but otherwise it should work the same.

My stripped down version of your test is:

(ns calvad.watcher.mount-test
  (:require [clojure.test :as t :refer [is are deftest testing use-fixtures]]
            [mount.core :as mount :refer [defstate]]))

(alter-meta! *ns* assoc ::load false)


(defn g [a b]
  (+ a b))

(defstate f-args :start g)

(defn start-states []
  (mount/start #'calvad.watcher.mount-test/f-args))

(use-fixtures :once #((start-states) (%) (mount/stop)))

(deftest fun-with-values
  (is (= (@f-args 41 1) 42)))

When run, I get

ERROR in (fun-with-values) (core.clj:2206)
expected: (= ((clojure.core/deref f-args) 41 1) 42)
  actual: java.lang.ClassCastException: calvad.watcher.mount_test$g cannot be cast to java.util.concurrent.Future

Any hints on what is missing would be appreciated.

CLJS: can't find "cleanup-if-dirty" var unless in CLJC mode

boot.user=> (start-repl)

cljs.user=> (require-macros '[mount.core :refer [defstate]])
nil
cljs.user=> (defstate a :start 42)

clojure.lang.ExceptionInfo: Unable to resolve var: cleanup-if-dirty in this context at line 1 <cljs repl> {:file "<cljs repl>", :line 1, :column 1, :tag :cljs/analysis-error}

cljs.user=>

cljs.user=> (require '[mount.core :as mount])
nil
cljs.user=> (mount/in-cljc-mode)
:cljc
cljs.user=> (defstate a :start 42)
#'cljs.user/a
cljs.user=> a
#object[mount.core.DerefableState]
cljs.user=> @a
42

thanks to @frankhenderson for reporting this. his sample project that confirms the behavior.

Different system configurations

Is there a way to have different system configurations (based on command line arguments or config etc.)? The way I'm using component in a large app is that I have a "server" system and a "cli" system. The "cli" system doesn't have all bells and whistles of the "server" system (e.g. it doesn't need the web server) it uses a different logging subsystem etc. Is there a way to do that with your library?

Note that they don't exist at the same time.

failed to initalize with boot version 2.6.0

$ boot repl

works fine with boot 2.5.2 (except for some deprecation warnings), but with boot version 2.6.0, I get:

     clojure.lang.ExceptionInfo: template already refers to: #'boot.core/template in namespace: adzerk.boot-reload
    data: {:file "adzerk/boot_reload.clj", :line 1}
java.lang.IllegalStateException: template already refers to: #'boot.core/template in namespace: adzerk.boot-reload
                                        ...
                         clojure.core/refer                          core.clj: 4098
                                        ...
                         clojure.core/apply                          core.clj:  632
                      clojure.core/load-lib                          core.clj: 5730
                                        ...
                         clojure.core/apply                          core.clj:  632
                     clojure.core/load-libs                          core.clj: 5749
                                        ...
                         clojure.core/apply                          core.clj:  632
                       clojure.core/require                          core.clj: 5832
                                        ...
adzerk.boot-reload/eval1054/loading--auto--                   boot_reload.clj:    1
                adzerk.boot-reload/eval1054                   boot_reload.clj:    1
                                        ...
                       clojure.core/load/fn                          core.clj: 5866
                          clojure.core/load                          core.clj: 5865
                                        ...
                      clojure.core/load-one                          core.clj: 5671
                   clojure.core/load-lib/fn                          core.clj: 5711
                      clojure.core/load-lib                          core.clj: 5710
                                        ...
                         clojure.core/apply                          core.clj:  632
                     clojure.core/load-libs                          core.clj: 5749
                                        ...
                         clojure.core/apply                          core.clj:  632
                       clojure.core/require                          core.clj: 5832
                                        ...
                           boot.user/eval70  boot.user1854421973038905773.clj:    9
                                        ...
                         boot.main/-main/fn                          main.clj:  196
                            boot.main/-main                          main.clj:  196
                                        ...
                           boot.App.runBoot                          App.java:  399
                              boot.App.main                          App.java:  488
                                        ...
                                  Boot.main                         Boot.java:  258

Swap should have an option to compose the original state

When I use start-with or start-with-states, I need to provide a new value/state that replaces the value specified by the :start expression. I would like to have an ability to wrap/compose the original :start expression.

For e.g., let us say we have a state holding a DB connection.

(defstate db-conn :start (init-db-conn))

I want to run my tests with a mock DB connection that simulates a slow connection. So, it will be very handy to have something like this:

(defstate mock-db-conn :start-with-compose (fn [orig-conn] (wrap orig-conn)))

(mount/start-with-states {#'db-conn #'mock-db-conn})

The :start-with-compose key specifies a function that takes the original DB connection initialized in the state db-conn as a parameter and produces a new connection that will be swapped in. So the initialization proceeds like this.

  1. Evaluate the :start expression, in this case (init-db-conn).
  2. Pass that to the :start-with-compose and get the returned value.
  3. The returned value gets bound to db-conn state.

Of course, this is a trivial example and one can very well argue that I can directly use (init-db-connection) in my mock-db-conn instead. But the ability to compose initialization expressions will be very handy when the initialization expressions are lot more complex.

stopping substitues

Versions

[org.clojure/clojure "1.8.0"]
[mount "0.1.10"]

Steps to reproduce the issue

(defn create [name from]
  (let [id (gensym)]
    (println "creating" name id "from" from)
    {:name name
     :id   id}))

(defn destroy [state from]
  (println "destroying" (:name state) (:id state) "from" from))

(defstate a :start (create "a" "start a")
            :stop  (destroy a "stop a"))

(defstate b :start (create "b" "start b")
            :stop  (destroy b "stop b"))

(defn info []
  (println "current states:")
  (println " " (mount.tools.graph/states-with-deps))
  (println "  a:" (:name a) (:id a))
  (println "  b:" (:name b) (:id b)))

(defn this-works-as-expected []
  (mount/start)
  (info)
  (mount/stop))

(defn this-is-not-stopping-b []
  (mount/start-with-states {#'user/a #'user/b})
  (info)
  (mount/stop))

no surprises when calling (this-works-as-expected)

user=> (this-works-as-expected)
creating a G__12960 from start a
creating b G__12961 from start b
current states:
  ({:name #'user/a, :order 1, :status #{:started}, :deps #{}} {:name #'user/b, :order 2, :status #{:started}, :deps #{}})
  a: a G__12960
  b: b G__12961
destroying b G__12961 from stop b
destroying a G__12960 from stop a
{:stopped ["#'user/b" "#'user/a"]}

but calling (this-is-not-stopping-b) does not properly stop the substitute

user=> (this-is-not-stopping-b)
creating b G__13029 from start b
current states:
  ({:name #'user/a, :order 1, :status #{:started}, :deps #{}} {:name #'user/b, :order 2, :status #{:stopped}, :deps #{}})
  a: b G__13029
  b: nil nil
destroying nil nil from stop b
{:stopped ["#'user/a"]}

Expected result

user=> (this-is-not-stopping-b)
...
destroying b G__13029 from stop b

Actual result

user=> (this-is-not-stopping-b)
...
destroying nil nil from stop b

Differences to Component: Multiple instances of system for testing

Hi,

One feature of Component is that multiple separate systems can be started in the same Clojure runtime with different settings. This is very useful for running tests against system.

As Mount attaches the state to namespaces, I presume that it's not possible to have multiple separate applications running in one Clojure runtime?

Would be great if you could mention this on differences to component documentation.

Swapping alternate implementations

Need a way to start an app with certain states being substituted. This is very useful for testing.

Currently I am thinking something like:

(mount/start-with {:db #'app/test-db 
                   :sms-sender #'app/test-sms-sender)

The keys will most likely need to be prefixed with their ns to avoid collisions.

does not compile with [clojurescript .. :classifier "aot"]

everything works in case the clojurescript dependency is:

:dependencies [[org.clojure/clojurescript "version"]]

but when it is brought in as AOT compiled:

:dependencies [[org.clojure/clojurescript "version" :classifier "aot"]]

things get wild:


WARNING: Use of undeclared Var cljs.tools.reader/find-ns at line 368 dev/resources/public/js/compiled/out/cljs/tools/reader.cljs
WARNING: Use of undeclared Var cljs.tools.reader/ns-name at line 369 dev/resources/public/js/compiled/out/cljs/tools/reader.cljs
WARNING: Use of undeclared Var cljs.tools.reader/*ns* at line 386 dev/resources/public/js/compiled/out/cljs/tools/reader.cljs
WARNING: Use of undeclared Var cljs.tools.reader/tagged-literal at line 788 dev/resources/public/js/compiled/out/cljs/tools/reader.cljs
WARNING: Use of undeclared Var cljs.tagged-literals/uuid at line 31 dev/resources/public/js/compiled/out/cljs/tagged_literals.cljc
WARNING: Use of undeclared Var cljs.analyzer/munge at line 180 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/munge at line 337 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/*print-err-fn* at line 365 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/munge at line 406 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/ns-interns* at line 489 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/symbol-identical? at line 862 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/symbol-identical? at line 862 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/symbol-identical? at line 864 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/symbol-identical? at line 866 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/char? at line 986 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/char? at line 986 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 1675 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 1675 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 2303 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 2305 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 2307 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-ns at line 2308 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 2321 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/find-macros-ns at line 2323 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var cljs.analyzer/symbol-identical? at line 2349 dev/resources/public/js/compiled/out/cljs/analyzer.cljc
WARNING: Use of undeclared Var mount.tools.macro/defmacro at line 3 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/on-error at line 3 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/msg at line 3 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/f at line 3 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/f at line 5 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/msg at line 9 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/defmacro at line 11 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/throw-runtime at line 11 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/msg at line 11 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.tools.macro/msg at line 13 src/mount/tools/macro.cljc
WARNING: Use of undeclared Var mount.core/defmacro at line 130 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/defstate at line 130 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/state at line 130 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/& at line 130 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/body at line 130 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/state at line 131 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/body at line 131 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var mount.core/var? at line 185 /Users/tolitius/1/fun/mount/src/mount/core.cljc
WARNING: Use of undeclared Var datascript.core/uuid at line 232 dev/resources/public/js/compiled/out/datascript/core.cljc

need to learn more about the reasons.
would appreciate any help from cljs gurus.

Error of plain value as `start` form

When I tried to use a plain value like a map as the start form, an error is thrown:

boot.user=> (defstate config :start {:init :state})
nil
boot.user=> (start)
21:21:30.007 [nREPL-worker-16] INFO  mount.core - >> starting..  config

 java.lang.RuntimeException: could not start [config] due to
clojure.lang.ArityException: Wrong number of args (0) passed to: PersistentVector

In defstate, should this

:start `(fn [] (~@start)) 

be changed to this?

:start `(fn [] ~start) 

A reset function which will reset all the states even if there are errors.

Situation I ran into:

This is interesting.
I defined a var with a defstate but made a mistake the first time around.

;; not the same mistake but it will help elaborate what I’m trying to explain

(def conn1-atom (atom nil))
(defstate conn
  :start (reset! conn1-atom :started)
  :stop (reset! :started conn1-atom))  ;; there is a mistake in :stop.

(mount/start) ;; this works fine.
(mount/stop) ;; produces the error.

ClassCastException clojure.lang.Keyword cannot be cast to clojure.lang.IAtom clojure.core/reset! (core.clj:2273)

;; trying to re-define conn but keep running into the same error.

(defstate conn
  :start (reset! conn1-atom :started)
  :stop (reset! conn1-atom :stopped))

A function to reset all the states/connections even if they produce error would be nice.
This was mentioned on Slack's mount channel by @tolitius .
"it might make sense to potentially have a some kind of (mount/reset) that could take params: i.e. (mount/reset :ignore-errors true). which, if well documented, could help in situations like this one."

have an option to not restart on recompile

when a namespace gets recompiled, in case it had states defined inside, it would stop and start (i.e. restart) these states to make sure they are not lost / bound to resources (more details).

this is usually a preferred behavior, but in some instances (see this discussion) it would be best to not restart states on recompile.

this needs more thinking so the original state references are not lost after "not restarting".

changing :start-with to take a "config" map

While it's good to have separate APIs to swap / filter states needed. It is limiting if they don't compose.

We started discussing it here with @aroemers, and this was one of the primary reasons for a mount-lite spinoff.

I really want to preserve the varargness of (mount/start) / (mount/stop) since it is really simple and useful in REPL, hence I don't want to overload them. But I feel (mount/start-with) can be a great candidate for composing the mount configuration. e.g.

(mount/start-with {:only #{}                   
                   :swap {}
                   :swap-states {}
                   :except #{}
                   :args {}
                   :with-deps {}})

This is the current approach that we discussed with @wkf on #mount slack channel, and it does break the existing (mount/start-with), but I feel the timing and the nature of its use still permits it.

How to reload (stop and start) a state on a -main function

Hi,

I've been reading the documentation but I'm not sure about how to fix this. I've defined something like this:


(defstate server-config :start {:port 7890 :join? false})

(defn start-server [server-config]
  (when-let [server (run-server myserv-ring-handler server-config)]
    (println "Server has started!")
    server))

(defstate myserv-server :start (start-server server-config)
          :stop  (myserv-server :timeout 100))

(defn system-port [args]
  (Integer/parseInt
    (or (System/getenv "PORT")
        (first args)
        "7890")))

(defn -main [& args]
  (mount/start-with-states
    {#'myserv/server-config
     {:start #(array-map :port (system-port args)
                         :join? false)}}))

So when I "lein run" everything works, but whenever I change a file, and the http-kit server is stopped, the command stops. For the moment I'm doing "while true; do lein run; done" to work, so I've thought about adding an infinite loop to the -main function, but it doesn't feel like this is the right way.

How should I do this?

lifecycle hook API

Would be niice to have open around hooks into lifecycle methods. This would address "cross cutting concerns":

  • logging
  • exception handling
  • security
  • performace
  • transaction management
  • validation
  • etc..

where all these functions can be custom made, and shared.
this could be a point of reference to bounce off of.

Clarity on using in uberwar

Hi - apologies if this isn't the place for support ;-).

One pain I feel all the time with these 'state/dependency-management' libs is the conflict between dynamically defining components and producing WARs which require the ring handlers to be a simple def.

In Stuarts' Component I simply have the handler pull the components it needs directly out of the #'system.

Is there a more elegant answer with mount?

Specifically, what would a web UI look like on top of your demo app which exposes the nyse-finder via a HTTP endpoint. Is it really just a simple as ring's :init method pointing to something which calls (mount/start) and the handlers then just require the nyse namespace?

Thanks!

Mount stops state after changing a .cljc file

Hi,

I was trying out 0.1.8-Snapshot as suggested and things have become better. But still I found two bugs.

The first one is that I have .cljc files in use. The code is used on clojure and clojurescript side, whenever I change the .cljc code my server component stops and does not get restarted.

I setup an example project (it is rather large, but hopefully it can help despite the noise): https://github.com/sveri/mountexample.

To reproduce:

  1. clone my example
  2. Run lein figwheel
  3. Run lein repl
  4. Run (start) in the repl
  5. Navigate to http://localhost:3000/example
  6. Open foo.bar.closp-schema
  7. Add an an extra line
  8. Switch back to browser, if you are fast enough you will see that figwheel reloads the changes
  9. Reload the browser and then you notice that the server is stopped and was not restarted.

The interesting namespaces regarding mount are:
foo.bar.user
and the component definitions are in:
foo.bar.components.*

State starts via side-effect

Things defined with defstate start themselves, I think due to some sort of side-effect of printing.

Example:

Given this file:

(ns foo.core
  (:require [mount.core :refer [defstate]]))

(defn- start-fn []
  (println "Launching missiles!")
  (+ 40 2))

(defstate blah
  :start (start-fn))

I started a repl (nrepl, in emacs, via leiningen), evaluated the namespace, then simply evaluated blah twice.

foo.core> blah
Launching missiles!
#mount.core.DerefableState[{:status :ready, :val #object[mount.core.NotStartedState 0x20ae4d21 "'#'foo.core/blah' is not started (to start all the states call mount/start)"]} 0x199b0da2]
foo.core> blah
42

Note that I never called mount.core/start, but blah is now started.

So a defstate seems to create some sort of Heisenvar, such that simply looking at it changes its value.

lein test mount.test.fun-with-values fails

I was curious why the semantics of defstate had @ (deref) in the test file. Those semantics don't seem to work as described. lein test mount.test.fun-with-values fails as expected, but lein test passes (running complete test suite). There seems to be some test state that I'm not aware of. Is this a bug or a feature?

mount/start-with should also take instances

While this works fine:

(mount/start-with {#'app.sms/send-sms #'test.app/send-sms})

it requires #'test.app/send-sms to be created, which could make it hard to narrow down the scope for the test (to a let binding).

Have start-with to take instances instead [(!) breaking, but worth it] / as an addition to [might not worth it]:

(let [sms-ch (chan)
      send-sms (fn [sms] (go (>! sms-ch sms)))]
  (mount/start-with {#'app.sms/send-sms send-sms})
  ;; testing.. and
  (mount/stop))

Any plans for a 0.1.0 release?

I'd like to use this in a releasable uberjar, but the SNAPSHOT thing worries me.

Are there plans on the horizon for a 0.1.0 release?

Pass data to start functions

I frequently write command line programs that rely heavily on command line arguments for parameters. This is just one use case, but it would be great to somehow pass arbitrary data to the start functions like so:

(defn -main
  [& args]
  (mount/start {:config_file (first args)}))

Dealing with failure on mount/start

At the moment there isn't a lot of guidance on how to deal with exceptions inside defstate start/stop methods.

My case is mostly related to bad config - if a user provides bad arguments to a program (specifically a directory that is supposed to contain certain files, and if those files are missing parts of the program will not be able to function.) how can we deal with some state being bad?

@tolitius suggested (in slack) wrapping the start method in a (try ... (catch ... )) which works if a default value can be supplied, however in many cases that may not be possible (database connection failure, for example)

In the database connection case currently I'm considering still using the try-catch and modifying application routes to prevent some actions, or changing having the defstate hold false or nil value to indicate failure, and having reliant parts of the application check the value before doing work. This pushes that concern out across large parts of the system, especially since this is largely affects the views - we want to disable buttons and provide error hints, for instance.

removing :suspend and :resume

These were added before realization that the same functionality can be achieved via:

(mount/stop-except ...)

and they unnecessary complicate the codebase making it harder to refacrtor / add new features.

I spoke to several people who use them in their apps, they agree that (mount/stop-except ...) can be used in all the cases they've used suspend / resume.

Hence the plan is to remove this API (i.e. :suspend / :resume)

Discussion: replace start-* functions with single start function with options

First of all, mount seems a great idea. I am currently refactoring a stuartsierra/component app to mount's defstates, in order to evaluate it. I like how stuff becomes more clojuresque. What I am missing though are two things (also mentioned in the following issues: #2 #8 #9)

  1. A solid way to pass arguments to defstate's start functions. I am not so fond of the with-args approach, binding them to a single var/atom in the mount.core namespace.
  2. A single data source that defines how the modules compose, and which states are started. My use case is having a "standard library" of module namespaces, and reusing those in actual apps. In mount's current state, some of this can be achieved, by using functions like start-with, start-without, but those do not compose. And even if they did, the required nesting/composition is not really programmatically configurable.

So, I was thinking, what about a single start function, taking a map for mix-n-matching the available options? This also makes mount more extendible with new features, without the need to update or add new API functions.

For example, what about this?

(mount/start
 {;; Only start the following state vars, replacing `(start <state>+)`
  :only [#'my.mount.config/config]
  ;; Or, start all known state vars, except the following,
  ;; replacing `(start-without ...)`
  :except [#'my.mount.db.datomic/connection]

  ;; Substitute some states' start and stop functions with those of
  ;; other states, replacing `(start-with {...})`
  :substitute-with-var   {#'my.mount.config/config #'my.mount.config.map/data}
  ;; and/or substitute some states with fixed values (new feature)
  :substitute-with-val   {#'my.mount.config/config {:my 'config-data}}
  ;; and/or substitute some states' start and stop functions with
  ;; those specified here directly (new feature)
  :substitute-with-state {#'my.mount.config/config {:start {:my 'config-data}}}

  ;; Specify arguments for state start functions, expecting those
  ;; functions to have (at least) a 1-arity version.
  :arguments {#'my.mount.config/config         {:resource "/config.edn"}
              #'my.mount.db.datomic/connection {:config-var #'my.mount.config/config}
              #'my.mount.config.map/data       {:my 'config-data}}})

This composes all options nicely, and the data structure can be passed around, read from a file and/or altered with Clojure's core functions (as advocated in https://www.youtube.com/watch?v=3oQTSP4FngY). I think this approach opens up the possibility of composing the modules better, enabling this "standard library" approach.

Of course, if you still want to offer a more function-based API, one could add "builder" functions, such as:

(-> (mount/with {#'my.mount.config/config #'my.mount.config.map/data})
    (mount/without #'my.mount.db.datomic/connection)
    (mount/with-args {#'my.mount.config/config {:resource "/config.edn"}})
    (mount/start))

I am very interested in your opinion, and if you like it, I'll try to find the time to write the actual code for this idea.

weird compilation issues when using aot w/use of defstate macro

Hi,
We're using mount to handle a single connection to a datomic database in our app, and
recently ran into an issue.
To fully describe what's happening here:

  • we have two git branches (develop and feature/add-feature)
  • On develop, db.clj resides at src/datomic_to_catalyst/utils/db.clj (ns: datomic-to-catalyst.utils.db)
  • On the feature branch, we moved the db.clj module up a level.

Switching between the two branches and issueing any lein repl/run/ring command will cause re-compilation to be attempted, which triggers the error linked to above.
In order to work around the error, the following steps "work around" the issue:

  1. switch git branch
  2. run lein .... (ClassDefNotFound error is thrown pointing at mount/core/DeferrableState | defstate macro)
  3. touch -m <path to db.clj>
  4. run lein (works again)

Repeating steps 0 .. 3 (on a different branch where db.clj is re-located) re-produces the error consistently.

Investigating further, my colleague found that disabling aot compilation also works-around this issue.

I found this which seems to indicate that this could be the cause of the issue here.

Specifically, the following passage was an "ah-ha!" moment:

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.

(Our project.clj is currently using aot compilation)

Load/execution order not clear

Hi,

I'm not sure if I'm missing the point here, but I'm having trouble with things not being started at the point when I need them. For example, when I have 2 files...

(ns oddity.core
  (:require [mount :refer (defstate)]))

(println "Loading core")
(defn open-a-thing []
  "hello world")
(defstate thing
  :start (open-a-thing))

and

(ns oddity.thing-user
  (:require [oddity.core :refer (thing)]))

(println "Opening thing_user")
(println thing)

Then I get the following

user> (dev)
#object[clojure.lang.Namespace 0x2ce92a78 "dev"]
dev> go
#object[dev$go 0x50c9780d "dev$go@50c9780d"]
dev> (go)
:ready
dev> (reset)
:reloading (oddity.core oddity.thing-user oddity.core-test dev)
#object[mount.NotStartedState 0x1d2642c9 'thing' is not started (to start all the states call mount/start)]
:ready

Here's my project

(defproject oddity "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [mount "0.1.2"]]
  :profiles {:dev {:dependencies [[org.clojure/tools.namespace "0.2.11"]]
                   :source-paths ["dev"]}})

My dev/dev.clj is a modified version of the nyse example

(ns dev
  "Tools for interactive development with the REPL. This file should
  not be included in a production build of the application."
  ;; (:use [cljs.repl :only [repl]]
  ;;       [cljs.repl.browser :only [repl-env]])
  (:require [clojure.java.io :as io]
            [clojure.java.javadoc :refer [javadoc]]
            [clojure.pprint :refer [pprint]]
            [clojure.reflect :refer [reflect]]
            [clojure.repl :refer [apropos dir doc find-doc pst source]]
            [clojure.set :as set]
            [clojure.string :as str]
            [clojure.test :as test]
            ;; [clojure.core.async :refer [>!! <!! >! <! go-loop alt! timeout]]
            [clojure.tools.namespace.repl :as tn]
            [mount]))

(defn start []
  (mount/start))

(defn stop []
  (mount/stop))

(defn refresh []
  (stop)
  (tn/refresh))

(defn refresh-all []
  (stop)
  (tn/refresh-all))

(defn go
  "starts all states defined by defstate"
  []
  (start)
  :ready)

(defn reset
  "stops all states defined by defstate, reloads modified source files, and restarts the states"
  []
  (stop)
  (tn/refresh :after 'dev/go))

and dev/user.clj

(defn dev
  []
  (require 'dev)
  (in-ns 'dev))

cljs: optimize state names in :advance mode

The cljs version of mount only allows states to be named as strings.

For example, on the example cljs app: https://github.com/tolitius/mount/blob/0825ad2ed085b73b7ae989b4382ce4e0376e4be3/dev/cljs/app/example.cljs
the app.audit-log/log state is named as a string like this: (js/setTimeout #(mount/stop-except "#'app.audit-log/log") 500)

Since the cljs compiler never munges strings, having a lot of long namespaces will result in a lot of un-optimized references to mount states. It'd be awesome to find a way to optimize the mount state names as well during :advanced compile!

Error in https://clojars.org/repo/mount/mount/maven-metadata.xml

The clojars repository has the following file:
https://clojars.org/repo/mount/mount/maven-metadata.xml

It has a bug in it that makes lein-ancient fail. It references a version (0.2.0-SNAPSHOT) that does not exist in clojars.org or maven.org and therefore causes lein-ancient to fail to pick up the latest snapshot of mount (currently 0.1.8-SNAPSHOT).

here is the relevant section of the file:

0.1.0-SNAPSHOT
0.1.0
0.2.0-SNAPSHOT ;; <- delete this line
0.1.2
0.1.3-SNAPSHOT
0.1.1

":with-deps" to manually override the start order / dependencies

In case of indirect dependencies sometimes it is needed to specify a particular order in which they start.

For example if we have a message bus and a subscriber that are indirectly connected via, say, a core.async channel, we need to make sure the subscriber does not start subscribing to the message bus before the bus is started.

It is not a simple task to validate the dependency order just for indirect dependencies:

i.e. in case a state a is "enriched" with a dependency on state b, how do we know that by starting b a bit ​_earlier_​, to satisfy this dependency, we not hurting it (b) by now not ​_yet_​ starting states that b naturally depends on

since mount records only a linear dependency / start order.

Therefore the idea (that belongs to @wkf) is to optionally specify all the dependencies on mount start e.g.:

(mount/start-with {:with-deps {... dependency graph / order ...}})

This has to do with start-with rebranding #47, and, as a side effect, will add more dependency graph explicitness.

Full source file names visible in compiled js file

Hi Anatoly,
After compiling some cljs using :advanced mode I happened to look in the minified file and saw that where I call defstate the output includes the full path of the file. For example, I end up with code like this:

ff([bv, S, ow, Gw, Zz, GC, mF, Xg, eJ, pK],
   [IF, KG, "/Users/tom/dev/kc/src/admin/client/components/app_state.cljs", 20, 1, 103, 103, ue, null , k(U6) ? U6.Kb : null])

I'm fairly certain mount is the culprit because I only see this occurring for files with a defstate. Aside from the fact that I'd like to conceal from my users that I develop on a mac, it seems wasteful to include all that in the minified script. I haven't been able to hunt down the offending lines, so could you point me in the right direction?

Mount fails to reload with exception

The second bug is not easy to understand for me. Also I am not sure if it is mounts fault or not, it would be nice if you could have a look at it.

To reproduce:

  1. clone my example: https://github.com/sveri/mountexample
  2. Run lein figwheel
  3. Run lein repl
  4. Run (start) in the repl
  5. Navigate to http://localhost:3000/example
  6. Open foo.bar.routes.home
  7. Change the println statement in line 19 to print something different
  8. Switch back to browser and reload.

You will see this stacktrace in the repl now:

"<< stopping.. #'foo.bar.components.server/server (namespace was recompiled)"
">> starting.. #'foo.bar.components.server/server (namespace was recompiled)"
Wed Dec 23 08:14:35 CET 2015 [worker-1] ERROR - GET /example
java.lang.RuntimeException: could not start [#'foo.bar.components.server/server] due to, compiling:(foo/bar/components/server.clj:31:1)
at clojure.lang.Compiler.load(Compiler.java:7239)
at clojure.lang.RT.loadResourceScript(RT.java:371)
at clojure.lang.RT.loadResourceScript(RT.java:362)
at clojure.lang.RT.load(RT.java:446)
at clojure.lang.RT.load(RT.java:412)
at clojure.core$load$fn__5448.invoke(core.clj:5866)
at clojure.core$load.doInvoke(core.clj:5865)
at clojure.lang.RestFn.invoke(RestFn.java:408)
at clojure.core$load_one.invoke(core.clj:5671)
at clojure.core$load_lib$fn__5397.invoke(core.clj:5711)
at clojure.core$load_lib.doInvoke(core.clj:5710)
at clojure.lang.RestFn.applyTo(RestFn.java:142)
at clojure.core$apply.invoke(core.clj:632)
at clojure.core$load_libs.doInvoke(core.clj:5749)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.core$apply.invoke(core.clj:632)
at clojure.core$require.doInvoke(core.clj:5832)
at clojure.lang.RestFn.invoke(RestFn.java:421)
at ring.middleware.reload$wrap_reload$fn__25452.invoke(reload.clj:21)
at clojure_miniprofiler$run_handler_profiled$fn__20024.invoke(clojure_miniprofiler.clj:382)
at clojure_miniprofiler$run_handler_profiled.invoke(clojure_miniprofiler.clj:382)
at clojure_miniprofiler$wrap_miniprofiler$fn__20035.invoke(clojure_miniprofiler.clj:429)
at noir.util.middleware$wrap_request_map$fn__17803.invoke(middleware.clj:39)
at ring.middleware.keyword_params$wrap_keyword_params$fn__16572.invoke(keyword_params.clj:35)
at ring.middleware.nested_params$wrap_nested_params$fn__16622.invoke(nested_params.clj:84)
at ring.middleware.multipart_params$wrap_multipart_params$fn__16749.invoke(multipart_params.clj:117)
at ring.middleware.params$wrap_params$fn__16775.invoke(params.clj:64)
at ring.middleware.cookies$wrap_cookies$fn__16359.invoke(cookies.clj:161)
at ring.middleware.absolute_redirects$wrap_absolute_redirects$fn__16903.invoke(absolute_redirects.clj:36)
at ring.middleware.resource$wrap_resource$fn__16811.invoke(resource.clj:28)
at ring.middleware.content_type$wrap_content_type$fn__16880.invoke(content_type.clj:30)
at ring.middleware.not_modified$wrap_not_modified$fn__16861.invoke(not_modified.clj:52)
at ring.middleware.x_headers$wrap_xss_protection$fn__16211.invoke(x_headers.clj:71)
at ring.middleware.x_headers$wrap_frame_options$fn__16201.invoke(x_headers.clj:38)
at ring.middleware.x_headers$wrap_content_type_options$fn__16206.invoke(x_headers.clj:53)
at hiccup.middleware$wrap_base_url$fn__17134.invoke(middleware.clj:12)
at ring.middleware.format_params$wrap_format_params$fn__17596.invoke(format_params.clj:113)
at ring.middleware.format_params$wrap_format_params$fn__17596.invoke(format_params.clj:113)
at ring.middleware.format_params$wrap_format_params$fn__17596.invoke(format_params.clj:113)
at ring.middleware.format_response$wrap_format_response$fn__17725.invoke(format_response.clj:174)
at noir.validation$wrap_noir_validation$fn__17184.invoke(validation.clj:155)
at noir.cookies$noir_cookies$fn__17245.invoke(cookies.clj:72)
at ring.middleware.cookies$wrap_cookies$fn__16359.invoke(cookies.clj:161)
at noir.session$noir_flash$fn__17328.invoke(session.clj:158)
at ring.middleware.flash$wrap_flash$fn__16232.invoke(flash.clj:35)
at noir.session$noir_session$fn__17314.invoke(session.clj:109)
at ring.middleware.session_timeout$wrap_idle_session_timeout$fn__17270.invoke(session_timeout.clj:25)
at ring.middleware.session$wrap_session$fn__16480.invoke(session.clj:102)
at ring.middleware.file$wrap_file$fn__16832.invoke(file.clj:44)
at ring.middleware.file_info$wrap_file_info$fn__17860.invoke(file_info.clj:69)
at compojure.core$routing$fn__16050.invoke(core.clj:144)
at clojure.core$some.invoke(core.clj:2570)
at compojure.core$routing.doInvoke(core.clj:144)
at clojure.lang.RestFn.applyTo(RestFn.java:139)
at clojure.core$apply.invoke(core.clj:632)
at compojure.core$routes$fn__16054.invoke(core.clj:149)
at org.httpkit.server.HttpHandler.run(RingHandler.java:91)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: could not start [#'foo.bar.components.server/server] due to
at mount.core$up$fn__2246.invoke(core.cljc:88)
at mount.core$up.invoke(core.cljc:88)
at clojure.lang.Var.invoke(Var.java:388)
at foo.bar.components.server$eval36438.invoke(server.clj:31)
at clojure.lang.Compiler.eval(Compiler.java:6782)
at clojure.lang.Compiler.eval(Compiler.java:6772)
at clojure.lang.Compiler.load(Compiler.java:7227)
... 61 more
Caused by: java.lang.ClassCastException: mount.core.DerefableState cannot be cast to clojure.lang.IFn
at org.httpkit.server$run_server.invoke(server.clj:8)
at foo.bar.components.server$eval36438$fn__36439.invoke(server.clj:31)
at mount.core$record_BANG_.invoke(core.cljc:82)
at mount.core$up$fn__2246.invoke(core.cljc:91)
... 67 more

The interesting namespaces regarding mount are:
foo.bar.user
and the component definitions are in:
foo.bar.components.*

How to update a state with a runtime function invocation result

I see in Mount's examples that a state captures the output of the :start fn and that the :stop fn interacts with that result.

What if the arguments to the :start fn aren't known until runtime? For example, I'd like to pass in a runtime-generated set of routes to my web handler. So if I have the following function to launch a server:

(defn launch-server [routes port] (run-jetty routes {:port port}))

How do I define a state to capture the output of that function at runtime, that I can then use to close the server in my :stop fn?

(defstate http-server :start launch-server :stop ???)

Currently I'm rigging my own solution with an atom but it feels like I'm side-stepping mount rather than using it correctly:

(def server-atom (atom nil))
(defn launch-server [routes port] (reset! server-atom (run-jetty routes {:port port}))
(defstate http-server :start launch-server :stop (.stop @server-atom))

Mount Reports Removed defstates

I noticed that if I delete a defstate and then do a clojure.tools.namespace.repl/refresh, calling (mount/start) still reports that the old defstate is being started even though it is not. I have to restart the repl to get (mount/start) to report what is actually happening.

Clarify usage of mount/in-cljc-mode

The CLJS documentation mentions that (mount/in-cljc-mode) has to be invoked "anywhere before a call to (mount/start), usually at the entry point of an app: in the -main, web handler, etc.".

This was not sufficient for me, as the namespaces that are required in the system namespace containing the -main method are evaluated before -main is invoked. The solution was to add (mount/in-cljc-mode) (right after after (ns ...)) in each namespace that contains (defstate ...).

Maybe there is even a cleaner solution to the mode-switching.

Thanks for "mount"!

def doesn't work with mount

(mount/defstate foo
                :start (println "!!! start")
                :stop (println "!!! stop"))

(def app foo)

(mount/start)

Then app throw an error mount.core.DerefableState cannot be cast to clojure.lang.IFn. This is typical situation when you use ring and have app and routes definition. This is how i get this situation.

It tooks me some time to discover it. If you can't fix it it will be nice to add information about it in the readme.

(mount.core/start (mount.core/only #{})) should be a no-op

Versions

[mount "0.1.10"]
[org.clojure/clojure "1.9.0-alpha14"]

Steps to reproduce the issue

(mount.core/defstate do-not-start-me
  :start (println "Darn. I started."))

(mount.core/start (mount/only #{}))

Expected result

Nothing is printed.

Actual result

Darn. I started. is printed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.