diff --git a/README.md b/README.md index ddb5c04..6fc62d5 100644 --- a/README.md +++ b/README.md @@ -72,16 +72,16 @@ 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 `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)) +(defstate conn :start create-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 @@ -129,7 +129,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 +139,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) diff --git a/dev/dev.clj b/dev/dev.clj index 76c58d1..349a1f4 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,7 +9,7 @@ [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/src/mount/core.clj b/src/mount/core.clj index 64e43e4..3210d91 100644 --- a/src/mount/core.clj +++ b/src/mount/core.clj @@ -35,16 +35,16 @@ (validate lifecycle) (let [s-meta (cond-> {:mount-state mount-state :order (make-state-seq state) - :start `(fn [] (~@start)) + :start `(fn [] ~start) :started? false} - stop (assoc :stop `(fn [] (~@stop))) - suspend (assoc :suspend `(fn [] (~@suspend))) - resume (assoc :resume `(fn [] (~@resume))))] + 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)))))) (defn- record! [{:keys [ns name]} f done] - (let [state (f)] + (let [state (trampoline f)] (swap! done conj (ns-resolve ns name)) state)) diff --git a/test/app/app.clj b/test/app/app.clj index a240f68..2c565b6 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 916a5a4..ed5a895 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 9dbafe8..0b12404 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 e3b8fb3..8150e2f 100644 --- a/test/app/www.clj +++ b/test/app/www.clj @@ -1,7 +1,6 @@ (ns app.www (:require [app.nyse :refer [add-order find-orders create-nyse-schema]] [app.config :refer [app-config]] - [app.utils.logging :refer [with-logging-status]] [mount.core :refer [defstate]] [cheshire.core :refer [generate-string]] [compojure.core :refer [routes defroutes GET POST]] @@ -23,13 +22,10 @@ (defn start-nyse [{:keys [www]}] (create-nyse-schema) ;; creating schema (usually done long before the app is started..) - (with-logging-status) ;; enables demo logging (-> (routes mount-example-routes) (handler/site) (run-jetty {:join? false :port (:port www)}))) -(declare nyse-app) ;; in case it needs to be accessed in "resume-nyse" (helping out Clojure compiler) - -(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