diff --git a/README.md b/README.md index 6fc62d5..2989373 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/mount/core.clj b/src/mount/core.clj index 3210d91..19125be 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) + :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) diff --git a/test/app/app.clj b/test/app/app.clj index 2c565b6..a240f68 100644 --- a/test/app/app.clj +++ b/test/app/app.clj @@ -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] diff --git a/test/app/config.clj b/test/app/config.clj index ed5a895..916a5a4 100644 --- a/test/app/config.clj +++ b/test/app/config.clj @@ -10,4 +10,4 @@ edn/read-string)) (defstate app-config - :start #(load-config "test/resources/config.edn")) + :start (load-config "test/resources/config.edn")) diff --git a/test/app/db.clj b/test/app/db.clj index 0b12404..9dbafe8 100644 --- a/test/app/db.clj +++ b/test/app/db.clj @@ -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] diff --git a/test/app/www.clj b/test/app/www.clj index 8150e2f..d8f6f06 100644 --- a/test/app/www.clj +++ b/test/app/www.clj @@ -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