├── .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 | [](https://clojars.org/nomnom/eternity)
4 |
5 | [](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 |
--------------------------------------------------------------------------------