updating to unpounded lifecycle fns

This commit is contained in:
anatoly 2015-11-30 16:02:34 -05:00
parent 18e4e229ce
commit 7624b9c7d5
6 changed files with 228 additions and 43 deletions

175
README.md
View file

@ -7,6 +7,7 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
module | branch | status module | branch | status
----------|----------|---------- ----------|----------|----------
mount | `master` | [![Circle CI](https://circleci.com/gh/tolitius/mount/tree/master.png?style=svg)](https://circleci.com/gh/tolitius/mount/tree/master) mount | `master` | [![Circle CI](https://circleci.com/gh/tolitius/mount/tree/master.png?style=svg)](https://circleci.com/gh/tolitius/mount/tree/master)
mount | `0.1.5` | [![Circle CI](https://circleci.com/gh/tolitius/mount/tree/0.1.5.png?style=svg)](https://circleci.com/gh/tolitius/mount/tree/0.1.5)
[![Clojars Project](http://clojars.org/mount/latest-version.svg)](http://clojars.org/mount) [![Clojars Project](http://clojars.org/mount/latest-version.svg)](http://clojars.org/mount)
@ -18,6 +19,7 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Differences from Component](#differences-from-component) - [Differences from Component](#differences-from-component)
- [How](#how) - [How](#how)
- [Creating State](#creating-state) - [Creating State](#creating-state)
- [Value of Values](#value-of-values)
- [Using State](#using-state) - [Using State](#using-state)
- [Dependencies](#dependencies) - [Dependencies](#dependencies)
- [Talking States](#talking-states) - [Talking States](#talking-states)
@ -25,7 +27,14 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Start and Stop Order](#start-and-stop-order) - [Start and Stop Order](#start-and-stop-order)
- [Start and Stop Parts of Application](#start-and-stop-parts-of-application) - [Start and Stop Parts of Application](#start-and-stop-parts-of-application)
- [Start an Application Without Certain States](#start-an-application-without-certain-states) - [Start an Application Without Certain States](#start-an-application-without-certain-states)
- [Stop an Application Except Certain States](#stop-an-application-except-certain-states)
- [Swapping Alternate Implementations](#swapping-alternate-implementations) - [Swapping Alternate Implementations](#swapping-alternate-implementations)
- [Suspending and Resuming](#suspending-and-resuming)
- [Suspendable Lifecycle](#suspendable-lifecycle)
- [Plugging into (reset)](#plugging-into-reset)
- [Suspendable Example Application](#suspendable-example-application)
- [Affected States](#affected-states)
- [Logging](#logging)
- [Mount and Develop!](#mount-and-develop) - [Mount and Develop!](#mount-and-develop)
- [Running New York Stock Exchange](#running-new-york-stock-exchange) - [Running New York Stock Exchange](#running-new-york-stock-exchange)
- [Web and Uberjar](#web-and-uberjar) - [Web and Uberjar](#web-and-uberjar)
@ -75,13 +84,13 @@ Creating state is easy:
(defstate conn :start create-conn) (defstate conn :start create-conn)
``` ```
where `create-conn` function 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` In case this state needs to be cleaned / destryed between reloads, there is also `:stop`
```clojure ```clojure
(defstate conn :start create-conn (defstate conn :start create-conn
:stop #(disconnect 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 That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply
@ -94,6 +103,14 @@ dev=> conn
#object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"] #object[datomic.peer.LocalConnection 0x1661a4eb "datomic.peer.LocalConnection@1661a4eb"]
``` ```
#### Value of values
Lifecycle functions start/stop/suspend/resume can take both functions and values. This is "valuable" and also works:
```clojure
(mount/defstate answer-to-the-ultimate-question-of-life-the-universe-and-everything :start 42)
```
### Using State ### Using State
For example let's say an `app` needs a connection above. No problem: For example let's say an `app` needs a connection above. No problem:
@ -129,7 +146,7 @@ There are of course direct dependecies that `mount` respects:
(:require [mount.core :refer [defstate]])) (:require [mount.core :refer [defstate]]))
(defstate app-config (defstate app-config
:start #(load-config "test/resources/config.edn")) :start (load-config "test/resources/config.edn"))
``` ```
this `app-config`, being top level, can be used in other namespaces, including the ones that create states: this `app-config`, being top level, can be used in other namespaces, including the ones that create states:
@ -139,7 +156,7 @@ this `app-config`, being top level, can be used in other namespaces, including t
(:require [mount.core :refer [defstate]] (:require [mount.core :refer [defstate]]
[app.config :refer [app-config]])) [app.config :refer [app-config]]))
(defstate conn :start #(create-connection app-config)) (defstate conn :start (create-connection app-config))
``` ```
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj) [here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)
@ -248,10 +265,157 @@ One thing to note, whenever
(mount/stop) (mount/stop)
``` ```
is run after `start-with`, it rolls back to an original "state of states", i.e. `#'app.nyse/db` is `#'app.nyse/db` again. So a subsequent calls to `(mount/start)` or even to `(mount/start-with {something else})` will start from a clean slate. is run after `start-with`, it rolls back to an original "state of states", i.e. `#'app.nyse/db` is `#'app.nyse/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/check/start_with_test.clj) test that starts an app with mocking Datomic connection and nREPL. Here is an [example](test/check/start_with_test.clj) 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`:
```clojure
dev=> (mount/start)
14:34:10.813 [nREPL-worker-0] INFO mount.core - >> starting.. app-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.. app-config
:stopped
dev=>
dev=> (mount/start)
14:34:58.673 [nREPL-worker-0] INFO mount.core - >> starting.. app-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.
## 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 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)
```
`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.. app-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.. app-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'
```
## Affected States
Every time a lifecycle function (start/stop/suspend/resume) is called mount will return all the states that were affected:
```clojure
dev=> (mount/start)
{:started [#'app.config/app-config
#'app.nyse/conn
#'app/nrepl
#'check.suspend-resume-test/web-server
#'check.suspend-resume-test/q-listener]}
```
```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]}
```
An interesting bit here is a vector vs. a set: all the states are returned _in the order they were changed_.
## 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](https://github.com/tolitius/mount/issues/15) 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.
The way this is done is via an excellent [robert hooke](https://github.com/technomancy/robert-hooke/). Example applications live in `test`, so does the [utility](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/test/app/utils/logging.clj#L44) that adds logging to all the mount's lifecycle functions on start in [dev.clj](https://github.com/tolitius/mount/blob/75d7cdc610ce38623d4d3aea1da3170d1c9a3b4b/dev/dev.clj#L21).
## Mount and Develop! ## Mount and Develop!
`mount` comes with an example [app](https://github.com/tolitius/mount/tree/master/test/app) `mount` comes with an example [app](https://github.com/tolitius/mount/tree/master/test/app)
@ -366,6 +530,7 @@ Switched to branch 'with-args'
``` ```
The documentation is [here](doc/runtime-arguments.md#passing-runtime-arguments). The documentation is [here](doc/runtime-arguments.md#passing-runtime-arguments).
## License ## License
Copyright © 2015 tolitius Copyright © 2015 tolitius

View file

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

View file

@ -10,8 +10,8 @@
(start-server :bind host :port port)) (start-server :bind host :port port))
;; nREPL is just another simple state ;; nREPL is just another simple state
(defstate nrepl :start #(start-nrepl (:nrepl app-config)) (defstate nrepl :start (start-nrepl (:nrepl app-config))
:stop #(stop-server nrepl)) :stop (stop-server nrepl))
;; example of an app entry point ;; example of an app entry point
(defn -main [& args] (defn -main [& args]

View file

@ -10,4 +10,4 @@
edn/read-string)) edn/read-string))
(defstate app-config (defstate app-config
:start #(load-config "test/resources/config.edn")) :start (load-config "test/resources/config.edn"))

View file

@ -17,8 +17,8 @@
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) (.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
(d/delete-database uri))) (d/delete-database uri)))
(defstate conn :start #(new-connection app-config) (defstate conn :start (new-connection app-config)
:stop #(disconnect app-config conn)) :stop (disconnect app-config conn))
;; datomic schema (staging as an example) ;; datomic schema (staging as an example)
(defn create-schema [conn] (defn create-schema [conn]

View file

@ -27,5 +27,5 @@
(run-jetty {:join? false (run-jetty {:join? false
:port (:port www)}))) :port (:port www)})))
(defstate nyse-app :start #(start-nyse app-config) (defstate nyse-app :start (start-nyse app-config)
:stop #(.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point :stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point