Created
August 27, 2024 14:52
-
-
Save adityaathalye/960845ad75d1c93ac60542a4031cfe50 to your computer and use it in GitHub Desktop.
Why do we need a whole library to start/stop our Clojure web app "system" (libraries like integrant / component / mount)?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns cats.core | |
(:gen-class) | |
(:require [cats.config :as cc] | |
cats.server)) | |
;; Quick spike to make a "system" tool, to better understand | |
;; the core ideas, and the little gotchas of the whole business | |
;; of safely starting/stopping a service. It's better to use | |
;; one of the community-built libraries for this. | |
;; | |
;; Source: a study project I was doing. | |
;; ref: https://gitlab.com/nilenso/cats/-/blob/cc52fad8a922729932ec73c7e7173a4aca747c06/src/clj/cats/core.clj | |
(defonce system ; a system in an atom = systom (I'll show myself out) | |
(atom {:profile {:requires-component :profile | |
:starter [identity] | |
:state nil} | |
:config {:requires-component :profile | |
:starter [cc/get-config] | |
:state nil} | |
:db {:requires-component :config | |
:starter [cc/db-connection-config | |
identity] ; cats.db.core/connect-db | |
:stopper identity ; cats.db.core/disconnect-db | |
:state nil} | |
:server {:requires-component :config | |
:starter [cc/server-config | |
cats.server/start-server] | |
:stopper cats.server/stop-server | |
:state nil} | |
:futures {:stopper shutdown-agents}})) | |
(def start-sequence | |
[:profile :config :db :server :futures]) | |
(def stop-sequence | |
(reverse start-sequence)) | |
(defn- get-component-state | |
[component-key] | |
(-> @system component-key :state)) | |
(defn- set-component-state! | |
[component-key override-state] | |
(swap! system | |
assoc-in | |
[component-key :state] | |
override-state)) | |
(comment | |
(set-component-state! :profile :dev) | |
) | |
(defn component-startable? | |
"Given a component key, return the component map if it is a thing | |
that can be started: | |
- It must have a sequence of one or more :starter functions | |
- It must not be live, i.e. :state must be nil." | |
[component-key] | |
(let [component (component-key @system)] | |
(when (and (:starter component) | |
(not (:state component))) | |
component))) | |
(comment | |
(map component-startable? | |
start-sequence) | |
) | |
(defn start-component! | |
[component-key] | |
(when-let [component (component-startable? component-key)] | |
(let [parent-state (get-component-state (-> component :requires-component)) | |
started-obj (reduce (fn [next-state starter-fn] | |
(starter-fn next-state)) | |
parent-state | |
(:starter component))] | |
(set-component-state! component-key | |
started-obj)))) | |
(comment | |
(do (set-component-state! :profile :dev) | |
(start-component! :config) | |
(start-component! :db) | |
(start-component! :server)) | |
) | |
(defn start-system | |
([profile] | |
(start-system profile | |
start-sequence)) | |
([profile start-sequence] | |
;; Ensure we bootstrap profile | |
(set-component-state! :profile | |
(keyword profile)) | |
(doseq [component-key start-sequence] | |
(start-component! component-key)))) | |
(comment | |
system | |
(start-system :dev | |
start-sequence) | |
) | |
(defn component-stoppable? | |
"Given an component key, return the component map if it is a thing | |
that can be stopped: | |
- It must have a single :stopper function that uses live :state | |
- The component :state must be the live object (not nil)." | |
([component-key] | |
(component-stoppable? component-key | |
@system)) | |
([component-key system-map] | |
(let [component (component-key system-map)] | |
(when (:stopper component) | |
component)))) | |
(defn stop-component! | |
[component-key] | |
(when-let [{stopper-fn :stopper | |
:as component} (component-stoppable? component-key)] | |
;; stop the component | |
(if (contains? component :state) | |
(stopper-fn (get-component-state component-key)) | |
(stopper-fn)) | |
;; evict its now-dead object from state | |
(set-component-state! component-key | |
nil))) | |
(comment | |
(swap! system | |
assoc-in | |
[:server :stopper] | |
cats.server/stop-server) | |
(set-component-state! :server | |
nil) | |
(start-component! :server) | |
(stop-component! :server) | |
) | |
(defn stop-system | |
([] | |
(stop-system stop-sequence)) | |
([stop-sequence] | |
(doseq [component-key stop-sequence] | |
(stop-component! component-key)))) | |
(comment | |
(do (stop-system (rest stop-sequence)) | |
system) | |
) | |
(defn -main | |
[& args] | |
(let [profile (-> args first keyword)] | |
(start-system profile start-sequence)) | |
(.addShutdownHook ^java.lang.Runtime (Runtime/getRuntime) | |
(Thread. ^Runnable stop-system))) | |
(comment | |
(-main :dev) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment