From e381ef74857c27fc6084a69f61b60311c40d8f5a Mon Sep 17 00:00:00 2001 From: anatoly Date: Thu, 19 Nov 2015 00:19:08 -0500 Subject: [PATCH 1/4] adding suspend/resume for #5 docs and tests will follow --- src/mount/core.clj | 79 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/src/mount/core.clj b/src/mount/core.clj index d81bd86..c4e9297 100644 --- a/src/mount/core.clj +++ b/src/mount/core.clj @@ -22,42 +22,70 @@ (toString [this] (str "'" state "' is not started (to start all the states call mount/start)"))) -;;TODO validate stop and the fact that start and stop are fns -(defn- validate [{:keys [start stop]}] +;;TODO validate the whole lifecycle +(defn- validate [{:keys [start stop suspend resume] :as lifecycle}] (when-not start (throw (IllegalArgumentException. "can't start a stateful thing without a start function. (i.e. missing :start fn)"))) - {:start start :stop stop}) + (when (and suspend (not resume)) + (throw (IllegalArgumentException. "suspendable state should have a resume function (i.e. missing :resume fn)")))) (defmacro defstate [state & body] - (let [[state [c cf d df]] (macro/name-with-attributes state body) - {:keys [start stop]} (validate {c cf d df})] + (let [[state params] (macro/name-with-attributes state body) + {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)] + (validate lifecycle) (let [s-meta (-> {:mount-state mount-state :order (make-state-seq state) :start `(fn [] (~@start)) :started? false} - (cond-> df (assoc :stop `(fn [] (~@stop)))))] + (cond-> stop (assoc :stop `(fn [] (~@stop)))) + (cond-> suspend (assoc :suspend `(fn [] (~@suspend)))) + (cond-> resume (assoc :resume `(fn [] (~@resume)))))] `(defonce ~(with-meta state (merge (meta state) s-meta)) (NotStartedState. ~(str state)))))) -(defn- up [var {:keys [ns name start started?]}] +(defn- up [var {:keys [ns name start started? resume suspended?]}] (when-not started? - (info ">> starting.. " name) - (let [s (try (start) - (catch Throwable t + (let [s (try (if suspended? + (do (info ">> resuming.. " name) + (resume)) + (do (info ">> starting.. " name) + (start))) + (catch Throwable t (throw (RuntimeException. (str "could not start [" name "] due to") t))))] (intern ns (symbol name) s) - (alter-meta! var assoc :started? true)))) + (alter-meta! var assoc :started? true :suspended? false)))) -(defn- down [var {:keys [ns name stop started?]}] - (when started? +(defn- down [var {:keys [ns name stop started? suspended?]}] + (when (or started? suspended?) (info "<< stopping.. " name) (when stop (try (stop) - (catch Throwable t + (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))) + (alter-meta! var assoc :started? false :suspended? false))) + +(defn- sigstop [var {:keys [ns name started? suspend resume]}] + (when (and started? resume) ;; can't have suspend without resume, but the reverse is possible + (info ">> suspending.. " name) + (when suspend ;; don't suspend if there is only resume function (just mark it :suspended?) + (let [s (try (suspend) + (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))) + +(defn- sigcont [var {:keys [ns name start started? resume suspended?]}] + (when (instance? NotStartedState var) + (throw (RuntimeException. (str "could not resume [" name "] since it is stoppped (i.e. not suspended)")))) + (when suspended? + (info ">> resuming.. " name) + (let [s (try (resume) + (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)))) ;;TODO args might need more thinking (defn args [] @-args) @@ -72,7 +100,6 @@ (map second) (filter mount-state?))) - ;;TODO ns based for now. need to be _state_ based (defn- add-deps [{:keys [ns] :as state} all] (let [refers (ns-refers ns) @@ -83,7 +110,7 @@ (defn states-with-deps [] (let [all (find-all-states)] (->> (map (comp #(add-deps % all) - #(select-keys % [:name :order :ns]) + #(select-keys % [:name :order :ns :started? :suspended?]) meta) all) (sort-by :order)))) @@ -127,6 +154,14 @@ (doall (map rollback! states)) ;; restore to origin from "start-with" :stopped)) +(defn stop-except [& states] + (let [all (set (find-all-states)) + states (remove (set states) all)] + (doall (map unsub states)) ;; unmark substitutions marked by "start-with" + (bring states down >) + (doall (map rollback! states)) ;; restore to origin from "start-with" + :stopped)) + (defn start-with-args [xs & states] (reset! -args xs) (if (first states) @@ -146,3 +181,13 @@ without (remove (set states) app)] (apply start without)) (start))) + +(defn suspend [& states] + (let [states (or (seq states) (find-all-states))] + (bring states sigstop <) + :suspended)) + +(defn resume [& states] + (let [states (or (seq states) (find-all-states))] + (bring states sigcont <) + :resumed)) From a05a73f82cb3e01297f138d33ccca48ed9411890 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 18 Nov 2015 02:10:24 -0500 Subject: [PATCH 2/4] [navigation]: namespaces + functions + components --- doc/differences-from-component.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/differences-from-component.md b/doc/differences-from-component.md index deb1376..fe09dc0 100644 --- a/doc/differences-from-component.md +++ b/doc/differences-from-component.md @@ -26,7 +26,7 @@ The not so hidden benefit is REPL time reloadability that it brings to the table - [Start and Stop Order](#start-and-stop-order) - [Component requires whole app buy in](#component-requires-whole-app-buy-in) - [Refactoring an existing application](#refactoring-an-existing-application) - - [Code navigation (vi, emacs, IDE..)](#code-navigation-vi-emacs-ide) + - [Code navigation](#code-navigation) - [Starting and stopping _parts_ of an application](#starting-and-stopping-_parts_-of-an-application) - [Boilerplate code](#boilerplate-code) - [What Component does better](#what-component-does-better) @@ -92,14 +92,12 @@ in Component, depending on the application size, is daunting at best. Mount allows adding `defstates` _incrementally_, the same way you would add functions to an application. -### Code navigation (vi, emacs, IDE..) +### Code navigation -Navigation between functions in Component can't really be done without Components themselves. Since in Component -a function usually references another function via a map lookup: `(:function component)`. This is not a big deal, but -it changes the way IDE / editors are used to navigate the code by adding that extra step. +Component changes the way the code is structured. Depending on the size of the code base, and how rich the dependency graph is, Component might add a good amount of cognitive load. To a simple navigation from namespace to namespace, from function to function, Components add, well.. "Components" that can't be ignored when [loading the codebase in one's head](http://paulgraham.com/head.html) -Since Mount relies on Clojure namespaces and `:require`/`:use`, the navigation accorss functions / states is exactly -the same with or without Mount: there are no extra click/mental steps. +Since Mount relies on Clojure namespaces (`:require`/`:use`), navigation accorss functions / states is exactly +the same with or without Mount: there are no extra mental steps. ### Starting and stopping _parts_ of an application From ae9c3438201e03e31c5d5df40e5a291eb11f4937 Mon Sep 17 00:00:00 2001 From: Malchevskiy Misha Date: Wed, 18 Nov 2015 18:20:52 +0300 Subject: [PATCH 3/4] Fix typo --- doc/differences-from-component.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/differences-from-component.md b/doc/differences-from-component.md index fe09dc0..de99577 100644 --- a/doc/differences-from-component.md +++ b/doc/differences-from-component.md @@ -96,7 +96,7 @@ Mount allows adding `defstates` _incrementally_, the same way you would add func Component changes the way the code is structured. Depending on the size of the code base, and how rich the dependency graph is, Component might add a good amount of cognitive load. To a simple navigation from namespace to namespace, from function to function, Components add, well.. "Components" that can't be ignored when [loading the codebase in one's head](http://paulgraham.com/head.html) -Since Mount relies on Clojure namespaces (`:require`/`:use`), navigation accorss functions / states is exactly +Since Mount relies on Clojure namespaces (`:require`/`:use`), navigation across functions / states is exactly the same with or without Mount: there are no extra mental steps. ### Starting and stopping _parts_ of an application From 992f67397aad8c86d00460aa394f2de15e93387d Mon Sep 17 00:00:00 2001 From: anatoly Date: Thu, 19 Nov 2015 09:11:30 -0500 Subject: [PATCH 4/4] [#5]: tests for (mount/stop-except) --- src/mount/core.clj | 14 +++++++------- test/check/stop_except_test.clj | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 test/check/stop_except_test.clj diff --git a/src/mount/core.clj b/src/mount/core.clj index c4e9297..d2cf278 100644 --- a/src/mount/core.clj +++ b/src/mount/core.clj @@ -33,13 +33,13 @@ (let [[state params] (macro/name-with-attributes state body) {:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)] (validate lifecycle) - (let [s-meta (-> {:mount-state mount-state - :order (make-state-seq state) - :start `(fn [] (~@start)) - :started? false} - (cond-> stop (assoc :stop `(fn [] (~@stop)))) - (cond-> suspend (assoc :suspend `(fn [] (~@suspend)))) - (cond-> resume (assoc :resume `(fn [] (~@resume)))))] + (let [s-meta (cond-> {:mount-state mount-state + :order (make-state-seq state) + :start `(fn [] (~@start)) + :started? false} + stop (assoc :stop `(fn [] (~@stop))) + suspend (assoc :suspend `(fn [] (~@suspend))) + resume (assoc :resume `(fn [] (~@resume))))] `(defonce ~(with-meta state (merge (meta state) s-meta)) (NotStartedState. ~(str state)))))) diff --git a/test/check/stop_except_test.clj b/test/check/stop_except_test.clj new file mode 100644 index 0000000..b314c31 --- /dev/null +++ b/test/check/stop_except_test.clj @@ -0,0 +1,31 @@ +(ns check.stop-except-test + (:require [mount.core :as mount :refer [defstate]] + [app.config :refer [app-config]] + [app.nyse :refer [conn]] + [app :refer [nrepl]] + [clojure.test :refer :all])) + +(deftest stop-except + + (testing "should stop all except nrepl" + (let [_ (mount/start) + _ (mount/stop-except #'app.nyse/conn #'app.config/app-config)] + (is (map? app-config)) + (is (instance? datomic.peer.LocalConnection conn)) + (is (instance? mount.core.NotStartedState nrepl)) + (mount/stop))) + + (testing "should start normally after stop-except" + (let [_ (mount/start)] + (is (map? app-config)) + (is (instance? clojure.tools.nrepl.server.Server nrepl)) + (is (instance? datomic.peer.LocalConnection conn)) + (mount/stop))) + + (testing "should stop all normally after stop-except" + (let [_ (mount/start) + _ (mount/stop-except #'app.nyse/conn #'app.config/app-config) + _ (mount/stop)] + (is (instance? mount.core.NotStartedState app-config)) + (is (instance? mount.core.NotStartedState conn)) + (is (instance? mount.core.NotStartedState nrepl)))))