diff --git a/README.md b/README.md index 0ba9502..684f239 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,6 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt - [Swapping Alternate Implementations](#swapping-alternate-implementations) - [Swapping States with Values](#swapping-states-with-values) - [Swapping States with States](#swapping-states-with-states) -- [Suspending and Resuming](#suspending-and-resuming) - - [Suspendable Lifecycle](#suspendable-lifecycle) - - [Plugging into (reset)](#plugging-into-reset) - - [Suspendable Example Application](#suspendable-example-application) - [ClojureScript is Clojure](doc/clojurescript.md#managing-state-in-clojurescript) - [Packaging](#packaging) - [Affected States](#affected-states) @@ -168,7 +164,7 @@ is an example of a web server that "depends" on a similar `config`. ## Value of values -Lifecycle functions start/stop/suspend/resume can take both functions and values. This is "valuable" and also works: +Lifecycle functions start/stop 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) @@ -263,7 +259,7 @@ You can see examples of start and stop flows in the [example app](README.md#moun 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/suspend/resume, can _optionally_ take states as vars (i.e. prefixed with their namespaces): +`mount`'s lifecycle functions, i.e. start/stop, can _optionally_ take states as vars (i.e. prefixed with their namespaces): ```clojure (mount/start #'app.config/config #'app.nyse/conn) @@ -378,80 +374,6 @@ dev=> (mount/start) 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. -## Suspending and Resuming - -Besides starting and stopping states can also be suspended and resumed. While this is not needed most of the time, it does comes really handy _when_ this need is there. For example: - -* while working in REPL, you only want to truly restart a web server/queue listener/db connection _iff_ something changed, all other times `(mount/stop)` / `(mount/start)` or `(reset)` is called, these states should not be restarted. This might have to do with time to connect / bound ports / connection timeouts, etc.. - -* when taking an application out of rotation in a data center, and then phasing it back in, it might be handy to still keep it _up_, but suspend all the client / novelty facing components in between. - -and some other use cases. - -### Suspendable Lifecycle - -In addition 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) - -``` - -`suspend` function is optional. Combining this with [(mount/stop-except)](#stop-an-application-except-certain-states), can result in an interesting restart behavior where everything is restared, but this `web-server` is _resumed_ instead (in this case `#'app.www/nyse-app` is an example of the above `web-server`): - -```clojure -dev=> (mount/stop-except #'app.www/nyse-app) -14:44:33.991 [nREPL-worker-1] INFO mount.core - << stopping.. nrepl -14:44:33.992 [nREPL-worker-1] INFO mount.core - << stopping.. conn -14:44:33.992 [nREPL-worker-1] INFO app.db - disconnecting from datomic:mem://mount -14:44:33.992 [nREPL-worker-1] INFO mount.core - << stopping.. config -:stopped -dev=> - -dev=> (mount/suspend) -14:44:52.467 [nREPL-worker-1] INFO mount.core - >> suspending.. nyse-app -:suspended -dev=> - -dev=> (mount/start) -14:45:00.297 [nREPL-worker-1] INFO mount.core - >> starting.. config -14:45:00.297 [nREPL-worker-1] INFO mount.core - >> starting.. conn -14:45:00.298 [nREPL-worker-1] INFO app.db - creating a connection to datomic: datomic:mem://mount -14:45:00.315 [nREPL-worker-1] INFO mount.core - >> resuming.. nyse-app -14:45:00.316 [nREPL-worker-1] INFO mount.core - >> starting.. nrepl -:started -``` - -Notice `>> resuming.. nyse-app`, which in [this case](https://github.com/tolitius/mount/blob/suspendable/test/app/www.clj#L32) just recreates Datomic schema vs. doing that _and_ starting the actual web server. - -### Plugging into (reset) - -In case `tools.namespace` is used, this lifecycle can be easily hooked up with `dev.clj`: - -```clojure -(defn start [] - (mount/start)) - -(defn stop [] - (mount/suspend) - (mount/stop-except #'app.www/nyse-app)) - -(defn reset [] - (stop) - (tn/refresh :after 'dev/start)) -``` - -### Suspendable Example Application - -An [example application](https://github.com/tolitius/mount/tree/suspendable/test/app) with a suspendable web server and `dev.clj` lives in the `suspendable` branch. You can clone mount and try it out: - -``` -$ git checkout suspendable -Switched to branch 'suspendable' -``` - ## Recompiling Namespaces with Running States Mount will detect when a namespace with states (i.e. with `(defstate ...)`) was reloaded/recompiled, @@ -566,28 +488,22 @@ In practice only a few namespaces need to be `:require`d, since others will be b ## Affected States -Every time a lifecycle function (start/stop/suspend/resume) is called mount will return all the states that were affected: +Every time a lifecycle function (start/stop) is called mount will return all the states that were affected: ```clojure dev=> (mount/start) {:started [#'app.config/config #'app.nyse/conn - #'app/nrepl - #'check.suspend-resume-test/web-server - #'check.suspend-resume-test/q-listener]} + #'app/nrepl]} ``` ```clojure -dev=> (mount/suspend) -{:suspended [#'check.suspend-resume-test/web-server - #'check.suspend-resume-test/q-listener]} -``` -```clojure -dev=> (mount/start) -{:started [#'check.suspend-resume-test/web-server - #'check.suspend-resume-test/q-listener]} +dev=> (mount/stop) +{:started [#'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 changed_. +An interesting bit here is a vector vs. a set: all the states are returned _in the order they were affected_. ## Logging diff --git a/dev/clj/app/utils/logging.clj b/dev/clj/app/utils/logging.clj index 2d8c64d..df58144 100644 --- a/dev/clj/app/utils/logging.clj +++ b/dev/clj/app/utils/logging.clj @@ -34,9 +34,7 @@ (defonce lifecycle-fns #{#'mount.core/up - #'mount.core/down - #'mount.core/sigstop - #'mount.core/sigcont}) + #'mount.core/down}) (defn without-logging-status [] (doall (map #(clear-hooks %) lifecycle-fns))) diff --git a/doc/differences-from-component.md b/doc/differences-from-component.md index 6d27742..1053ef3 100644 --- a/doc/differences-from-component.md +++ b/doc/differences-from-component.md @@ -197,7 +197,6 @@ Testing is not alien to Mount and it knows how to do a thing or two: * [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states) * [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations) * [stop an application except certain states](https://github.com/tolitius/mount#stop-an-application-except-certain-states) -* [suspending and resuming](https://github.com/tolitius/mount#suspending-and-resuming) After [booting mount](http://www.dotkam.com/2015/12/22/the-story-of-booting-mount/) I was secretly thinking of achieving multiple separate systems by running them in different [Boot Pods](https://github.com/boot-clj/boot/wiki/Pods). diff --git a/src/mount/core.cljc b/src/mount/core.cljc index b1036d5..95f6816 100644 --- a/src/mount/core.cljc +++ b/src/mount/core.cljc @@ -30,8 +30,7 @@ (defn- validate [{:keys [start stop suspend resume] :as lifecycle}] (cond (not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)") - (and suspend - (not resume)) (throw-runtime "suspendable state should have a resume function (i.e. missing :resume fn)"))) + (or suspend resume) (throw-runtime "suspend / resume lifecycle support was removed in \"0.1.10\" in favor of (mount/stop-except)"))) (defn- with-ns [ns name] (str "#'" ns "/" name)) @@ -86,18 +85,16 @@ (swap! done conj state-name) state)) -(defn- up [state {:keys [start stop resume status] :as current} done] +(defn- up [state {:keys [start stop status] :as current} done] (when-not (:started status) (let [s (on-error (str "could not start [" state "] due to") - (if (:suspended status) - (record! state resume done) - (record! state start done)))] + (record! state start done))] (alter-state! current s) (swap! running assoc state {:stop stop}) (update-meta! [state :status] #{:started})))) (defn- down [state {:keys [stop status] :as current} done] - (when (some status #{:started :suspended}) + (when (some status #{:started}) (when stop (on-error (str "could not stop [" state "] due to") (record! state stop done))) @@ -105,21 +102,6 @@ (swap! running dissoc state) (update-meta! [state :status] #{:stopped}))) -(defn- sigstop [state {:keys [resume suspend status] :as current} 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 (on-error (str "could not suspend [" state "] due to") - (record! state suspend done))] - (alter-state! current s))) - (update-meta! [state :status] #{:suspended}))) - -(defn- sigcont [state {:keys [resume status] :as current} done] - (when (:suspended status) - (let [s (on-error (str "could not resume [" state "] due to") - (record! state resume done))] - (alter-state! current s) - (update-meta! [state :status] #{:started})))) - (deftype DerefableState [name] #?(:clj clojure.lang.IDeref :cljs IDeref) @@ -151,16 +133,14 @@ #?(:clj (defmacro defstate [state & body] (let [[state params] (macro/name-with-attributes state body) - {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params) + {:keys [start stop] :as lifecycle} (apply hash-map params) state-name (with-ns *ns* state) order (make-state-seq state-name)] (validate lifecycle) (let [s-meta (cond-> {:order order :start `(fn [] ~start) :status #{:stopped}} - stop (assoc :stop `(fn [] ~stop)) - suspend (assoc :suspend `(fn [] ~suspend)) - resume (assoc :resume `(fn [] ~resume)))] + stop (assoc :stop `(fn [] ~stop)))] `(do (~'defonce ~state (DerefableState. ~state-name)) (mount-it (~'var ~state) ~state-name ~s-meta) @@ -226,14 +206,14 @@ (defn- merge-lifecycles "merges with overriding _certain_ non existing keys. - i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil + i.e. :stop is in a 'state', but not in a 'substitute': it should be overriden with nil 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 status]}] + ([state origin {:keys [start stop status]}] (assoc state :origin origin :status status - :start start :stop stop :suspend suspend :resume resume))) + :start start :stop stop))) (defn- rollback! [state] (let [{:keys [origin] :as sub} (@meta-state state)] @@ -241,7 +221,7 @@ (update-meta! [state] (merge-lifecycles sub origin))))) (defn- substitute! [state with mode] - (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status]) + (let [lifecycle-fns #(select-keys % [:start :stop :status]) origin (@meta-state state) sub (if (= :value mode) {:start (fn [] with) :status :stopped} @@ -299,11 +279,3 @@ without (remove (set states) app)] (apply start without)) (start))) - -(defn suspend [& states] - (let [states (or (seq states) (all-without-subs))] - {:suspended (bring states sigstop <)})) - -(defn resume [& states] - (let [states (or (seq states) (all-without-subs))] - {:resumed (bring states sigcont <)})) diff --git a/test/clj/tapp/utils/logging.clj b/test/clj/tapp/utils/logging.clj index ea7f1ff..516101a 100644 --- a/test/clj/tapp/utils/logging.clj +++ b/test/clj/tapp/utils/logging.clj @@ -34,9 +34,7 @@ (defonce lifecycle-fns #{#'mount.core/up - #'mount.core/down - #'mount.core/sigstop - #'mount.core/sigcont}) + #'mount.core/down}) (defn without-logging-status [] (doall (map clear-hooks lifecycle-fns))) diff --git a/test/core/mount/test.cljc b/test/core/mount/test.cljc index 41158d2..1377e3a 100644 --- a/test/core/mount/test.cljc +++ b/test/core/mount/test.cljc @@ -13,7 +13,6 @@ mount.test.start-without mount.test.start-with mount.test.start-with-states - mount.test.suspend-resume )) #?(:clj (alter-meta! *ns* assoc ::load false)) @@ -32,7 +31,6 @@ 'mount.test.start-without 'mount.test.start-with 'mount.test.start-with-states - 'mount.test.suspend-resume )) (defn run-tests [] diff --git a/test/core/mount/test/suspend_resume.cljc b/test/core/mount/test/suspend_resume.cljc deleted file mode 100644 index c5f5fda..0000000 --- a/test/core/mount/test/suspend_resume.cljc +++ /dev/null @@ -1,212 +0,0 @@ -(ns mount.test.suspend-resume - (:require - #?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]] - [mount.core :as mount :refer-macros [defstate]] - [tapp.websockets :refer [system-a]] - [tapp.conf :refer [config]] - [tapp.audit-log :refer [log]]] - :clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]] - [mount.core :as mount :refer [defstate]] - [tapp.conf :refer [config]] - [tapp.nyse :refer [conn]] - [tapp.example :refer [nrepl]]]) - [mount.test.helper :refer [dval]])) - -#?(:clj (alter-meta! *ns* assoc ::load false)) - -(defn koncat [k s] - (-> (name k) - (str "-" (name s)) - keyword)) - -(defn start [s] (koncat s :started)) -(defn stop [s] (koncat s :stopped)) -(defn suspend [s] (koncat s :suspended)) -(defn resume [s] (koncat s :resumed)) - -(defstate web-server :start (start :w) - :stop (stop :w) - :suspend (suspend :w) - :resume (resume :w)) - -(defstate q-listener :start (start :q) - :stop (stop :q) - :suspend (suspend :q) - :resume (resume :q)) - -(defstate randomizer :start (rand-int 42)) - -#?(:cljs - (deftest suspendable-lifecycle - - (testing "should suspend _only suspendable_ states that are currently started" - (let [_ (mount/start) - _ (mount/suspend)] - (is (map? (dval config))) - (is (instance? datascript.db/DB @(dval log))) - (is (instance? js/WebSocket (dval system-a))) - (is (= (dval web-server) :w-suspended)) - (mount/stop))) - - (testing "should resume _only suspendable_ states that are currently suspended" - (let [_ (mount/start) - _ (mount/stop #'tapp.websockets/system-a) - _ (mount/suspend) - _ (mount/resume)] - (is (map? (dval config))) - (is (instance? mount.core.NotStartedState (dval system-a))) - (is (instance? datascript.db/DB @(dval log))) - (is (= (dval web-server) :w-resumed)) - (mount/stop))) - - (testing "should start all the states, except the ones that are currently suspended, should resume them instead" - (let [_ (mount/start) - _ (mount/suspend) - _ (mount/start)] - (is (map? (dval config))) - (is (instance? js/WebSocket (dval system-a))) - (is (instance? datascript.db/DB @(dval log))) - (is (= (dval web-server) :w-resumed)) - (mount/stop))) - - (testing "should stop all: started and suspended" - (let [_ (mount/start) - _ (mount/suspend) - _ (mount/stop)] - (is (instance? mount.core.NotStartedState (dval config))) - (is (instance? mount.core.NotStartedState (dval system-a))) - (is (instance? mount.core.NotStartedState (dval log))) - (is (instance? mount.core.NotStartedState (dval web-server))))))) - -#?(:cljs - (deftest suspendable-start-with-states - - (testing "when replacing a non suspendable state with a suspendable one, - the later should be able to suspend/resume, - the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop" - (let [_ (mount/start-with-states {#'tapp.websockets/system-a #'mount.test.suspend-resume/web-server}) - _ (mount/suspend)] - (is (= (dval system-a) :w-suspended)) - (is (instance? mount.core.NotStartedState (dval web-server))) - (mount/stop) - (mount/start) - (mount/suspend) - (is (instance? js/WebSocket (dval system-a))) - (is (= (dval web-server) :w-suspended)) - (mount/stop))))) - -#?(:clj - (deftest suspendable-lifecycle - - (testing "should suspend _only suspendable_ states that are currently started" - (let [_ (mount/start) - _ (mount/suspend)] - (is (map? (dval config))) - (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) - (is (instance? datomic.peer.LocalConnection (dval conn))) - (is (= (dval web-server) :w-suspended)) - (mount/stop))) - - (testing "should resume _only suspendable_ states that are currently suspended" - (let [_ (mount/start) - _ (mount/stop #'tapp.example/nrepl) - _ (mount/suspend) - _ (mount/resume)] - (is (map? (dval config))) - (is (instance? mount.core.NotStartedState (dval nrepl))) - (is (instance? datomic.peer.LocalConnection (dval conn))) - (is (= (dval web-server) :w-resumed)) - (mount/stop))) - - (testing "should start all the states, except the ones that are currently suspended, should resume them instead" - (let [_ (mount/start) - _ (mount/suspend) - _ (mount/start)] - (is (map? (dval config))) - (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) - (is (instance? datomic.peer.LocalConnection (dval conn))) - (is (= (dval web-server) :w-resumed)) - (mount/stop))) - - (testing "should stop all: started and suspended" - (let [_ (mount/start) - _ (mount/suspend) - _ (mount/stop)] - (is (instance? mount.core.NotStartedState (dval config))) - (is (instance? mount.core.NotStartedState (dval nrepl))) - (is (instance? mount.core.NotStartedState (dval conn))) - (is (instance? mount.core.NotStartedState (dval web-server))))))) - - -#?(:clj - (deftest suspendable-start-with-states - - (testing "when replacing a non suspendable state with a suspendable one, - the later should be able to suspend/resume, - the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop" - (let [_ (mount/start-with-states {#'tapp.example/nrepl #'mount.test.suspend-resume/web-server}) - _ (mount/suspend)] - (is (= (dval nrepl) :w-suspended)) - (is (instance? mount.core.NotStartedState (dval web-server))) - (mount/stop) - (mount/start) - (mount/suspend) - (is (instance? clojure.tools.nrepl.server.Server (dval nrepl))) - (is (= (dval web-server) :w-suspended)) - (mount/stop))) - - ;; this is a messy use case, but can still happen especially at REPL time - ;; 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-states {#'mount.test.suspend-resume/web-server #'mount.test.suspend-resume/randomizer}) - _ (mount/suspend)] - (is (integer? (dval web-server))) - (is (instance? mount.core.NotStartedState (dval randomizer))) - (mount/stop) - (mount/start) - (mount/suspend) - (is (integer? (dval randomizer))) - (is (= (dval 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) - _ (mount/suspend) - _ (mount/start-with-states {#'mount.test.suspend-resume/web-server #'tapp.nyse/conn}) ;; TODO: good to WARN on started states during "start-with-states" - _ (mount/suspend)] - (is (instance? datomic.peer.LocalConnection (dval conn))) - (is (= (dval 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) - (is (instance? datomic.peer.LocalConnection (dval conn))) - (is (= (dval 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 suspendable one, - the later should be suspendable, - the original should still be suspended and preserve its lifecycle fns after the rollback/stop" - (let [_ (mount/start) - _ (mount/suspend) - _ (mount/start-with-states {#'mount.test.suspend-resume/web-server - #'mount.test.suspend-resume/q-listener})] ;; TODO: good to WARN on started states during "start-with-states" - (is (= (dval q-listener) :q-suspended)) - (is (= (dval web-server) :q-resumed)) - (mount/suspend) - (is (= (dval q-listener) :q-suspended)) - (is (= (dval web-server) :q-suspended)) - (mount/stop) - (is (instance? mount.core.NotStartedState (dval web-server))) - (is (instance? mount.core.NotStartedState (dval q-listener))) - (mount/start) - (mount/suspend) - (is (= (dval q-listener) :q-suspended)) - (is (= (dval web-server) :w-suspended)) - (mount/stop)))))