[uberjar]: lifecycle taking fns and values
This commit is contained in:
parent
897f997630
commit
18e4e229ce
14 changed files with 22 additions and 519 deletions
12
README.md
12
README.md
|
|
@ -72,16 +72,16 @@ mount is an alternative to the [component](https://github.com/stuartsierra/compo
|
||||||
Creating state is easy:
|
Creating state is easy:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(defstate conn :start (create-conn))
|
(defstate conn :start create-conn)
|
||||||
```
|
```
|
||||||
|
|
||||||
where `(create-conn)` is defined elsewhere, can be right above it.
|
where `create-conn` function is defined elsewhere, can be right above it.
|
||||||
|
|
||||||
In case this state needs to be cleaned / destryed between reloads, there is also `:stop`
|
In case this state needs to be cleaned / destryed between reloads, there is also `:stop`
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(defstate conn :start (create-conn)
|
(defstate conn :start create-conn
|
||||||
:stop (disconnect conn))
|
:stop #(disconnect conn))
|
||||||
```
|
```
|
||||||
|
|
||||||
That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply
|
That is pretty much it. But wait, there is more.. this state is _a top level being_, which means it can be simply
|
||||||
|
|
@ -129,7 +129,7 @@ There are of course direct dependecies that `mount` respects:
|
||||||
(:require [mount.core :refer [defstate]]))
|
(:require [mount.core :refer [defstate]]))
|
||||||
|
|
||||||
(defstate app-config
|
(defstate app-config
|
||||||
:start (load-config "test/resources/config.edn"))
|
:start #(load-config "test/resources/config.edn"))
|
||||||
```
|
```
|
||||||
|
|
||||||
this `app-config`, being top level, can be used in other namespaces, including the ones that create states:
|
this `app-config`, being top level, can be used in other namespaces, including the ones that create states:
|
||||||
|
|
@ -139,7 +139,7 @@ this `app-config`, being top level, can be used in other namespaces, including t
|
||||||
(:require [mount.core :refer [defstate]]
|
(:require [mount.core :refer [defstate]]
|
||||||
[app.config :refer [app-config]]))
|
[app.config :refer [app-config]]))
|
||||||
|
|
||||||
(defstate conn :start (create-connection app-config))
|
(defstate conn :start #(create-connection app-config))
|
||||||
```
|
```
|
||||||
|
|
||||||
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)
|
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
(ns dev
|
(ns dev
|
||||||
"Tools for interactive development with the REPL. This file should
|
"Tools for interactive development with the REPL. This file should
|
||||||
not be included in a production build of the application."
|
not be included in a production build of the application."
|
||||||
;; (:use [cljs.repl :only [repl]]
|
|
||||||
;; [cljs.repl.browser :only [repl-env]])
|
|
||||||
(:require [clojure.java.io :as io]
|
(:require [clojure.java.io :as io]
|
||||||
[clojure.java.javadoc :refer [javadoc]]
|
[clojure.java.javadoc :refer [javadoc]]
|
||||||
[clojure.pprint :refer [pprint]]
|
[clojure.pprint :refer [pprint]]
|
||||||
|
|
@ -11,7 +9,7 @@
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.test :as test]
|
[clojure.test :as test]
|
||||||
;; [clojure.core.async :refer [>!! <!! >! <! go-loop alt! timeout]]
|
[app.utils.logging :refer [with-logging-status]]
|
||||||
[clojure.tools.namespace.repl :as tn]
|
[clojure.tools.namespace.repl :as tn]
|
||||||
[mount.core :as mount]
|
[mount.core :as mount]
|
||||||
[app.www]
|
[app.www]
|
||||||
|
|
@ -43,3 +41,5 @@
|
||||||
[]
|
[]
|
||||||
(stop)
|
(stop)
|
||||||
(tn/refresh :after 'dev/go))
|
(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.
|
|
||||||
|
|
@ -35,16 +35,16 @@
|
||||||
(validate lifecycle)
|
(validate lifecycle)
|
||||||
(let [s-meta (cond-> {:mount-state mount-state
|
(let [s-meta (cond-> {:mount-state mount-state
|
||||||
:order (make-state-seq state)
|
:order (make-state-seq state)
|
||||||
:start `(fn [] (~@start))
|
:start `(fn [] ~start)
|
||||||
:started? false}
|
:started? false}
|
||||||
stop (assoc :stop `(fn [] (~@stop)))
|
stop (assoc :stop `(fn [] (~stop)))
|
||||||
suspend (assoc :suspend `(fn [] (~@suspend)))
|
suspend (assoc :suspend `(fn [] (~suspend)))
|
||||||
resume (assoc :resume `(fn [] (~@resume))))]
|
resume (assoc :resume `(fn [] (~resume))))]
|
||||||
`(defonce ~(with-meta state (merge (meta state) s-meta))
|
`(defonce ~(with-meta state (merge (meta state) s-meta))
|
||||||
(NotStartedState. ~(str state))))))
|
(NotStartedState. ~(str state))))))
|
||||||
|
|
||||||
(defn- record! [{:keys [ns name]} f done]
|
(defn- record! [{:keys [ns name]} f done]
|
||||||
(let [state (f)]
|
(let [state (trampoline f)]
|
||||||
(swap! done conj (ns-resolve ns name))
|
(swap! done conj (ns-resolve ns name))
|
||||||
state))
|
state))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
(start-server :bind host :port port))
|
(start-server :bind host :port port))
|
||||||
|
|
||||||
;; nREPL is just another simple state
|
;; nREPL is just another simple state
|
||||||
(defstate nrepl :start (start-nrepl (:nrepl app-config))
|
(defstate nrepl :start #(start-nrepl (:nrepl app-config))
|
||||||
:stop (stop-server nrepl))
|
:stop #(stop-server nrepl))
|
||||||
|
|
||||||
;; example of an app entry point
|
;; example of an app entry point
|
||||||
(defn -main [& args]
|
(defn -main [& args]
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@
|
||||||
edn/read-string))
|
edn/read-string))
|
||||||
|
|
||||||
(defstate app-config
|
(defstate app-config
|
||||||
:start (load-config "test/resources/config.edn"))
|
:start #(load-config "test/resources/config.edn"))
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
|
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
|
||||||
(d/delete-database uri)))
|
(d/delete-database uri)))
|
||||||
|
|
||||||
(defstate conn :start (new-connection app-config)
|
(defstate conn :start #(new-connection app-config)
|
||||||
:stop (disconnect app-config conn))
|
:stop #(disconnect app-config conn))
|
||||||
|
|
||||||
;; datomic schema (staging as an example)
|
;; datomic schema (staging as an example)
|
||||||
(defn create-schema [conn]
|
(defn create-schema [conn]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
(ns app.www
|
(ns app.www
|
||||||
(:require [app.nyse :refer [add-order find-orders create-nyse-schema]]
|
(:require [app.nyse :refer [add-order find-orders create-nyse-schema]]
|
||||||
[app.config :refer [app-config]]
|
[app.config :refer [app-config]]
|
||||||
[app.utils.logging :refer [with-logging-status]]
|
|
||||||
[mount.core :refer [defstate]]
|
[mount.core :refer [defstate]]
|
||||||
[cheshire.core :refer [generate-string]]
|
[cheshire.core :refer [generate-string]]
|
||||||
[compojure.core :refer [routes defroutes GET POST]]
|
[compojure.core :refer [routes defroutes GET POST]]
|
||||||
|
|
@ -23,13 +22,10 @@
|
||||||
|
|
||||||
(defn start-nyse [{:keys [www]}]
|
(defn start-nyse [{:keys [www]}]
|
||||||
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
|
(create-nyse-schema) ;; creating schema (usually done long before the app is started..)
|
||||||
(with-logging-status) ;; enables demo logging
|
|
||||||
(-> (routes mount-example-routes)
|
(-> (routes mount-example-routes)
|
||||||
(handler/site)
|
(handler/site)
|
||||||
(run-jetty {:join? false
|
(run-jetty {:join? false
|
||||||
:port (:port www)})))
|
:port (:port www)})))
|
||||||
|
|
||||||
(declare nyse-app) ;; in case it needs to be accessed in "resume-nyse" (helping out Clojure compiler)
|
(defstate nyse-app :start #(start-nyse app-config)
|
||||||
|
:stop #(.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
|
||||||
(defstate nyse-app :start (start-nyse app-config)
|
|
||||||
:stop (.stop nyse-app)) ;; it's a "org.eclipse.jetty.server.Server" at this point
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue