Merge branch '0.1.5'

This commit is contained in:
anatoly 2015-12-01 08:49:14 -05:00
commit fd4d846c3f
14 changed files with 227 additions and 73 deletions

View file

@ -22,6 +22,7 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Using State](#using-state)
- [Dependencies](#dependencies)
- [Talking States](#talking-states)
- [Value of Values](#value-of-values)
- [The Importance of Being Reloadable](#the-importance-of-being-reloadable)
- [Start and Stop Order](#start-and-stop-order)
- [Start and Stop Parts of Application](#start-and-stop-parts-of-application)
@ -80,15 +81,15 @@ mount is an alternative to the [component](https://github.com/stuartsierra/compo
Creating state is easy:
```clojure
(defstate conn :start (create-conn))
(defstate conn :start create-conn)
```
where `(create-conn)` is defined elsewhere, can be right above it.
where the `create-conn` function is defined elsewhere, can be right above it.
In case this state needs to be cleaned / destryed between reloads, there is also `:stop`
```clojure
(defstate conn :start (create-conn)
(defstate conn :start create-conn
:stop (disconnect conn))
```
@ -153,6 +154,44 @@ this `app-config`, being top level, can be used in other namespaces, including t
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)
is an example of a Datomic connection that "depends" on a similar `app-config`.
## Value of values
Lifecycle functions start/stop/suspend/resume can take both functions and values. This is "valuable" and also works:
```clojure
(defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)
```
Besides scalar values, lifecycle functions can take anonymous functions, partial functions, function references, etc.. Here are some examples:
```clojure
(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](https://github.com/tolitius/mount/blob/0.1.5/test/check/fun_with_values_test.clj) 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
@ -311,9 +350,9 @@ and some other use cases.
In additiong to `start` / `stop` functions, a state can also have `resume` and, if needed, `suspend` ones:
```clojure
(defstate web-server :start (start-server ...)
:resume (resume-server ...)
:stop (stop-server ...))
(defstate web-server :start start-server
:resume resume-server
:stop stop-server)
```

View file

@ -19,11 +19,9 @@
(defn start []
(with-logging-status)
(mount/start-without #'check.start-with-test/test-conn
#'check.start-with-test/test-nrepl
#'check.parts-test/should-not-start
#'check.suspend-resume-test/web-server
#'check.suspend-resume-test/q-listener)) ;; example on how to start app without certain states
(mount/start #'app.config/app-config
#'app.nyse/conn
#'app/nrepl)) ;; example on how to start app with certain states
(defn stop []
(mount/stop))

View file

@ -137,8 +137,8 @@ Depending on the number of application components the "extra" size may vary.
Mount is pretty much:
```clojure
(defstate name :start (fn)
:stop (fn))
(defstate name :start fn
:stop fn)
```
no "ceremony".

View file

@ -1,3 +0,0 @@
# Introduction to statuo
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

View file

@ -84,13 +84,13 @@ In order to demo all of the above, we'll build an uberjar:
```bash
$ lein do clean, uberjar
...
Created .. mount/target/mount-0.2.0-SNAPSHOT-standalone.jar
Created .. mount/target/mount-0.1.5-SNAPSHOT-standalone.jar
```
Since we have a default for a Datomic URI, it'll work with no arguments:
```bash
$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar
22:12:03.290 [main] INFO mount - >> starting.. app-config
22:12:03.293 [main] INFO mount - >> starting.. conn
@ -101,7 +101,7 @@ $ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar
Now let's ask it to help us:
```bash
$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar --help
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar --help
22:13:48.798 [main] INFO mount - >> starting.. app-config
22:13:48.799 [main] INFO app.config -
@ -116,7 +116,7 @@ this is a sample mount app to demo how to pass and read runtime arguments
And finally let's connect to the Single Malt Database. It's Friday..
```bash
$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar -d datomic:mem://single-malt-database
$ java -jar target/mount-0.1.5-SNAPSHOT-standalone.jar -d datomic:mem://single-malt-database
22:16:10.733 [main] INFO mount - >> starting.. app-config
22:16:10.737 [main] INFO mount - >> starting.. conn

View file

@ -38,15 +38,14 @@ where `nyse-app` is _the_ app. It has the usual routes:
and the reloadable state:
```clojure
(defn start-nyse []
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
(defn start-nyse [{:keys [www]}]
(-> (routes mount-example-routes)
(handler/site)
(run-jetty {:join? false
:port (get-in app-config [:www :port])})))
:port (:port www)})))
(defstate nyse-app :start (start-nyse)
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
(defstate nyse-app :start (start-nyse app-config)
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
```
In order not to block, and being reloadable, the Jetty server is started in the "`:join? false`" mode which starts the server,

View file

@ -1,11 +1,11 @@
(ns mount.core
(:require [clojure.tools.macro :as macro]))
;; (defonce ^:private session-id (System/currentTimeMillis))
(defonce ^:private mount-state 42)
(defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
(defonce ^:private state-seq (atom 0))
(defonce ^:private state-order (atom {}))
(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
;; supporting tools.namespace: (disable-reload!)
(alter-meta! *ns* assoc ::load false) ;; to exclude the dependency
@ -29,17 +29,39 @@
(and suspend (not resume)) (throw
(IllegalArgumentException. "suspendable state should have a resume function (i.e. missing :resume fn)"))))
(defn- with-ns [ns name]
(str ns "/" name))
(defn- pounded? [f]
(let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
(.startsWith (str f) pound)))
(defn- unpound [f]
(if (pounded? f)
(nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
f))
(defn- cleanup-if-dirty
"in case a namespace is recompiled without calling (mount/stop),
a running state instance will still be running.
this function stops this 'lost' state instance.
it is meant to be called by defstate before defining a new state"
[state]
(when-let [stop (@running state)]
(stop)))
(defmacro defstate [state & body]
(let [[state params] (macro/name-with-attributes state body)
{:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)]
(validate lifecycle)
(cleanup-if-dirty (with-ns *ns* state))
(let [s-meta (cond-> {:mount-state mount-state
:order (make-state-seq state)
:start `(fn [] (~@start))
:started? false}
stop (assoc :stop `(fn [] (~@stop)))
suspend (assoc :suspend `(fn [] (~@suspend)))
resume (assoc :resume `(fn [] (~@resume))))]
:order (make-state-seq (with-ns *ns* state))
:start `(fn [] ~start)
:status #{:stopped}}
stop (assoc :stop `(fn [] ~(unpound stop)))
suspend (assoc :suspend `(fn [] ~suspend))
resume (assoc :resume `(fn [] ~resume)))]
`(defonce ~(with-meta state (merge (meta state) s-meta))
(NotStartedState. ~(str state))))))
@ -48,44 +70,46 @@
(swap! done conj (ns-resolve ns name))
state))
(defn- up [var {:keys [ns name start started? resume suspended?] :as state} done]
(when-not started?
(let [s (try (if suspended?
(defn- up [var {:keys [ns name start stop resume status] :as state} done]
(when-not (:started status)
(let [s (try (if (:suspended status)
(record! state resume done)
(record! state start done))
(catch Throwable t
(throw (RuntimeException. (str "could not start [" name "] due to") t))))]
(intern ns (symbol name) s)
(alter-meta! var assoc :started? true :suspended? false))))
(swap! running assoc (with-ns ns name) stop)
(alter-meta! var assoc :status #{:started}))))
(defn- down [var {:keys [ns name stop started? suspended?] :as state} done]
(when (or started? suspended?)
(defn- down [var {:keys [ns name stop status] :as state} done]
(when (some status #{:started :suspended})
(when stop
(try
(record! state stop done)
(catch Throwable t
(throw (RuntimeException. (str "could not stop [" name "] due to") t)))))
(intern ns (symbol name) (NotStartedState. name)) ;; (!) if a state does not have :stop when _should_ this might leak
(alter-meta! var assoc :started? false :suspended? false)))
(swap! running dissoc (with-ns ns name))
(alter-meta! var assoc :status #{:stopped})))
(defn- sigstop [var {:keys [ns name started? suspend resume] :as state} done]
(when (and started? resume) ;; can't have suspend without resume, but the reverse is possible
(when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
(defn- sigstop [var {:keys [ns name suspend resume status] :as state} done]
(when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
(when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
(let [s (try (record! state suspend done)
(catch Throwable t
(throw (RuntimeException. (str "could not suspend [" name "] due to") t))))]
(intern ns (symbol name) s)))
(alter-meta! var assoc :started? false :suspended? true)))
(alter-meta! var assoc :status #{:suspended})))
(defn- sigcont [var {:keys [ns name start started? resume suspended?] :as state} done]
(defn- sigcont [var {:keys [ns name start resume status] :as state} done]
(when (instance? NotStartedState var)
(throw (RuntimeException. (str "could not resume [" name "] since it is stoppped (i.e. not suspended)"))))
(when suspended?
(when (:suspended status)
(let [s (try (record! state resume done)
(catch Throwable t
(throw (RuntimeException. (str "could not resume [" name "] due to") t))))]
(intern ns (symbol name) s)
(alter-meta! var assoc :started? true :suspended? false))))
(alter-meta! var assoc :status #{:started}))))
;;TODO args might need more thinking
(defn args [] @-args)
@ -110,7 +134,7 @@
(defn states-with-deps []
(let [all (find-all-states)]
(->> (map (comp #(add-deps % all)
#(select-keys % [:name :order :ns :started? :suspended?])
#(select-keys % [:name :order :ns :status])
meta)
all)
(sort-by :order))))
@ -129,9 +153,9 @@
however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
([state sub]
(merge-lifecycles state nil sub))
([state origin {:keys [start stop suspend resume suspended?]}]
([state origin {:keys [start stop suspend resume status]}]
(assoc state :origin origin
:suspended? suspended?
:status status
:start start :stop stop :suspend suspend :resume resume)))
(defn- rollback! [state]
@ -140,7 +164,7 @@
(alter-meta! state #(merge-lifecycles % origin)))))
(defn- substitute! [state with]
(let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :suspended?])
(let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
origin (meta state)
sub (meta with)]
(alter-meta! with assoc :sub? true)
@ -148,8 +172,7 @@
(defn- unsub [state]
(when (-> (meta state) :sub?)
(alter-meta! state assoc :sub? nil
:started false)))
(alter-meta! state dissoc :sub?)))
(defn- all-without-subs []
(remove (comp :sub? meta) (find-all-states)))
@ -167,11 +190,8 @@
(defn stop-except [& states]
(let [all (set (find-all-states))
states (remove (set states) all)
_ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
stopped (bring states down >)]
(dorun (map rollback! states)) ;; restore to origin from "start-with"
{:stopped stopped}))
states (remove (set states) all)]
(apply stop states)))
(defn start-with-args [xs & states]
(reset! -args xs)

View file

@ -17,13 +17,13 @@
"mount.core$sigcont" :resume
:noop)))
(defn whatcha-doing? [{:keys [started? suspended? suspend]} action]
(defn whatcha-doing? [{:keys [status suspend]} action]
(case action
:up (if suspended? ">> resuming"
(if-not started? ">> starting"))
:down (if (or started? suspended?) "<< stopping")
:suspend (if (and started? suspend) "<< suspending")
:resume (if suspended? ">> resuming")))
:up (if (status :suspended) ">> resuming"
(if-not (status :started) ">> starting"))
:down (if (or (status :started) (status :suspended)) "<< stopping")
:suspend (if (and (status :started) suspend) "<< suspending")
:resume (if (status :suspended) ">> resuming")))
(defn log-status [f & args]
(let [{:keys [ns name] :as state} (second args)

View file

@ -0,0 +1,13 @@
(ns check.cleanup_dirty_states_test
(:require [mount.core :as mount]
[app]
[clojure.test :refer :all]))
(deftest cleanup-dirty-states
(let [_ (mount/start)]
(is (not (.isClosed (:server-socket app/nrepl))))
(require 'app :reload)
(mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
(is (not (.isClosed (:server-socket app/nrepl))))
(mount/stop)
(is (instance? mount.core.NotStartedState app/nrepl))))

View file

@ -0,0 +1,55 @@
(ns check.fun-with-values-test
(:require [mount.core :as mount :refer [defstate]]
[clojure.test :refer :all]))
(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)
(defn with-fun-and-values [f]
(mount/start #'check.fun-with-values-test/scalar
#'check.fun-with-values-test/fun
#'check.fun-with-values-test/with-fun
#'check.fun-with-values-test/with-partial
#'check.fun-with-values-test/f-in-f
#'check.fun-with-values-test/f-args
#'check.fun-with-values-test/f-no-args-value
#'check.fun-with-values-test/f-no-args
#'check.fun-with-values-test/private-f
#'check.fun-with-values-test/f-value)
(f)
(mount/stop))
(use-fixtures :each with-fun-and-values)
(deftest fun-with-values
(is (= scalar 42))
(is (= (fun) 42))
(is (= with-fun 42))
(is (= (with-partial 1) 42))
(is (= (f-in-f 1) 42))
(is (= f-no-args-value 42))
(is (= (f-no-args) 42))
(is (= (f-args 41 1) 42))
(is (= (private-f 1) 42))
(is (= f-value 42)))

View file

@ -3,7 +3,7 @@
[app.nyse :refer [conn]]
[clojure.test :refer :all]))
(defstate should-not-start :start (constantly 42))
(defstate should-not-start :start #(constantly 42))
(defn with-parts [f]
(m/start #'app.config/app-config #'app.nyse/conn)

View file

@ -0,0 +1,14 @@
(ns check.private-fun-test
(:require [mount.core :as mount :refer [defstate]]
[check.fun-with-values-test :refer [private-f]]
[clojure.test :refer :all]))
(defn with-fun-and-values [f]
(mount/start #'check.fun-with-values-test/private-f)
(f)
(mount/stop))
(use-fixtures :each with-fun-and-values)
(deftest fun-with-valuesj
(is (= (private-f 1) 42)))

View file

@ -5,10 +5,10 @@
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defstate test-conn :start (long 42)
:stop (constantly 0))
(defstate test-conn :start 42
:stop #(constantly 0))
(defstate test-nrepl :start (vector))
(defstate test-nrepl :start [])
(deftest start-with
@ -44,3 +44,4 @@
(is (instance? mount.core.NotStartedState test-conn))
(is (instance? mount.core.NotStartedState test-nrepl))
(mount/stop))))

View file

@ -25,9 +25,9 @@
:suspend (suspend :q)
:resume (resume :q))
(deftest suspendable
(defstate randomizer :start (rand-int 42))
;; lifecycle
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
@ -66,9 +66,10 @@
(is (instance? mount.core.NotStartedState app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? mount.core.NotStartedState conn))
(is (instance? mount.core.NotStartedState web-server))))
(is (instance? mount.core.NotStartedState web-server)))))
;; start-with
(deftest suspendable-start-with
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
@ -85,7 +86,24 @@
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable one,
;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
;; (unchanged/not substituted in its lexical scope), and original state won't be started
(testing "when replacing a suspendable state with a non suspendable one,
the later should not be suspendable,
the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/randomizer})
_ (mount/suspend)]
(is (integer? web-server))
(is (instance? mount.core.NotStartedState randomizer))
(mount/stop)
(mount/start)
(mount/suspend)
(is (integer? randomizer))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable started one,
the later should not be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
@ -93,7 +111,7 @@
_ (mount/start-with {#'check.suspend-resume-test/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
_ (mount/suspend)]
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? datomic.peer.LocalConnection web-server))
(is (= web-server :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
(mount/stop)
(mount/start)
(mount/suspend)