updating suspendable example to the latest mount
This commit is contained in:
parent
d1ba7908bf
commit
0384bc3d10
12 changed files with 314 additions and 583 deletions
173
README.md
173
README.md
|
|
@ -7,6 +7,7 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
|
|||
module | branch | status
|
||||
----------|----------|----------
|
||||
mount | `master` | [](https://circleci.com/gh/tolitius/mount/tree/master)
|
||||
mount | `0.1.5` | [](https://circleci.com/gh/tolitius/mount/tree/0.1.5)
|
||||
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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 [>!! <!! >! <! go-loop alt! timeout]]
|
||||
[clojure.tools.namespace.repl :as tn]
|
||||
[app.utils.logging :refer [with-logging-status]]
|
||||
[mount.core :as mount]
|
||||
[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
|
||||
|
|
@ -43,3 +41,5 @@
|
|||
[]
|
||||
(stop)
|
||||
(tn/refresh :after 'dev/go))
|
||||
|
||||
(with-logging-status) ;; to demo REPL time logging
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -1,3 +0,0 @@
|
|||
# Introduction to statuo
|
||||
|
||||
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)
|
||||
|
|
@ -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]`.
|
||||
163
doc/uberjar.md
163
doc/uberjar.md
|
|
@ -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.
|
||||
26
project.clj
26
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}})
|
||||
|
|
|
|||
|
|
@ -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 <)}))
|
||||
|
|
|
|||
46
test/app/utils/logging.clj
Normal file
46
test/app/utils/logging.clj
Normal 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)))
|
||||
Loading…
Reference in a new issue