[#5]: suspendable example app.. [in progress]

This commit is contained in:
anatoly 2015-11-21 12:04:10 -05:00
parent 4e97280be9
commit 9d7c5576f8
13 changed files with 128 additions and 315 deletions

View file

@ -13,21 +13,15 @@
[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]
[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/stop-except #'app.www/nyse-app))
(defn refresh []
(stop)

View file

@ -14,5 +14,18 @@
: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]]]}})
[com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]}
;; "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}})

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

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

@ -0,0 +1,37 @@
(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)})))
(defn resume-nyse [conf]
;; making decision to whether call start / do something ro resume / or just do nothing
;; ...
)
(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
{:uri "datomic:mem://mount"}
:www {:port 4242}
:h2
{:classname "org.h2.Driver"
:subprotocol "h2"