:deps { org.rssys/context {:mvn/version "0.1.0-alpha2"}}
Warning! It is an alpha release. Everything is subject to change.
Context is a library designed to manage system state.
System state (or context) should be in one place. Context is like a central database holding the current world. Every system component should be able to get any information about the system state from one place in a consistent manner.
State management should be as simple as possible, avoiding framework-style constraints for application code. Ideally, context should be a pure Clojure atom produced from a declarative data structure on the fly.
State management should be flexible to facilitate the building of multi-tenant systems. Context should not be hard linked to a specific namespace. Ideally, context is a parameter for any system component which performs some business logic.
System state should have declarative information structure which allows one to read and understand the current state without the need for special tools. What is the whole system config? What parameters were used to start a database? What is the current implementation of a cache component? Which components are started? All of these questions should have the immediate answer from context.
Dependencies between system components should be resolved accurately.
For a long time I used the mount library and it is a very good solution for state management. But now, for my projects, I need to see the system state in one place. Other Clojure solutions force me to arrange my components in a specific manner (e.g. using defrecords and protocols). These framework-style constraints sometimes hamper the building of flexible systems by requiring that all components of an application follow the same pattern.
Add to :deps section of deps.edn the context dependency (see latest version above):
:deps { org.rssys/context {:mvn/version "0.1.0-alpha2"}}
Require necessary namespaces:
(require '[org.rssys.context.core :as ctx])
(require '[unifier.response :as r])
Each stateful object, e.g., a database connection, a cache, etc., in a system is called a component. The minimal component is a map with two required parameters: :id (keyword) and :config (map or function). The minimal component can be registered in a system context but it cannot be started (it is not executable yet).
;; define context with a minimal component
@(let [system [{:id :web :config {}}]]
(ctx/build-context system))
;; => #:context{:components {:web {:id :web, :config {}, :state-obj nil, :status :stopped, :start-deps []}}}
The context is a pure Clojure atom containing a map of components {:id-1 component-state1, …}
.
All components are placed under the :context/components
key.
This key can be overridden using the *components-path-vec*
variable.
(binding [ctx/*components-path-vec* []]
@(let [system [{:id :web :config {}}]]
(ctx/build-context system)))
;; => {:web {:id :web, :config {}, :state-obj nil, :status :stopped, :start-deps []}}
The context can be built in a step-by-step manner:
(def *ctx (atom {}))
;;=> #'user/*ctx
;; create a minimal component
(ctx/create! *ctx {:id :web :config {}})
;;=> #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :web, :meta nil}
;; create another one
(ctx/create! *ctx {:id :db :config {:host "localhost" :port 1234}})
;;=> #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :db, :meta nil}
@*ctx
;;=>
#:context{:components {:web {:id :web, :config {}, :state-obj nil, :status :stopped, :start-deps []},
:db {:id :db,
:config {:host "localhost", :port 1234},
:state-obj nil,
:status :stopped,
:start-deps []}}}
Context uses the Unified responses library. Every successful operation
with a context has type unifier.response.UnifiedSuccess
and every failure has type unifier.response.UnifiedError
.
In this example, an attempt to create a component with a pre-existing id returns a failure code:
(ctx/create! *ctx {:id :db :config {:host "localhost" :port 1234}})
;;=>
#unifier.response.UnifiedError{:type :unifier.response/conflict,
:data :db,
:meta "component with such id is already exist"}
The r/success?
and r/error?
functions from the Unified responses library
can be used to categorize the result of a context operation.
To start the component it has to be executable. The executable component has defined
start/stop functions. The start function (supplied via the :start-fn
key) takes one argument (config value) and should return a stateful object. The stop function
(under :stop-fn
) takes one argument (a stateful object) and should close connections, release resources and so on.
(def *ctx (atom {}))
;; => #'user/*ctx
;; a minimal executable component. Start function has a :config value as argument.
(ctx/create! *ctx {:id :web :config {} :start-fn (fn [config]) :stop-fn (fn [obj-state])})
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :web, :meta nil}
(ctx/start! *ctx :web)
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/success, :data :web, :meta nil}
(ctx/stop! *ctx :web)
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/success, :data :web, :meta nil}
Context library has dependency management. Every component dependency should be declared in the :start-deps
vector using other component id’s. That is, the :start-deps
vector of component C ontains the id’s of the components that should be started before C.
Example: If :web component depends on :cache component, and :cache component depends on :db component, then it can be declared like this:
(def *ctx (atom {}))
;; => #'user/*ctx
(ctx/create! *ctx {:id :db :config {} :start-deps [] :start-fn (fn [config]) :stop-fn (fn [obj-state])})
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :db, :meta nil}
(ctx/create! *ctx {:id :cache :config {} :start-deps [:db] :start-fn (fn [config]) :stop-fn (fn [obj-state])})
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :cache, :meta nil}
(ctx/create! *ctx {:id :web :config {} :start-deps [:cache] :start-fn (fn [config]) :stop-fn (fn [obj-state])})
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/created, :data :web, :meta nil}
;; the start of the :web component causes the start of :db and :cache components, respectively.
(ctx/start! *ctx :web)
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/success, :data :web, :meta nil}
;; check which components are started
(ctx/started-ids *ctx)
;; => [:db :cache :web]
The cyclic dependency check between components is implemented. To control behaviour of cyclic dependency check use flag ignore-cyclic-deps?. If flag is false (default) then throw Exception if cyclic dependency is detected. If flag is true (you know what you are doing!), then ignore cyclic dependency loop and force to start the components.
(let [*ctx (atom {})
system-map [
{:id :cfg ;; cfg component will prepare config for all context
:config {}
:start-deps []
:start-fn (fn [config]
(println "reading config data from OS & JVM environment variables or config file")
{:db {:host "localhost" :port 1234 :user "sa" :password "*****"}
:cache {:host "127.0.0.1" :user "cache-user" :pwd "***"}
:web {:host "localhost" :port 8080 :root-context "/main"}})
:stop-fn (fn [obj-state])}
{:id :db
:config (fn [ctx] (-> (ctx/get-component-value ctx :cfg) :state-obj :db))
:start-deps [:cfg]
:start-fn (fn [config] (println "starting db" :config config))
:stop-fn (fn [obj-state] (println "stopping db..."))}
{:id :cache
:config (fn [ctx] (-> (ctx/get-component-value ctx :cfg) :state-obj :cache))
:start-deps [:cfg :db]
:start-fn (fn [config] (println "starting cache" :config config))
:stop-fn (fn [obj-state] (println "stopping cache..."))}
{:id :log
:config {:output "stdout"}
:start-deps []
:start-fn (fn [config] (println "starting logging" :config config))
:stop-fn (fn [obj-state] (println "stopping logging..."))}
{:id :web
:config (fn [ctx] (-> (ctx/get-component-value ctx :cfg) :state-obj :web))
:start-deps [:cfg :db :cache :log]
:start-fn (fn [config]
(println "starting web" :config config)
(println "pass the whole context as atom to web handler:" *ctx))
:stop-fn (fn [obj-state] (println "stopping web..."))}
]
]
(ctx/build-context *ctx system-map)
(println "list of all registered components:" (ctx/list-all-ids *ctx))
(ctx/start-all *ctx)
(do
(println "do some business logic or control functions with context"))
(println "list of all started components:" (ctx/started-ids *ctx))
(ctx/stop-all *ctx))
list of all registered components: [:cfg :db :cache :log :web]
reading config data from OS & JVM environment variables or config file
starting db :config {:host localhost, :port 1234, :user sa, :password *****}
starting cache :config {:host 127.0.0.1, :user cache-user, :pwd ***}
starting logging :config {:output stdout}
starting web :config {:host localhost, :port 8080, :root-context /main}
pass the whole context as atom to web handler: #object[clojure.lang.Atom 0x25f7aecb ...
do some business logic or control functions with context
list of all started components: [:cfg :db :cache :log :web]
stopping web...
stopping cache...
stopping db...
stopping logging...
;; => #unifier.response.UnifiedSuccess{:type :unifier.response/success, :data [:cfg :db :cache :web :log], :meta nil}
Complete structure of component:
{:id :db, ;; component identifier
:config {}, ;; config is a map or fn with one arg - current whole context value
:start-deps [], ;; dependencies which should be started before this component
:start-fn #object[fn], ;; fn which starts this component with one argument (:config value)
:stop-fn #object[fn], ;; fn which stops this component with one argument (stateful object)
:state-obj nil, ;; stateful object (any value)
:status :started, ;; component status
:stop-deps [:cache]} ;; dependencies which should be stopped before this component
There are some useful low-level API functions for managing component state:
(def *ctx (atom {}))
(ctx/create! *ctx {:id :db :config {} })
(ctx/get-component *ctx :db)
(ctx/get-component-value @*ctx :db)
(ctx/update! *ctx {:id :db :config {:a 1 :b 2} :start-deps []}) ;; update the whole value
(ctx/set-config! *ctx :db {:a 42}) ;; modify :config value
(ctx/delete! *ctx :db) ;; if status is :started then it cannot be deleted
(ctx/start-all *ctx)
(ctx/stop-all *ctx)
(ctx/start-some *ctx [:db :cache])
(ctx/stop-some *ctx [:db :cache])
(ctx/started? *ctx :db)
(ctx/stopped? *ctx :db)
(ctx/isolated-stop! *ctx :db) ;; isolated stop the component ignoring all its dependencies
(ctx/isolated-start! *ctx :db) ;; isolated start the component ignoring all its dependencies
To build a project run make <command>
.
List of available commands:
clean - clear target folder
javac - compile java sources
compile - compile clojure code
build - build jar file (as library)
install - install jar file (library) to local .m2
deploy - deploy jar file (library) to clojars.org
conflicts - show class conflicts (same name class in multiple jar files)
release - release artifact.
To release artifact run clojure -A:pbuild release
.
bump - bump version artifact in build file. E.g: clojure -A:pbuilder bump beta
.
Parameter should be one of: major, minor, patch, alpha, beta, rc, qualifier
To run tests use clojure -A:test
or make test
.
Put your repository credentials to settings.xml (or set password prompt in pbuild.edn). This command will sign jar before deploy, using your gpg key. (see pbuild.edn for signing options)
Copyright © 2020 Mikhail Ananev (@MikeAnanev)
Distributed under the Eclipse Public License 2.0 or (at your option) any later version.
Can you improve this documentation? These fine people already did:
Mike Ananev & Joel DunhamEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close