#46: removing :suspend and :resume... [done]

This commit is contained in:
anatoly 2016-02-03 23:20:54 -05:00
parent 4787ae12f5
commit dc91a44f72
7 changed files with 21 additions and 352 deletions

102
README.md
View file

@ -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

View file

@ -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)))

View file

@ -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).

View file

@ -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 <)}))

View file

@ -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)))

View file

@ -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 []

View file

@ -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)))))