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
----------|----------|----------
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)
@ -18,6 +19,7 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Differences from Component](#differences-from-component)
- [How](#how)
- [Creating State](#creating-state)
- [Value of Values](#value-of-values)
- [Using State](#using-state)
- [Dependencies](#dependencies)
- [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 Parts of Application](#start-and-stop-parts-of-application)
- [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)
- [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)
- [Running New York Stock Exchange](#running-new-york-stock-exchange)
- [Web and Uberjar](#web-and-uberjar)
@ -75,13 +84,13 @@ Creating state is easy:
(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`
```clojure
(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
@ -94,6 +103,14 @@ dev=> conn
#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
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]]))
(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:
@ -139,7 +156,7 @@ this `app-config`, being top level, can be used in other namespaces, including t
(:require [mount.core :refer [defstate]]
[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)
@ -248,10 +265,157 @@ One thing to note, whenever
(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.
## 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` 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).
## License
Copyright © 2015 tolitius

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)
:order (make-state-seq (with-ns *ns* state))
:start `(fn [] ~start)
:started? false}
stop (assoc :stop `(fn [] (~stop)))
suspend (assoc :suspend `(fn [] (~suspend)))
resume (assoc :resume `(fn [] (~resume))))]
: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

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

View file

@ -10,4 +10,4 @@
edn/read-string))
(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)
(d/delete-database uri)))
(defstate conn :start #(new-connection app-config)
:stop #(disconnect app-config conn))
(defstate conn :start (new-connection app-config)
:stop (disconnect app-config conn))
;; datomic schema (staging as an example)
(defn create-schema [conn]

View file

@ -27,5 +27,5 @@
(run-jetty {:join? false
:port (:port www)})))
(defstate nyse-app :start #(start-nyse app-config)
: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