diff --git a/README.md b/README.md index ddb5c04..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) @@ -72,15 +81,15 @@ mount is an alternative to the [component](https://github.com/stuartsierra/compo Creating state is easy: ```clojure -(defstate conn :start (create-conn)) +(defstate conn :start create-conn) ``` -where `(create-conn)` 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) +(defstate conn :start create-conn :stop (disconnect conn)) ``` @@ -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: @@ -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/dev/dev.clj b/dev/dev.clj index 76c58d1..9894e54 100644 --- a/dev/dev.clj +++ b/dev/dev.clj @@ -1,8 +1,6 @@ (ns dev "Tools for interactive development with the REPL. This file should - not be included in a production build of the application." - ;; (:use [cljs.repl :only [repl]] - ;; [cljs.repl.browser :only [repl-env]]) + not be included in a production build of the application." (:require [clojure.java.io :as io] [clojure.java.javadoc :refer [javadoc]] [clojure.pprint :refer [pprint]] @@ -11,8 +9,8 @@ [clojure.set :as set] [clojure.string :as str] [clojure.test :as test] - ;; [clojure.core.async :refer [>!! ! _[source](http://www.javacodegeeks.com/2015/09/clojure-web-development-state-of-the-art.html):_ - -> _I think all agreed that Component is the industry standard for managing lifecycle of Clojure applications. If you are a Java developer you may think of it as a Spring (DI) replacement – you declare dependencies between “components” which are resolved on “system” startup. So you just say “my component needs a repository/database pool” and component library “injects” it for you._ - -While this is a common understanding, the Component is far from being Spring, in a good sense: - -* its codebase is fairly small -* it aims to solve one thing and one thing only: manage application state via inversion of control - -The not so hidden benefit is REPL time reloadability that it brings to the table with `component/start` and `component/stop` - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Then why "mount"!?](#then-why-mount) -- [So what are the differences?](#so-what-are-the-differences) - - [Objects vs. Namespaces](#objects-vs-namespaces) - - [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](#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) - - [Swapping alternate implementations](#swapping-alternate-implementations) - - [Uberjar / Packaging](#uberjar--packaging) - - [Multiple separate systems](#multiple-separate-systems) - - [Visualizing dependency graph](#visualizing-dependency-graph) - - - -## Then why "mount"!? - -[mount](https://github.com/tolitius/mount) was created after using Component for several projects. - -While Component is an interesting way to manage state, it has its limitations that prevented us -from having the ultimate super power of Clojure: _fun working with it_. Plus several other disadvantages -that we wanted to "fix". - -## So what are the differences? - -### Objects vs. Namespaces - -One thing that feels a bit "unClojure" about Component is "Objects". Objects everywhere, and Objects for everything. -This is how Component "separates explicit dependencies" and "clears the bounaries". - -This is also how an Object Oriented language does it, which does not leave a lot of room for functions: -with Component most of the functions are _methods_ which is an important distinction. - -Mount relies on Clojure namespaces to clear the boundaries. No change from Clojure here: `defstate` in one namespace -can be easily `:require`d in another. - -### Start and Stop Order - -Component relies on a cool [dependency](https://github.com/stuartsierra/dependency) library to build -a graph of dependencies, and start/stop them via topological sort based on the dependencies in this graph. - -Since Mount relies on Clojure namespaces and `:require`/`:use`, the order of states -and their dependencies are revealed by the Clojure Compiler itself. Mount just records that order and replays -it back and forth on stop and start. - -### Component requires whole app buy in - -Component really only works if you build your entire app around its model: application is fully based on Components -where every Component is an Object. - -Mount does not require you to "buy anything at all", it is free :) Just create a `defstate` whenever/whereever -you need it and use it. - -This one was a big deal for all the projects we used Component with, "the whole app buy in" converts an "_open_" application -of Namespaces and Functions to a "_closed_" application of Objects and Methods. "open" and "close" -here are rather feelings, but it is way easier and more natural to - -* go to a namespace to see this function -than to -* go to a namespace, go to a component, go to another component that this function maybe using/referenced at via a component key, to get the full view of the function. - -Again this is mostly a personal preference: the code works in both cases. - -### Refactoring an existing application - -Since to get the most benefits of Component the approach is "all or nothing", to rewrite an existing application -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 - -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 across functions / states is exactly -the same with or without Mount: there are no extra mental steps. - -### Starting and stopping _parts_ of an application - -Component can't really start and stop parts of an application within the same "system". Other sub systems can be -created from scratch or by dissoc'ing / merging with existing systems, but it is usually not all -that flexible in terms of REPL sessions where lots of time is spent. - -Mount _can_ start and stop parts of an application via given states with their namespaces: - -```clojure -dev=> (mount/start #'app.config/app-config #'app.nyse/conn) - -11:35:06.753 [nREPL-worker-1] INFO mount - >> starting.. app-config -11:35:06.756 [nREPL-worker-1] INFO mount - >> starting.. conn -:started -dev=> -``` - -Here is more [documentation](../README.md#start-and-stop-parts-of-application) on how to start/stop parts of an app. - -### Boilerplate code - -Component does not require a whole lot of "extra" code but: - -* a system with dependencies -* components as records -* with optional constructors -* and a Lifecycle/start Lifecycle/stop implementations -* destructuring component maps - -Depending on the number of application components the "extra" size may vary. - -Mount is pretty much: - -```clojure -(defstate name :start (fn) - :stop (fn)) -``` - -no "ceremony". - -## What Component does better - -### Swapping alternate implementations - -This is someting that is very useful for testing and is very easy to do in Component by simply assoc'ing onto a map. - -Mount can do it to: https://github.com/tolitius/mount#swapping-alternate-implementations - -The reason it is in "Component does it better" section is because, while result is the same, merging maps is a bit simpler than: - -```clojure -(mount/start-with {#'app.nyse/db #'app.test/test-db - #'app.nyse/publisher #'app.test/test-publisher}) -``` - -### Uberjar / Packaging - -Since Component fully controls the `system` where the whole application lives, it is quite simple -to start an application from anywhere including a `-main` function of the uberjar. - -In order to start the whole system in development, Mount just needs `(mount/start)` or `(reset)` -it's [simple](https://github.com/tolitius/mount#the-importance-of-being-reloadable). - -However there is no "tools.namespaces"/REPL at a "stand alone jar runtime" and in order for Mount to start / stop -the app, states need to be `:require`/`:use`d, which is usually done within the same namespace as `-main`. - -Depending on app dependencies, it could only require a few states to be `:require`/`:use`d, others -will be brought transitively. Here is an [example](uberjar.md#creating-reloadable-uberjarable-app) of building a wepapp uberjar with Mount. - -On the flip side, Component _system_ usually requires lots of `:require`s as well, since in order to be built, it needs to "see" all the top level states. - -###### _conclusion: it's simple in Mount as well, but requires an additional step._ - -### Multiple separate systems - -With Component multiple separate systems can be started _in the same Clojure runtime_ with different settings. Which is very useful for testing. - -Mount keeps states in namespaces, hence the app becomes "[The One](https://en.wikipedia.org/wiki/Neo_(The_Matrix))", and there can't be "multiples The Ones". - -Testing is not alien to Mount and it knows how to do a thing or two: - -* [starting / stopping parts of an application](https://github.com/tolitius/mount/blob/master/doc/differences-from-component.md#starting-and-stopping-parts-of-an-application) -* [start an application without certain states](https://github.com/tolitius/mount#start-an-application-without-certain-states) -* and [swapping alternate implementations](https://github.com/tolitius/mount#swapping-alternate-implementations) - -But running two apps in the same JVM side by side with "same but different" states, is not something Mount can do at the moment. - -###### _conclusion: needs more thinking._ - -### Visualizing dependency graph - -Component keeps an actual graph which can be visualized with great libraries like [loom](https://github.com/aysylu/loom). -Having this visualization is really helpful, especially during code discusions between multiple developers. - -Mount does not have this at the moment. It does have all the data to create such a visualization, perhaps even -by building a graph out of the data it has just for this purpose. diff --git a/doc/img/get-uberjar.png b/doc/img/get-uberjar.png deleted file mode 100644 index 97b1dd9..0000000 Binary files a/doc/img/get-uberjar.png and /dev/null differ diff --git a/doc/img/post-uberjar.png b/doc/img/post-uberjar.png deleted file mode 100644 index 24f215b..0000000 Binary files a/doc/img/post-uberjar.png and /dev/null differ diff --git a/doc/img/welcome-uberjar.png b/doc/img/welcome-uberjar.png deleted file mode 100644 index ef112aa..0000000 Binary files a/doc/img/welcome-uberjar.png and /dev/null differ diff --git a/doc/intro.md b/doc/intro.md deleted file mode 100644 index f1e987b..0000000 --- a/doc/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Introduction to statuo - -TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) diff --git a/doc/runtime-arguments.md b/doc/runtime-arguments.md deleted file mode 100644 index d226cbb..0000000 --- a/doc/runtime-arguments.md +++ /dev/null @@ -1,130 +0,0 @@ -## Passing Runtime Arguments - -This example lives in the `with-args` branch. If you'd like to follow along: - -```bash -$ git checkout with-args -Switched to branch 'with-args' -``` - -## Start with args - -In order to pass runtime arguments, these could be `-this x -that y` params or `-Dparam=` or -just a path to an external configuration file, `mount` has a special `start-with-args` function: - -```clojure -(defn -main [& args] - (mount/start-with-args args)) -``` - -Most of the time it is better to parse args before they "get in", so usually accepting args would look something like: - -```clojure -(defn -main [& args] - (mount/start-with-args - (parse-args args))) -``` - -where the `parse-args` is an app specific function. - -### Reading arguments - -Once the arguments are passed to the app, they are available via: - -```clojure -(mount/args) -``` - -Which, unless the arguments were parsed or modified in the `-main` function, -will return the original `args` that were passed to `-main`. - -### "Reading" example - -Here is an [example app](https://github.com/tolitius/mount/blob/with-args/test/app/app.clj) that takes `-main` arguments -and parses them with [tools.cli](https://github.com/clojure/tools.cli): - -```clojure -;; "any" regular function to pass arguments -(defn parse-args [args] - (let [opts [["-d" "--datomic-uri [datomic url]" "Datomic URL" - :default "datomic:mem://mount"] - ["-h" "--help"]]] - (-> (parse-opts args opts) - :options))) - -;; example of an app entry point with arguments -(defn -main [& args] - (mount/start-with-args - (parse-args args))) -``` - -For the example sake the app reads arguments in two places: - -* [inside](https://github.com/tolitius/mount/blob/with-args/test/app/nyse.clj#L17) a `defstate` - -```clojure -(defstate conn :start (new-connection (mount/args)) - :stop (disconnect (mount/args) conn)) -``` - -* and from "any" [other place](https://github.com/tolitius/mount/blob/with-args/test/app/config.clj#L8) within a function: - -```clojure -(defn load-config [path] - ;; ... - (if (:help (mount/args)) - (info "\n\nthis is a sample mount app to demo how to pass and read runtime arguments\n")) - ;; ...) -``` - -### "Uber" example - -In order to demo all of the above, we'll build an uberjar: - -```bash -$ lein do clean, uberjar -... -Created .. mount/target/mount-0.2.0-SNAPSHOT-standalone.jar -``` - -Since we have a default for a Datomic URI, it'll work with no arguments: - -```bash -$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar - -22:12:03.290 [main] INFO mount - >> starting.. app-config -22:12:03.293 [main] INFO mount - >> starting.. conn -22:12:03.293 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount -22:12:03.444 [main] INFO mount - >> starting.. nrepl -``` - -Now let's ask it to help us: - -```bash -$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar --help - -22:13:48.798 [main] INFO mount - >> starting.. app-config -22:13:48.799 [main] INFO app.config - - -this is a sample mount app to demo how to pass and read runtime arguments - -22:13:48.801 [main] INFO mount - >> starting.. conn -22:13:48.801 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://mount -22:13:48.946 [main] INFO mount - >> starting.. nrepl -``` - -And finally let's connect to the Single Malt Database. It's Friday.. - -```bash -$ java -jar target/mount-0.2.0-SNAPSHOT-standalone.jar -d datomic:mem://single-malt-database - -22:16:10.733 [main] INFO mount - >> starting.. app-config -22:16:10.737 [main] INFO mount - >> starting.. conn -22:16:10.737 [main] INFO app.nyse - creating a connection to datomic: datomic:mem://single-malt-database -22:16:10.885 [main] INFO mount - >> starting.. nrepl -``` - -### Other usecases - -Depending the requirements, these runtime arguments could take different shapes of forms. You would have a full control -over what is passed to the app, the same way you have it without mount through `-main [& args]`. diff --git a/doc/uberjar.md b/doc/uberjar.md deleted file mode 100644 index 1300840..0000000 --- a/doc/uberjar.md +++ /dev/null @@ -1,163 +0,0 @@ -## Creating Reloadable Uberjar'able App - -This example lives in the `uberjar` branch. If you'd like to follow along: - -```bash -$ git checkout uberjar -Switched to branch 'uberjar' -``` - -### App state - -Here is an example [app](https://github.com/tolitius/mount/tree/uberjar/test/app) that has these states: - -```clojure -16:20:44.997 [nREPL-worker-0] INFO mount - >> starting.. app-config -16:20:44.998 [nREPL-worker-0] INFO mount - >> starting.. conn -16:20:45.393 [nREPL-worker-0] INFO mount - >> starting.. nyse-app -16:20:45.443 [nREPL-worker-0] INFO mount - >> starting.. nrepl -``` - -where `nyse-app` is _the_ app. It has the usual routes: - -```clojure -(defroutes mount-example-routes - - (GET "/" [] "welcome to mount sample app!") - (GET "/nyse/orders/:ticker" [ticker] - (generate-string (find-orders ticker))) - - (POST "/nyse/orders" [ticker qty bid offer] - (add-order ticker (bigdec bid) (bigdec offer) (Integer/parseInt qty)) - (generate-string {:added {:ticker ticker - :qty qty - :bid bid - :offer offer}}))) -``` - -and the reloadable state: - -```clojure -(defn start-nyse [] - (create-nyse-schema) ;; creating schema (usually done long before the app is started..) - (-> (routes mount-example-routes) - (handler/site) - (run-jetty {:join? false - :port (get-in app-config [:www :port])}))) - -(defstate nyse-app :start (start-nyse) - :stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point -``` - -In order not to block, and being reloadable, the Jetty server is started in the "`:join? false`" mode which starts the server, -and just returns a reference to it, so it can be easily stopped by `(.stop server)` - -### "Uberjar is the :main" - -In order for a standalone jar to run, it needs an entry point. This sample app [has one](https://github.com/tolitius/mount/blob/uberjar/test/app/app.clj#L16): - -```clojure -;; example of an app entry point -(defn -main [& args] - (mount/start)) -``` - -And some usual suspects from `project.clj`: - -```clojure -;; "test" is in sources here to just "demo" the uberjar without poluting mount "src" -:uberjar {:source-paths ["test/app"] - :dependencies [[compojure "1.4.0"] - [ring/ring-jetty-adapter "1.1.0"] - [cheshire "5.5.0"] - [org.clojure/tools.nrepl "0.2.11"] - [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]] - :main app - :aot :all}} -``` - -### REPL time - -```clojure -$ lein do clean, repl - -user=> (dev)(reset) -16:20:44.997 [nREPL-worker-0] INFO mount - >> starting.. app-config -16:20:44.998 [nREPL-worker-0] INFO mount - >> starting.. conn -16:20:45.393 [nREPL-worker-0] INFO mount - >> starting.. nyse-app - -16:20:45.442 [nREPL-worker-0] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:53600 - -16:20:45.443 [nREPL-worker-0] INFO mount - >> starting.. nrepl -:ready -dev=> -``` - -Jetty server is started and ready to roll. And everything is still reloadable: - -```clojure -dev=> (reset) -16:44:16.625 [nREPL-worker-2] INFO mount - << stopping.. nrepl -16:44:16.626 [nREPL-worker-2] INFO mount - << stopping.. nyse-app -16:44:16.711 [nREPL-worker-2] INFO mount - << stopping.. conn -16:44:16.713 [nREPL-worker-2] INFO mount - << stopping.. app-config - -16:44:16.747 [nREPL-worker-2] INFO mount - >> starting.. app-config -16:44:16.748 [nREPL-worker-2] INFO mount - >> starting.. conn -16:44:16.773 [nREPL-worker-2] INFO mount - >> starting.. nyse-app - -16:44:16.777 [nREPL-worker-2] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:54476 - -16:44:16.778 [nREPL-worker-2] INFO mount - >> starting.. nrepl -``` - -Notice the Jetty port difference between reloads: `53600` vs. `54476`. This is done on purpose via [config](https://github.com/tolitius/mount/blob/uberjar/test/resources/config.edn#L4): - -```clojure -:www {:port 0} ;; Jetty will randomly assign the available port (this is good for dev reloadability) -``` - -This of course can be solidified for different env deployments. For example I like `4242` :) - -### Packaging one super uber jar - -```clojure -$ lein do clean, uberjar -... -Created /Users/tolitius/1/fun/mount/target/mount-0.1.0-SNAPSHOT-standalone.jar ;; your version may vary -``` - -Let's give it a spin: - -```bash -$ java -jar target/mount-0.1.0-SNAPSHOT-standalone.jar -... -16:51:35.586 [main] DEBUG o.e.j.u.component.AbstractLifeCycle - STARTED SelectChannelConnector@0.0.0.0:54728 -``` - -Up and running on port `:54728`: - - - -See if we have any orders: - - - -we don't. let's put something into Datomic: - -```clojure -$ curl -X POST -d "ticker=GOOG&qty=100&bid=665.51&offer=665.59" "http://localhost:54728/nyse/orders" -{"added":{"ticker":"GOOG","qty":"100","bid":"665.51","offer":"665.59"}} -``` - -now we should: - - - -### Choices - -There are multiple ways to start a web app. This above is the most straighforward one: start server / stop server. - -But depending on the requirements / architecture, the app can also have an entry point to `(mount/start)` -via something like [:ring :init](https://github.com/weavejester/lein-ring#general-options)). Or the `(mount/start)` -can go into the handler function, etc. diff --git a/project.clj b/project.clj index 857fbd7..c47068f 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject mount "0.1.4-SNAPSHOT" +(defproject mount "0.1.5-SNAPSHOT" :description "managing Clojure app state since (reset)" :url "https://github.com/tolitius/mount" :license {:name "Eclipse Public License" @@ -6,26 +6,22 @@ :source-paths ["src"] + ;; these dependencies are here for uberjar and dev example :dependencies [[org.clojure/clojure "1.7.0"] - [ch.qos.logback/logback-classic "1.1.3"] [org.clojure/tools.logging "0.3.1"] - [org.clojure/tools.macro "0.1.2"] - [org.clojure/tools.namespace "0.2.11"]] + [ch.qos.logback/logback-classic "1.1.3"] + [compojure "1.4.0"] + [ring/ring-jetty-adapter "1.1.0"] + [cheshire "5.5.0"] + [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]] + [robert/hooke "1.3.0"] + [org.clojure/tools.nrepl "0.2.11"] + [org.clojure/tools.macro "0.1.2"]] :profiles {:dev {:source-paths ["dev" "test/app"] - :dependencies [[yesql "0.5.1"] - [compojure "1.4.0"] - [ring/ring-jetty-adapter "1.1.0"] - [cheshire "5.5.0"] - [org.clojure/tools.nrepl "0.2.11"] - [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]} + :dependencies [[org.clojure/tools.namespace "0.2.11"]]} ;; "test" is in sources here to just "demo" the uberjar without poluting mount "src" :uberjar {:source-paths ["test/app"] - :dependencies [[compojure "1.4.0"] - [ring/ring-jetty-adapter "1.1.0"] - [cheshire "5.5.0"] - [org.clojure/tools.nrepl "0.2.11"] - [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]] :main app :aot :all}}) diff --git a/src/mount/core.clj b/src/mount/core.clj index c03be64..6ca57c4 100644 --- a/src/mount/core.clj +++ b/src/mount/core.clj @@ -1,15 +1,14 @@ (ns mount.core - (:require [clojure.tools.macro :as macro] - [clojure.tools.namespace.repl :refer [disable-reload!]] - [clojure.tools.logging :refer [info warn debug error]])) + (:require [clojure.tools.macro :as macro])) -(disable-reload!) - -;; (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 (defn- make-state-seq [state] (or (@state-order state) @@ -30,63 +29,87 @@ (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) - :start `(fn [] (~@start)) - :started? false} - stop (assoc :stop `(fn [] (~@stop))) - suspend (assoc :suspend `(fn [] (~@suspend))) - resume (assoc :resume `(fn [] (~@resume))))] + :order (make-state-seq (with-ns *ns* state)) + :start `(fn [] ~start) + :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)))))) -(defn- up [var {:keys [ns name start started? resume suspended?]}] - (when-not started? - (let [s (try (if suspended? - (do (info ">> resuming.. " name) - (resume)) - (do (info ">> starting.. " name) - (start))) +(defn- record! [{:keys [ns name]} f done] + (let [state (f)] + (swap! done conj (ns-resolve ns name)) + state)) + +(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?]}] - (when (or started? suspended?) - (info "<< stopping.. " name) +(defn- down [var {:keys [ns name stop status] :as state} done] + (when (some status #{:started :suspended}) (when stop (try - (stop) + (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]}] - (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) +(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?]}] +(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? - (info ">> resuming.. " name) - (let [s (try (resume) + (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) @@ -111,35 +134,37 @@ (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)))) (defn- bring [states fun order] - (->> states - (sort-by (comp :order meta) order) - (map #(fun % (meta %))) - doall)) + (let [done (atom [])] + (->> states + (sort-by (comp :order meta) order) + (map #(fun % (meta %) done)) + dorun) + @done)) -(defn merge-lifecycles +(defn- merge-lifecycles "merges with overriding _certain_ non existing keys. i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil 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] +(defn- rollback! [state] (let [{:keys [origin]} (meta state)] (when origin (alter-meta! state #(merge-lifecycles % origin))))) -(defn substitute! [state with] - (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :suspended?]) +(defn- substitute! [state with] + (let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status]) origin (meta state) sub (meta with)] (alter-meta! with assoc :sub? true) @@ -147,31 +172,26 @@ (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))) (defn start [& states] (let [states (or (seq states) (all-without-subs))] - (bring states up <) - :started)) + {:started (bring states up <)})) (defn stop [& states] - (let [states (or states (find-all-states))] - (doall (map unsub states)) ;; unmark substitutions marked by "start-with" - (bring states down >) - (doall (map rollback! states)) ;; restore to origin from "start-with" - :stopped)) + (let [states (or states (find-all-states)) + _ (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})) (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)) + (apply stop states))) (defn start-with-args [xs & states] (reset! -args xs) @@ -180,9 +200,8 @@ (start))) (defn start-with [with] - (doall - (for [[from to] with] - (substitute! from to))) + (doseq [[from to] with] + (substitute! from to)) (start)) (defn start-without [& states] @@ -194,10 +213,8 @@ (defn suspend [& states] (let [states (or (seq states) (all-without-subs))] - (bring states sigstop <) - :suspended)) + {:suspended (bring states sigstop <)})) (defn resume [& states] (let [states (or (seq states) (all-without-subs))] - (bring states sigcont <) - :resumed)) + {:resumed (bring states sigcont <)})) diff --git a/test/app/utils/logging.clj b/test/app/utils/logging.clj new file mode 100644 index 0000000..dc0216c --- /dev/null +++ b/test/app/utils/logging.clj @@ -0,0 +1,46 @@ +(ns app.utils.logging ;; << change to your namespace/path + (:require [mount.core] + [robert.hooke :refer [add-hook clear-hooks]] + [clojure.string :refer [split]] + [clojure.tools.logging :refer [info]])) + +(alter-meta! *ns* assoc ::load false) + +(defn- f-to-action [f] + (let [fname (-> (str f) + (split #"@") + first)] + (case fname + "mount.core$up" :up + "mount.core$down" :down + "mount.core$sigstop" :suspend + "mount.core$sigcont" :resume + :noop))) + +(defn whatcha-doing? [{:keys [status suspend]} action] + (case action + :up (if (status :suspended) ">> resuming" + (if-not (status :started) ">> starting")) + :down (if (or (status :started) (status :suspended)) "<< stopping") + :suspend (if (and (status :started) suspend) "<< suspending") + :resume (if (status :suspended) ">> resuming"))) + +(defn log-status [f & args] + (let [{:keys [ns name] :as state} (second args) + action (f-to-action f)] + (when-let [taking-over-the-world (whatcha-doing? state action)] + (info (str taking-over-the-world ".. " (ns-resolve ns name)))) + (apply f args))) + +(defonce lifecycle-fns + #{#'mount.core/up + #'mount.core/down + #'mount.core/sigstop + #'mount.core/sigcont}) + +(defn without-logging-status [] + (doall (map #(clear-hooks %) lifecycle-fns))) + +(defn with-logging-status [] + (without-logging-status) + (doall (map #(add-hook % log-status) lifecycle-fns)))