Compare commits

...

3 commits

Author SHA1 Message Date
anatoly
0384bc3d10 updating suspendable example to the latest mount 2015-11-30 22:03:41 -05:00
anatoly
d1ba7908bf [#5]: suspendable example app.. [done] 2015-11-21 14:04:15 -05:00
anatoly
9d7c5576f8 [#5]: suspendable example app.. [in progress] 2015-11-21 12:04:22 -05:00
23 changed files with 438 additions and 889 deletions

173
README.md
View file

@ -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

View file

@ -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,23 +9,18 @@
[clojure.set :as set]
[clojure.string :as str]
[clojure.test :as test]
;; [clojure.core.async :refer [>!! <!! >! <! go-loop alt! timeout]]
[clojure.tools.namespace.repl :as tn]
[check.parts-test]
[check.start-with-test]
[check.suspend-resume-test]
[app.utils.logging :refer [with-logging-status]]
[mount.core :as mount]
[app :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time
[app.www]
[app.nyse :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time
(defn start []
(mount/start-without #'check.start-with-test/test-conn
#'check.start-with-test/test-nrepl
#'check.parts-test/should-not-start
#'check.suspend-resume-test/web-server
#'check.suspend-resume-test/q-listener)) ;; example on how to start app without certain states
(mount/start))
(defn stop []
(mount/stop))
(mount/suspend)
(mount/stop-except #'app.www/nyse-app))
(defn refresh []
(stop)
@ -48,3 +41,5 @@
[]
(stop)
(tn/refresh :after 'dev/go))
(with-logging-status) ;; to demo REPL time logging

View file

@ -1,197 +0,0 @@
###### Differences from "Component"
## Perception
Solving the "application state" in Clojure, where an application is not a tool or a library,
but a product that has lots of state to deal with, is not a trivial task.
The [Component](https://github.com/stuartsierra/component) framework is a solution that has been gaining popularity:
> _[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`
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,3 +0,0 @@
# Introduction to statuo
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

View file

@ -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]`.

View file

@ -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`:
<img src="img/welcome-uberjar.png" width="250">
See if we have any orders:
<img src="img/get-uberjar.png" width="400">
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:
<img src="img/post-uberjar.png" width="400">
### 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.

View file

@ -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,13 +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"]
[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"]
:main app
:aot :all}})

View file

@ -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 <)}))

View file

@ -1,10 +1,9 @@
(ns app
(:require [datomic.api :as d]
[clojure.tools.nrepl.server :refer [start-server stop-server]]
(:require [clojure.tools.nrepl.server :refer [start-server stop-server]]
[mount.core :as mount :refer [defstate]]
[app.utils.datomic :refer [touch]]
[app.config :refer [app-config]]
[app.nyse :as nyse]))
[app.www])
(:gen-class)) ;; for -main / uberjar (no need in dev)
;; example on creating a network REPL
(defn- start-nrepl [{:keys [host port]}]
@ -14,52 +13,6 @@
(defstate nrepl :start (start-nrepl (:nrepl app-config))
:stop (stop-server nrepl))
;; datomic schema
(defn create-schema [conn]
(let [schema [{:db/id #db/id [:db.part/db]
:db/ident :order/symbol
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/index true
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/bid
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/qty
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/offer
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}]]
@(d/transact conn schema)))
(defn add-order [ticker bid offer qty] ;; can take connection as param
@(d/transact nyse/conn [{:db/id (d/tempid :db.part/user)
:order/symbol ticker
:order/bid bid
:order/offer offer
:order/qty qty}]))
(defn find-orders [ticker] ;; can take connection as param
(let [orders (d/q '[:find ?e :in $ ?ticker
:where [?e :order/symbol ?ticker]]
(d/db nyse/conn) ticker)]
(touch nyse/conn orders)))
(defn create-nyse-schema []
(create-schema nyse/conn))
;; example of an app entry point
(defn -main [& args]
(mount/start))

View file

@ -1,5 +1,5 @@
(ns app.config
(:require [mount.core :as mount :refer [defstate]]
(:require [mount.core :refer [defstate]]
[clojure.edn :as edn]
[clojure.tools.logging :refer [info]]))

50
test/app/db.clj Normal file
View file

@ -0,0 +1,50 @@
(ns app.db
(:require [mount.core :refer [defstate]]
[datomic.api :as d]
[clojure.tools.logging :refer [info]]
[app.config :refer [app-config]]))
(defn- new-connection [conf]
(info "conf: " conf)
(let [uri (get-in conf [:datomic :uri])]
(info "creating a connection to datomic:" uri)
(d/create-database uri)
(d/connect uri)))
(defn disconnect [conf conn]
(let [uri (get-in conf [:datomic :uri])]
(info "disconnecting from " uri)
(.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))
;; datomic schema (staging as an example)
(defn create-schema [conn]
(let [schema [{:db/id #db/id [:db.part/db]
:db/ident :order/symbol
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/index true
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/bid
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/qty
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/offer
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}]]
@(d/transact conn schema)))

View file

@ -1,21 +1,20 @@
(ns app.nyse
(:require [mount.core :as mount :refer [defstate]]
[datomic.api :as d]
[clojure.tools.logging :refer [info]]
[app.config :refer [app-config]]))
(:require [datomic.api :as d]
[app.db :refer [create-schema] :as db]
[app.utils.datomic :refer [touch]]))
(defn- new-connection [conf]
(info "conf: " conf)
(let [uri (get-in conf [:datomic :uri])]
(info "creating a connection to datomic:" uri)
(d/create-database uri)
(d/connect uri)))
(defn add-order [ticker bid offer qty] ;; can take connection as param
@(d/transact db/conn [{:db/id (d/tempid :db.part/user)
:order/symbol ticker
:order/bid bid
:order/offer offer
:order/qty qty}]))
(defn disconnect [conf conn]
(let [uri (get-in conf [:datomic :uri])]
(info "disconnecting from " uri)
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
(d/delete-database uri)))
(defn find-orders [ticker] ;; can take connection as param
(let [orders (d/q '[:find ?e :in $ ?ticker
:where [?e :order/symbol ?ticker]]
(d/db db/conn) ticker)]
(touch db/conn orders)))
(defstate conn :start (new-connection app-config)
:stop (disconnect app-config conn))
(defn create-nyse-schema []
(create-schema db/conn))

View file

@ -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)))

41
test/app/www.clj Normal file
View file

@ -0,0 +1,41 @@
(ns app.www
(:require [app.nyse :refer [add-order find-orders create-nyse-schema]]
[app.config :refer [app-config]]
[mount.core :refer [defstate]]
[cheshire.core :refer [generate-string]]
[compojure.core :refer [routes defroutes GET POST]]
[compojure.handler :as handler]
[ring.adapter.jetty :refer [run-jetty]]))
(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}})))
(defn start-nyse [{:keys [www]}]
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
(-> (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)
(defn resume-nyse [conf]
;; making decision to whether call start / do something / or resume / or just do nothing
;; access to the current (previous/old) state is here just by its name "nyse-app"
;; ...
(create-nyse-schema)
nyse-app) ;; returning an existing nyse-app, _so it can be stopped_, later on
(defstate nyse-app :start (start-nyse app-config)
:resume (resume-nyse app-config)
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point

View file

@ -1,17 +0,0 @@
(ns check.parts-test
(:require [mount.core :as mount :refer [defstate] :as m]
[app.nyse :refer [conn]]
[clojure.test :refer :all]))
(defstate should-not-start :start (constantly 42))
(defn with-parts [f]
(m/start #'app.config/app-config #'app.nyse/conn)
(f)
(m/stop))
(use-fixtures :each with-parts)
(deftest start-only-parts
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? mount.core.NotStartedState should-not-start)))

View file

@ -1,46 +0,0 @@
(ns check.start-with-test
(:require [mount.core :as mount :refer [defstate]]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defstate test-conn :start (long 42)
:stop (constantly 0))
(defstate test-nrepl :start (vector))
(deftest start-with
(testing "should start with substitutes"
(let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn
#'app/nrepl #'check.start-with-test/test-nrepl})]
(is (map? app-config))
(is (vector? nrepl))
(is (= conn 42))
(mount/stop)))
(testing "should not start the substitute itself"
(let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn})]
(is (instance? mount.core.NotStartedState test-conn))
(is (= conn 42))
(mount/stop)))
(testing "should start normally after start-with"
(let [_ (mount/start)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= test-conn 42))
(is (vector? test-nrepl))
(mount/stop)))
(testing "should start-without normally after start-with"
(let [_ (mount/start-without #'check.start-with-test/test-conn
#'check.start-with-test/test-nrepl)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? mount.core.NotStartedState test-conn))
(is (instance? mount.core.NotStartedState test-nrepl))
(mount/stop))))

View file

@ -1,18 +0,0 @@
(ns check.start-without-test
(:require [mount.core :as m]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defn without [f]
(m/start-without #'app.nyse/conn #'app/nrepl)
(f)
(m/stop))
(use-fixtures :each without)
(deftest start-without-states
(is (map? app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? mount.core.NotStartedState conn)))

View file

@ -1,31 +0,0 @@
(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)))))

View file

@ -1,123 +0,0 @@
(ns check.suspend-resume-test
(:require [mount.core :as mount :refer [defstate]]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defn koncat [k s]
(-> (name k)
(str "-" (name s))
keyword))
(defn start [s] (koncat s :started))
(defn stop [s] (koncat s :stopped))
(defn suspend [s] (koncat s :suspended))
(defn resume [s] (koncat s :resumed))
(defstate web-server :start (start :w)
:stop (stop :w)
:suspend (suspend :w)
:resume (resume :w))
(defstate q-listener :start (start :q)
:stop (stop :q)
:suspend (suspend :q)
:resume (resume :q))
(deftest suspendable
;; lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'app/nrepl)
_ (mount/suspend)
_ (mount/resume)]
(is (map? app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? mount.core.NotStartedState conn))
(is (instance? mount.core.NotStartedState web-server))))
;; start-with
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with {#'app/nrepl #'check.suspend-resume-test/web-server})
_ (mount/suspend)]
(is (= nrepl :w-suspended))
(is (instance? mount.core.NotStartedState web-server))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable one,
the later should not be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'check.suspend-resume-test/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
_ (mount/suspend)]
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? datomic.peer.LocalConnection web-server))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a suspendable one,
the later should be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/q-listener})] ;; TODO: good to WARN on started states during "start-with"
(is (= q-listener :q-suspended))
(is (= web-server :q-resumed))
(mount/suspend)
(is (= q-listener :q-suspended))
(is (= web-server :q-suspended))
(mount/stop)
(is (instance? mount.core.NotStartedState web-server))
(is (instance? mount.core.NotStartedState q-listener))
(mount/start)
(mount/suspend)
(is (= q-listener :q-suspended))
(is (= web-server :w-suspended))
(mount/stop))))

View file

@ -1,6 +1,8 @@
{:datomic
{:datomic
{:uri "datomic:mem://mount"}
:www {:port 4242}
:h2
{:classname "org.h2.Driver"
:subprotocol "h2"