tolitius / mount

managing Clojure and ClojureScript app state since (reset)
Eclipse Public License 1.0
1.22k stars 88 forks source link

A reset function which will reset all the states even if there are errors. #63

Open sdave2 opened 7 years ago

sdave2 commented 7 years ago

Situation I ran into:

This is interesting. I defined a var with a defstate but made a mistake the first time around.

;; not the same mistake but it will help elaborate what I’m trying to explain

(def conn1-atom (atom nil))
(defstate conn
  :start (reset! conn1-atom :started)
  :stop (reset! :started conn1-atom))  ;; there is a mistake in :stop.

(mount/start) ;; this works fine. (mount/stop) ;; produces the error.

ClassCastException clojure.lang.Keyword cannot be cast to clojure.lang.IAtom clojure.core/reset! (core.clj:2273)

;; trying to re-define conn but keep running into the same error.

(defstate conn
  :start (reset! conn1-atom :started)
  :stop (reset! conn1-atom :stopped))

A function to reset all the states/connections even if they produce error would be nice. This was mentioned on Slack's mount channel by @tolitius . "it might make sense to potentially have a some kind of (mount/reset) that could take params: i.e. (mount/reset :ignore-errors true). which, if well documented, could help in situations like this one."

dm3 commented 7 years ago

To summarize the discussion with @tolitius:

The only open question is how to deal with states which fail to stop cleanly:

tolitius commented 7 years ago

After thinking a bit more a separate API such as reset is unnecessary, a call to (mount/stop) should handle this use case.

Pushed changes to 0.1.12-SNAPSHOT. A call to (mount/stop) on failure to stop a state will report it and mark the state as :stopped. This would allow to:

(mount/stop) will return all states that were successfully stopped.

boot.user=> (defstate a :start 42 :stop (throw (RuntimeException. "BOOM")))
#'boot.user/a
boot.user=> (defstate b :start 1 :stop 2)
#'boot.user/b
boot.user=> (mount/start)
{:started ["#'boot.user/a" "#'boot.user/b"]}
boot.user=> a
42
boot.user=> b
1
boot.user=> (mount/stop)
#error {
 :cause "BOOM"
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "could not stop [#'boot.user/a] due to"
   ...}]}
{:stopped ["#'boot.user/b"]}

Since a state could not be stopped, it still points to its original value after the "stop" function was called:

boot.user=> a
42
boot.user=> b
#object[mount.core.NotStartedState 0x2156abe2 "'#'boot.user/b' is not started (to start all the states call mount/start)"]

However from mount's perspective a is :stopped and can be started / redefined again. This allows a manual "force stop" of a in REPL.

We can now redefine a's stop function to "fix" the exception:

boot.user=> (defstate a :start 42 :stop 34)
#'boot.user/a
boot.user=> (mount/start)
{:started ["#'boot.user/a" "#'boot.user/b"]}
boot.user=> a
42
boot.user=> b
1
boot.user=> (mount/stop)
{:stopped ["#'boot.user/b" "#'boot.user/a"]}
boot.user=> a
#object[mount.core.NotStartedState 0xa04c91c "'#'boot.user/a' is not started (to start all the states call mount/start)"]
boot.user=> b
#object[mount.core.NotStartedState 0x56b0c866 "'#'boot.user/b' is not started (to start all the states call mount/start)"]