Skip to content

Instantly share code, notes, and snippets.

@adityaathalye
Created August 27, 2024 14:52
Show Gist options
  • Save adityaathalye/960845ad75d1c93ac60542a4031cfe50 to your computer and use it in GitHub Desktop.
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)?
(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