├── .gitignore ├── src └── eternity │ ├── middleware │ ├── with_exception_tracking.clj │ └── with_lock.clj │ ├── pool.clj │ ├── scheduler.clj │ └── scheduler │ └── time.clj ├── .circleci ├── run └── config.yml ├── .github └── workflows │ └── test.yml ├── LICENSE ├── project.clj ├── test └── eternity │ ├── scheduler │ └── time_test.clj │ └── scheduler_test.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .lein-failures 3 | pom.xml 4 | /.nrepl-port 5 | -------------------------------------------------------------------------------- /src/eternity/middleware/with_exception_tracking.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.middleware.with-exception-tracking 2 | (:require 3 | [caliban.tracker.protocol :as tracker] 4 | [clojure.tools.logging :as log])) 5 | 6 | 7 | (defn handler [scheduled-fn] 8 | (fn [{:keys [exception-tracker] :as component}] 9 | (try 10 | (scheduled-fn component) 11 | (catch Exception err 12 | (log/error err) 13 | (tracker/report exception-tracker err))))) 14 | -------------------------------------------------------------------------------- /src/eternity/middleware/with_lock.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.middleware.with-lock 2 | (:require 3 | [clojure.tools.logging :as log] 4 | [lockjaw.protocol :as lock])) 5 | 6 | 7 | (defn handler [scheduled-fn] 8 | (fn [{:keys [lock] :as component}] 9 | (if (lock/acquire! lock) 10 | (do 11 | (log/debugf "lock-status=acquired name=%s" (:name lock)) 12 | (scheduled-fn component)) 13 | (log/debugf "lock-status=none name=%s" (:name lock))))) 14 | -------------------------------------------------------------------------------- /.circleci/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 4 | chmod +x ./cc-test-reporter 5 | ./cc-test-reporter before-build 6 | 7 | 8 | LEIN_FAST_TRAMPOLINE=1 lein trampoline cloverage \ 9 | --lcov \ 10 | --no-text \ 11 | --no-html \ 12 | --no-summary \ 13 | -o $PWD 14 | testRes=$? 15 | mkdir -p coverage 16 | mv lcov.info coverage/lcov.info 17 | ./cc-test-reporter after-build --coverage-input-type lcov --exit-code $testRes 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2.1 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/clojure:openjdk-11-lein-2.9.1 7 | working_directory: ~/repo 8 | 9 | environment: 10 | LEIN_ROOT: "true" 11 | JVM_OPTS: -Xmx3200m 12 | 13 | steps: 14 | - checkout 15 | 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "project.clj" }} 19 | - v1-dependencies- 20 | 21 | - run: lein deps 22 | 23 | - save_cache: 24 | paths: 25 | - ~/.m2 26 | key: v1-dependencies-{{ checksum "project.clj" }} 27 | 28 | - run: ./.circleci/run 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | 5 | concurrency: 6 | group: ci-${{ github.head_ref }} 7 | cancel-in-progress: true 8 | 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: clojure:openjdk-11-lein-2.9.6 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Cache deps 20 | uses: actions/cache@v2 21 | with: 22 | path: /root/.m2 23 | key: v1-deps-${{ hashFiles('project.clj') }} 24 | restore-keys: | 25 | v1-deps-${{ hashFiles('project.clj') }} 26 | 27 | - name: Install dependencies 28 | run: lein deps 29 | 30 | - name: Run tests 31 | id: tests 32 | run: lein test 33 | -------------------------------------------------------------------------------- /src/eternity/pool.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.pool 2 | (:require 3 | [clojure.tools.logging :as log] 4 | [com.stuartsierra.component :as component] 5 | [overtone.at-at :as at])) 6 | 7 | 8 | (defrecord SchedulerPool [] 9 | component/Lifecycle 10 | (start [component] 11 | (if (:pool component) 12 | component 13 | (do 14 | (log/info "starting pool") 15 | (assoc component :pool (at/mk-pool))))) 16 | 17 | (stop [component] 18 | (when-let [pool (:pool component)] 19 | (let [jobs (at/scheduled-jobs pool)] 20 | (log/infof "stopping pool jobs:%s" jobs) 21 | (at/stop-and-reset-pool! pool))) 22 | (assoc component :pool nil))) 23 | 24 | 25 | (defn create [] 26 | (SchedulerPool.)) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018 NomNom Insights 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject nomnom/eternity "1.0.1" 2 | :description "Scheduled function execution, as a Component. With optional error reporting and and lock support." 3 | :min-lein-version "2.5.0" 4 | :url "https://github.com/nomnom-insights/nomnom.eternity" 5 | :deploy-repositories {"clojars" {:sign-releases false 6 | :username :env/clojars_username 7 | :password :env/clojars_password}} 8 | 9 | :license {:name "MIT License" 10 | :url "https://opensource.org/licenses/MIT" 11 | :year 2018 12 | :key "mit"} 13 | 14 | :dependencies [[org.clojure/clojure "1.10.3"] 15 | [clj-time "0.15.2"] 16 | [overtone/at-at "1.2.0" 17 | :exclusions [org.clojure/clojure]] 18 | [com.stuartsierra/component "1.0.0"] 19 | [nomnom/caliban "1.0.3"] 20 | [nomnom/lockjaw "0.1.2"] 21 | [org.clojure/tools.logging "1.1.0"]] 22 | 23 | :profiles {:dev {:dependencies [[ch.qos.logback/logback-classic "1.2.7"]]}}) 24 | -------------------------------------------------------------------------------- /test/eternity/scheduler/time_test.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.scheduler.time-test 2 | (:require 3 | [clj-time.coerce :as coerce] 4 | [clj-time.core :as time] 5 | [clojure.test :refer [deftest is testing]] 6 | [eternity.scheduler.time :as scheduler.time])) 7 | 8 | 9 | (deftest get-delay-test 10 | (with-redefs [time/now (fn [] (coerce/to-date-time "2016-01-01T12:30:00"))] 11 | (testing "no specified" 12 | (is (= 0 (scheduler.time/get-delay {})))) 13 | (testing "delay" 14 | (is (= 123 (scheduler.time/get-delay 123)))) 15 | (testing "minutes" 16 | (is (= (* 10 60 1000) (scheduler.time/get-delay "40"))) 17 | (is (= (* 40 60 1000) (scheduler.time/get-delay "10")))) 18 | (testing "hours" 19 | (is (= (* 10 60 1000) (scheduler.time/get-delay "12:40"))) 20 | ;; 24h without 20 mins 21 | (is (= (* 1420 60 1000) (scheduler.time/get-delay "12:10")))))) 22 | 23 | 24 | (deftest to-interval-test 25 | (testing "to-interval" 26 | (is (= 1 (scheduler.time/to-interval 1))) 27 | (is (= 10000 (scheduler.time/to-interval "10s"))) 28 | (is (= 60000 (scheduler.time/to-interval "1m"))) 29 | (is (= 3900000 (scheduler.time/to-interval "65m"))) 30 | (is (= 3600000 (scheduler.time/to-interval "1h"))) 31 | (is (= 86400000 (scheduler.time/to-interval "1d"))))) 32 | -------------------------------------------------------------------------------- /src/eternity/scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.scheduler 2 | (:require 3 | [clojure.tools.logging :as log] 4 | [com.stuartsierra.component :as component] 5 | [eternity.scheduler.time :as time] 6 | [overtone.at-at :as at])) 7 | 8 | 9 | (defrecord SchedulerJob [name interval initial-delay schedule-fn scheduler-pool] 10 | component/Lifecycle 11 | (start [component] 12 | (if (:job component) 13 | component 14 | (let [_ (when-not scheduler-pool 15 | (throw (ex-info "scheduler-pool is a required dependency" {}))) 16 | handler (fn scheduled [] (schedule-fn component)) 17 | func (at/every interval 18 | handler 19 | (:pool scheduler-pool) 20 | :desc name 21 | :initial-delay initial-delay)] 22 | (log/infof "starting-scheduler name=%s interval=%s delay=%s" 23 | name interval initial-delay) 24 | (assoc component :job func)))) 25 | 26 | (stop [component] 27 | (if-let [job (:job component)] 28 | (do 29 | (log/warnf "stoppping-scheduler name=%s" (:desc job)) 30 | (at/stop job) 31 | (assoc component :job nil)) 32 | component))) 33 | 34 | 35 | (defn create [{:keys [name frequency delay]} 36 | schedule-fn] 37 | {:pre [(string? name) 38 | (or (string? frequency) (integer? frequency)) 39 | (fn? schedule-fn)]} 40 | (map->SchedulerJob {:name name 41 | :interval (time/to-interval frequency) 42 | :schedule-fn schedule-fn 43 | :initial-delay (time/get-delay delay)})) 44 | -------------------------------------------------------------------------------- /src/eternity/scheduler/time.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.scheduler.time 2 | (:require 3 | [clj-time.coerce :as coerce] 4 | [clj-time.core :as time] 5 | [clojure.string :as string])) 6 | 7 | 8 | (defn to-int [s] 9 | (Integer. ^String s)) 10 | 11 | 12 | (defn to-interval 13 | "Convert string indicating time duration 14 | to time interval in ms 15 | (to-interval '1s') => 60 16 | (to-interval '2m') => 120000 17 | (to-interval '2h') => 3600000 18 | Supports seconds (s), minutes (m), hours (h) and days (d). 19 | Cannot combine (e.g '1h1m' not allowed)!!!" 20 | [t] 21 | (if (string? t) 22 | (let [unit (last t) 23 | v (Integer. ^String (apply str (drop-last t)))] 24 | (case unit 25 | \s (* v 1000) 26 | \m (* v 60 1000) 27 | \h (* v 60 60 1000) 28 | \d (* v 24 60 60 1000))) 29 | t)) 30 | 31 | 32 | (defn- get-closest-minute 33 | "Given 'mm' return the closest time for mm in future 34 | e.g now 15:46 and mm=30 => 16:30 35 | now 15:26 and mm=30 => 15:30" 36 | [mm now] 37 | (let [scheduled (time/today-at (time/hour now) (to-int mm))] 38 | (if (= 1 (compare scheduled now)) 39 | ;; scheduled start is in future 40 | scheduled 41 | (time/plus scheduled (time/hours 1))))) 42 | 43 | 44 | (defn- get-closest-datetime 45 | "Given 'hh:mm' return the closest time for mm in future 46 | e.g now 15:46 and hh:mm=15:30 => tomorrow 15:30 47 | now 15:26 and hh:mm=15:30 => today 15:30" 48 | [hh mm now] 49 | (let [scheduled (time/today-at (to-int hh) (to-int mm))] 50 | (if (= 1 (compare scheduled now)) 51 | ;; scheduled start is in future 52 | scheduled 53 | (time/plus scheduled (time/days 1))))) 54 | 55 | 56 | (defn calculate-delay 57 | "Return the number of miliseconds till 58 | nearest start-at time (can be 'mm' or 'hh:mm'). 59 | Depends on current time whether we need to adjust the hour/day" 60 | [delay] 61 | (let [[v1 v2] (string/split delay #":") 62 | now (time/now) 63 | start-time (if v2 64 | (get-closest-datetime v1 v2 now) 65 | (get-closest-minute v1 now))] 66 | (max 0 (- (coerce/to-long start-time) (coerce/to-long now))))) 67 | 68 | 69 | (defn get-delay 70 | "Return delay before scheduler should start" 71 | [delay] 72 | (cond 73 | (integer? delay) delay 74 | (string? delay) (calculate-delay delay) 75 | :else 76 | 0)) 77 | -------------------------------------------------------------------------------- /test/eternity/scheduler_test.clj: -------------------------------------------------------------------------------- 1 | (ns eternity.scheduler-test 2 | (:require 3 | [caliban.tracker.protocol] 4 | [clojure.test :refer [deftest is testing use-fixtures]] 5 | [clojure.tools.logging :as log] 6 | [com.stuartsierra.component :as component] 7 | [eternity.middleware.with-exception-tracking :as with-exception-tracking] 8 | [eternity.middleware.with-lock :as with-lock] 9 | [eternity.pool :as pool] 10 | [eternity.scheduler :as scheduler] 11 | [lockjaw.mock])) 12 | 13 | 14 | ;; cant use Caliban mock, as we want to inspect the exception thrown 15 | (defrecord FakeTracker [store] 16 | component/Lifecycle 17 | (start [this] 18 | this) 19 | (stop [this] 20 | this) 21 | caliban.tracker.protocol/ExceptionTracker 22 | (report [_this err] 23 | (log/error "REPORTING") 24 | (reset! store err))) 25 | 26 | 27 | (defrecord test-component [atom] 28 | component/Lifecycle 29 | (start [c] 30 | (reset! atom 0) 31 | c) 32 | (stop [c] 33 | (reset! atom nil) 34 | c)) 35 | 36 | 37 | (def test-atom (atom 0)) 38 | (def system-delay 200) 39 | (def initial-delay 205) 40 | (def frequency 250) 41 | 42 | 43 | 44 | (defn simple-handler [component] 45 | (log/info "a simple handler") 46 | (let [test-atom (-> component :test-atom)] 47 | (swap! test-atom inc))) 48 | 49 | 50 | (defn exploding-handler [_] 51 | (log/info "exploding handler") 52 | (throw (ex-info "FAIL" {}))) 53 | 54 | 55 | (defn test-system [{:keys [handler always-acquire]}] 56 | (component/map->SystemMap 57 | {:test-atom test-atom 58 | :lock (lockjaw.mock/create {:always-acquire always-acquire}) 59 | :exception-tracker (->FakeTracker (atom nil)) 60 | :scheduler-pool (pool/create) 61 | :scheduler (component/using 62 | (scheduler/create 63 | {:name "test" 64 | :frequency frequency 65 | :delay system-delay} 66 | handler) 67 | [:test-atom :lock :exception-tracker :scheduler-pool])})) 68 | 69 | 70 | (use-fixtures :each (fn [test-fn] 71 | (reset! test-atom 0) 72 | (test-fn))) 73 | 74 | 75 | (deftest scheduler-test 76 | (testing "schedule fn with component" 77 | (is (= @test-atom 0)) 78 | (let [system (component/start-system (test-system {:handler simple-handler}))] 79 | ;; initial delay 80 | (is (= @test-atom 0)) 81 | (Thread/sleep initial-delay) 82 | (is (= @test-atom 1)) 83 | (Thread/sleep frequency) 84 | (is (= @test-atom 2)) 85 | (component/stop-system system) 86 | (Thread/sleep frequency) 87 | (is (= @test-atom 2))))) 88 | 89 | 90 | (deftest lock-middleware 91 | (testing "has lock and does work" 92 | (is (= @test-atom 0)) 93 | (let [system (component/start-system (test-system {:handler (with-lock/handler simple-handler) 94 | :always-acquire true}))] 95 | (is (= @test-atom 0)) 96 | (Thread/sleep initial-delay) 97 | (is (= @test-atom 1)) 98 | (Thread/sleep frequency) 99 | (is (= @test-atom 2)) 100 | (component/stop-system system) 101 | (Thread/sleep frequency) 102 | (is (= @test-atom 2)))) 103 | (testing "doesnt have a lock and doesnt work" 104 | (reset! test-atom 0) 105 | (let [system (component/start-system (test-system {:handler (with-lock/handler simple-handler) 106 | :always-acquire false}))] 107 | (is (= @test-atom 0)) 108 | (Thread/sleep initial-delay) 109 | (is (= @test-atom 0)) 110 | (Thread/sleep frequency) 111 | (is (= @test-atom 0)) 112 | (component/stop-system system) 113 | (Thread/sleep frequency) 114 | (is (= @test-atom 0))))) 115 | 116 | 117 | (deftest exception-middleware 118 | (testing "reports exception on handler error" 119 | (is (= @test-atom 0)) 120 | (let [system (component/start-system (test-system {:handler (with-exception-tracking/handler 121 | exploding-handler)}))] 122 | (is (= @test-atom 0)) 123 | (Thread/sleep initial-delay) 124 | (is (= @test-atom 0)) 125 | (Thread/sleep frequency) 126 | (testing "verify that exception was reported after throwing" 127 | (is (= "FAIL" 128 | (.getMessage (-> system :exception-tracker :store deref))))) 129 | (component/stop-system system) 130 | (Thread/sleep frequency) 131 | (testing "verify that no work has been done" 132 | (is (= @test-atom 0)))))) 133 | 134 | 135 | (deftest both-middlewares 136 | (testing "uses locks and never runs and throws but that doesnt do anything" 137 | (is (= @test-atom 0)) 138 | (let [system (component/start-system (test-system {:handler (with-exception-tracking/handler 139 | (with-lock/handler 140 | exploding-handler)) 141 | :always-acquire false}))] 142 | (is (= @test-atom 0)) 143 | (Thread/sleep initial-delay) 144 | (is (= @test-atom 0)) 145 | (Thread/sleep frequency) 146 | (testing "verify that no exception was reported because the handler didn't run" 147 | (is (nil? (-> system :exception-tracker :store deref))) 148 | (component/stop-system system) 149 | (Thread/sleep frequency) 150 | (is (= @test-atom 0))))) 151 | (testing "uses locks, always runs but throws an exception" 152 | (is (= @test-atom 0)) 153 | (let [system (component/start-system (test-system {:handler (with-exception-tracking/handler 154 | (with-lock/handler 155 | exploding-handler)) 156 | :always-acquire true}))] 157 | (is (= @test-atom 0)) 158 | (Thread/sleep initial-delay) 159 | (is (= @test-atom 0)) 160 | (Thread/sleep frequency) 161 | (testing "verify that work was done (because lock was acquired) but handler errord and exception was reported" 162 | (is (= "FAIL" (.getMessage (-> system :exception-tracker :store deref)))) 163 | (component/stop-system system) 164 | (Thread/sleep frequency) 165 | (is (= @test-atom 0)))))) 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eternity 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/nomnom/eternity.svg)](https://clojars.org/nomnom/eternity) 4 | 5 | [![CircleCI](https://circleci.com/gh/nomnom-insights/nomnom.eternity.svg?style=svg)](https://circleci.com/gh/nomnom-insights/nomnom.eternity) 6 | 7 | 8 | 9 | 10 | A [Component](https://github.com/stuartsierra/component) for scheduling jobs which is suitable to be used in component systems. Wrapper around [at-at](https://github.com/overtone/at-at) which is a wrapper around `java.util.concurrent.ScheduledThreadPoolExecutor` 11 | 12 | ## Description 13 | 14 | There are two components that you will need to setup in your system: 15 | 16 | - `scheduler-pool` the `j.u.c.ScheduledThreadPoolExecutor` instance, can (should!) be shared among multiple scheduled jobs 17 | - your scheduled job component(s) 18 | 19 | The scheduled job component: 20 | - requires the scheduler pool as a dependency, by default it uses the `:scheduler-pool` key 21 | - supports the following config options: 22 | - `name`: just to keep track of things 23 | - `frequency`: how often should scheduler run the function, can be specified in ms (100) or as date string "1m"/"2h" 24 | - `delay` (optional): used for fine-grained control of when to run the *first time*, can either be specifed in ms (1000) or as time string specifying when scheduler should start 25 | ("07:00" - at 7am, "30" - at next half hour). Use this option along with `frequency` setting to have schedulers run a function every 4 hours, exactly 15 past the hour 26 | - passed-in scheduled function: will receive 1 argument: the map of components it depends on 27 | 28 | ## How to use it 29 | 30 | Add component to your system map. 31 | 32 | ```clojure 33 | (require '[eternity.scheduler :as scheduler] 34 | '[eternity.pool :as pool]) 35 | 36 | (def system-map 37 | {:scheduler-pool (pool/create) 38 | :every-4h-stuff (component/using 39 | (scheduler/create 40 | ;; the config: will run every 4hrs, at the beginning of the hour 41 | {:name "scheduler-1" :delay "00" :frequency "4h"} 42 | ;; scheduled function 43 | (fn [components] 44 | (do-stuff (:db components))) 45 | [:scheduler-pool :db])) 46 | :every-15s (component/using 47 | (scheduler/create 48 | ;; the config: will run every 15s, right from the start 49 | {:name "scheduler-2" :frequency "15s"} 50 | ;; scheduled function 51 | (fn [components] 52 | (do-very-frequesnt-stuff components)) 53 | [:scheduler-pool :redis :publisher]))}) 54 | ``` 55 | 56 | Following setup will run scheduled job every 4 hours starting at the beginning of the hour. 57 | On each tick function `do-stuff` will be evaluated where `db` will be passed as arg. 58 | 59 | Second scheduled function will run every 15s, as soon as the system starts, the `:redis` and `:publisher` components will be passed in as the dependency map. 60 | 61 | ### Middlewares 62 | 63 | Eternity ships with middlewares, which are just functions wrapping execution of the scheduled function. They're inspired by Ring and you can add yours as long as they do the following: 64 | 65 | - accept the scheduled function as the argument 66 | - return a function which accepts `components` map as an argument 67 | - call the scheduled functions with the components as the argument 68 | 69 | This way you can compose many middlewares by nesting function calls. Just like Ring. 70 | 71 | Here's a sample/simple logging middleware: 72 | 73 | ```clojure 74 | (defn with-logging [scheduled-fn] 75 | (fn [component] 76 | (log/info "about to do work") 77 | (scheduled-fn component) 78 | (log/info "done!"))) 79 | ``` 80 | 81 | You can use it in your Component system like so: 82 | 83 | ```clojure 84 | { :scheduled-fn (component/using 85 | {:name "a sched" :frequency "15s" } 86 | (with-logging (fn [component] (db/do-query (:db-conn component))))) 87 | [:db-conn :scheduler-pool]} 88 | 89 | ``` 90 | 91 | #### Included Middlewares 92 | 93 | #### `with-lock` (`eternity.middleware.with-lock`) 94 | 95 | Depends on [Lockjaw](https://github.com/nomnom-insights/nomnom.lockjaw) and Postgres, guarantees that only the lock holder will do the job. Requires a `:lock` component as a dependency. You can use [nomnom/utility-belt.sql](https://github.com/nomnom-insights/nomnom.utility-belt.sql) if you need a JDBC connection pool component. 96 | Example: 97 | 98 | ```clojure 99 | 100 | {:scheduler-pool (eternity.pool/create) 101 | :db-conn (db-pool/create pg-config) 102 | :lock (component/using 103 | (lockjaw.create {:name "a-service-name"}) 104 | [:db-conn]) 105 | :scheduled-thing (component/using 106 | (eternity.scheduler 107 | {:name "scheduled-thing" :frequency "8h"} 108 | (eternity.middleware.with-lock/handler 109 | ;; will execute only if the lock is acquired! 110 | (fn [component] 111 | (do-stuff component)))) 112 | [:lock :scheduler-pool])} 113 | 114 | 115 | ``` 116 | 117 | #### `with-exception-tracking` (`eternity.middleware.with-exception-tracking`) 118 | 119 | Depends on [Caliban](https://github.com/nomnom-insights/nomnom.caliban). Requires `:exception-tracker` component as a dependency. 120 | 121 | 122 | ```clojure 123 | 124 | {:scheduler-pool (eternity.pool/create) 125 | :exception-tracker (caliban.tracker/create config) 126 | :scheduled-thing (component/using 127 | (eternity.scheduler 128 | {:name "scheduled-thing" :frequency "8h"} 129 | (eternity.middleware.with-exception-tracking/handler 130 | ;; if do -stuff throws an exception it will be logged and reported 131 | (fn [component] 132 | (do-stuff component)))) 133 | [:exception-tracker :scheduler-pool])} 134 | 135 | 136 | ``` 137 | 138 | 139 | ### Combining middlewares 140 | 141 | Just like in Ring, middlewares can be combined: 142 | 143 | ```clojure 144 | 145 | {:scheduler-pool (eternity.pool/create) 146 | :exception-trakcer (caliban.tracker/create config) 147 | :lock (component/using (lockjaw.create {:name "service"}) [:db-conn]) 148 | :scheduled-thing (component/using 149 | (eternity.scheduler 150 | {:name "scheduled-thing" :frequency "8h"} 151 | ;; Stack middlewares: 152 | (eternity.middleware.with-exception-tracking/handler 153 | (eternity.middleware.with-lock/handler 154 | (fn [component] 155 | (do-stuff component))))) 156 | [:exception-tracker :lock :scheduler-pool])} 157 | 158 | 159 | ``` 160 | 161 | 162 | ## Roadmap 163 | 164 | 165 | - [ ] *Maybe* remove direct dependency on `at-at` and use the Scheduled ThreadPool Executor directly 166 | - [ ] More middlewares 167 | 168 | # Authors 169 | 170 | In alphabetical order 171 | 172 | - [Afonso Tsukamoto](https://github.com/AfonsoTsukamoto) 173 | - [Łukasz Korecki](https://github.com/lukaszkorecki) 174 | - [Marketa Adamova](https://github.com/MarketaAdamova) 175 | --------------------------------------------------------------------------------