Skip to content

Donut Systemλ︎

Donut system takes a system as data approach, using a hash-map to define the overall system with keys to define each component (or component group) in that system.

Component definitions are also a hash-map with :start, :stop, :config keys to express how to manage that component

Donut system configuration is a similar data-centric approach to that used by reitit for http request routing.

Practicalli uses ::donut alias instead of ::ds

The donut.system library is required using the :as donut alias.

::donut is used as the keyword qualifier

Practicalli recommends meaningful names to make code easier to read and searching considerably simpler (fewer false matches)

Create project with Donutλ︎

practicalli/service template from Practicalli Project Templates can be given a :component option to include the Donut System library and example code.

:project/create alias from Practicalli Clojure CLI Config

Create Clojure Web Service project with Donut

clojure -T:project/create :template practicalli/service :component :donut :name practicalli/web-service-name

Including Donutλ︎

Donut library includes a REPL workflow namespace, so there is only one library dependency to add to the project. This project must be included at runtime so should be added to the project deps.edn configuration

Donut dependency in Gameboard project

 ["src" "resources"]

 {;; Service
  http-kit/http-kit {:mvn/version "2.6.0"}  ; latest "2.7.0-alpha1"
  metosin/reitit    {:mvn/version "0.5.13"}

  ;; Logging
  com.brunobonacci/mulog             {:mvn/version "0.9.0"}
  com.brunobonacci/mulog-adv-console {:mvn/version "0.9.0"}

  ;; System
  aero/aero           {:mvn/version "1.1.6"}
  party.donut/system {:mvn/version "0.0.202"}
  org.clojure/clojure {:mvn/version "1.12.0"}}}

Define a Systemλ︎

Donut defines a system using a Clojure hash-map with the following top level keys

  • ::donut/defs to define components of a system or component group
  • ::donut/signals customise the startup/shutdown approach (optional)

Create a system namespace to define the donut system

Require libraries in the namespace form

(ns practicalli.gameboard.system
   ;; Application dependencies
   [practicalli.donoughty.router :as router]

   ;; System dependencies
   [org.httpkit.server     :as http-server]
   [com.brunobonacci.mulog :as mulog]
   [donut.system           :as donut]
   [aero.core              :as aero]
   [        :as io]))

Define a system that runs a web server with event log publisher

The http server use the :env environment to determine the port, although this could be defined directly in the :http :server :config section.

There is a relationship inside the http component between server and handler. The handler depends on configuration within the :env environment configuration.

The :instance key is associated with the component reference that is returned when a component is started. The :instance reference is used to shut down the service.

The event log publisher and http service have no intrinsic relationship, so order of startup is not an issue as any mulog events created are cached until the publisher has started.

Simple Web Service

(def system
  "System Component management with Donut"
   {:env  {:http-port 8080
           :persistence {:database-host (System/getenv "POSTGRES_HOST")
                         :database-port (System/getenv "POSTGRES_PORT")
                         :database-username (System/getenv "POSTGRES_USERNAME")
                         :database-password (System/getenv "POSTGRES_PASSWORD")
                         :database-schema (System/getenv "POSTGRES_SCHEMA")}}
    :event-log {:publisher
                #::donut{:start (fn mulog-publisher-start
                                  [{{:keys [dev]} ::donut/config}]
                                  (mulog/start-publisher! dev))
                         :stop (fn mulog-publisher-stop
                                 [{::donut/keys [instance]}]
                         :config {:dev {:type :console :pretty? true}}}}
    :http {:server
           #::donut{:start (fn http-kit-run-server
                             [{{:keys [handler options]} ::donut/config}]
                             (http-server/run-server handler options))
                    :stop  (fn http-kit-stop-server
                             [{::donut/keys [instance]}]
                    :config {:handler (donut/local-ref [:handler])
                             :options {:port  (donut/ref [:env :http-port])
                                       :join? false}}}
           :handler (router/app (donut/ref [:env :persistence]))}}})

Start the systemλ︎

Use donut/signal with the ::donut/start key to start all the components in the system.

::donut/signals key is associated with a signal configuration to modify the start and stop process, although the default process should work in most cases.

Define a -main function in the main namespace of the service, e.g practicalli.gameboard.service

The -main function starts the Donut system and keeps the system reference as a local name

The system reference is used to shutdown the system, typically wrapped in code to handle SIGTERM signals from the infrastructure running the service (Operating system, Kubernettes, EC2, etc.)

Start a Donut system

(defn -main
  "practicalli service managed by donut system,
  Aero is used to configure Integrant configuration based on profile (dev, test, prod),
  allowing environment specific configuration, e.g. mulog publisher
  The shutdown hook gracefully stops the service on receipt of a SIGTERM from the infrastructure,
  giving the application 30 seconds before forced termination."

   {:app-name "practicalli donoughty service" :version  "0.1.0"})

  (mulog/log ::gameboard-system :system-config system/config)

  (let [running-system (donut/signal system/system ::donut/start)]
         (Thread. ^Runnable #(donut/signal running-system ::donut/stop)))))
Mulog event logging is included and the system should start a mulog publisher via the system configuration

Service REPL Workflowλ︎

donut.system.repl namespace provides functions to start, stop and restart system components.

The main system configuration used when starting the service can also be used for the REPL, or other named systems can be defined allowing for a customised system during development.

Service REPL workflow

(ns system-repl
  "Tools for REPL Driven Development"
   [donut.system :as donut]
   [donut.system.repl :as donut-repl]
   [practicalli.donoughty.system :as donoughty]
   [com.brunobonacci.mulog :as mulog]))

(defmethod donut/named-system :donut.system/repl
  [_] donoughty/main)

(defn start
  "Start system with donut, optionally passing a named system"
  ([] (donut-repl/start))
  ([system-config] (donut-repl/start system-config)))

(defn stop
  "Stop the currently running system"
  []  (donut-repl/stop))

(defn restart
  "Restart the system with donut repl,
  Uses to reload namespaces
  `( :after 'donut.system.repl/start)`"
  [] (donut-repl/restart))