diff --git a/README.md b/README.md index f9221b7..7a3660b 100644 --- a/README.md +++ b/README.md @@ -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) ``` diff --git a/dev/dev.clj b/dev/dev.clj index 7b23b0a..e74d05b 100644 --- a/dev/dev.clj +++ b/dev/dev.clj @@ -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)) diff --git a/doc/differences-from-component.md b/doc/differences-from-component.md index 93f678e..28b0ba2 100644 --- a/doc/differences-from-component.md +++ b/doc/differences-from-component.md @@ -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". diff --git a/doc/intro.md b/doc/intro.md deleted file mode 100644 index f1e987b..0000000 --- a/doc/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Introduction to statuo - -TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) diff --git a/doc/runtime-arguments.md b/doc/runtime-arguments.md index d226cbb..93dbd14 100644 --- a/doc/runtime-arguments.md +++ b/doc/runtime-arguments.md @@ -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 diff --git a/doc/uberjar.md b/doc/uberjar.md index d0d9444..25db9f4 100644 --- a/doc/uberjar.md +++ b/doc/uberjar.md @@ -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, diff --git a/src/mount/core.clj b/src/mount/core.clj index 64e43e4..6ca57c4 100644 --- a/src/mount/core.clj +++ b/src/mount/core.clj @@ -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) diff --git a/test/app/utils/logging.clj b/test/app/utils/logging.clj index 7a1307d..dc0216c 100644 --- a/test/app/utils/logging.clj +++ b/test/app/utils/logging.clj @@ -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) diff --git a/test/check/cleanup_dirty_states_test.clj b/test/check/cleanup_dirty_states_test.clj new file mode 100644 index 0000000..73fd0a8 --- /dev/null +++ b/test/check/cleanup_dirty_states_test.clj @@ -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)))) diff --git a/test/check/fun_with_values_test.clj b/test/check/fun_with_values_test.clj new file mode 100644 index 0000000..f1f4772 --- /dev/null +++ b/test/check/fun_with_values_test.clj @@ -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))) diff --git a/test/check/parts_test.clj b/test/check/parts_test.clj index a8c506f..53f4b38 100644 --- a/test/check/parts_test.clj +++ b/test/check/parts_test.clj @@ -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) diff --git a/test/check/private_fun_test.clj b/test/check/private_fun_test.clj new file mode 100644 index 0000000..2f2c413 --- /dev/null +++ b/test/check/private_fun_test.clj @@ -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))) diff --git a/test/check/start_with_test.clj b/test/check/start_with_test.clj index b5fe507..8e362c0 100644 --- a/test/check/start_with_test.clj +++ b/test/check/start_with_test.clj @@ -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)))) + diff --git a/test/check/suspend_resume_test.clj b/test/check/suspend_resume_test.clj index a4eb724..eb37ace 100644 --- a/test/check/suspend_resume_test.clj +++ b/test/check/suspend_resume_test.clj @@ -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)