|
| 1 | +# Cronut Javax: A Clojure Companion to Quartz (Legacy Javax Support) |
| 2 | + |
| 3 | +[](https://github.com/factorhouse/cronut/actions/workflows/ci.yml) |
| 4 | +[](https://clojars.org/io.factorhouse/cronut) |
| 5 | + |
| 6 | +# Summary |
| 7 | + |
| 8 | +[Cronut](https://github.com/factorhouse/cronut) provides a data-first [Clojure](https://clojure.org/) wrapper |
| 9 | +for [Quartz Scheduler](https://github.com/quartz-scheduler/quartz) version `2.4.0`, compatible |
| 10 | +with [Javax](https://jakarta.ee/blogs/javax-jakartaee-namespace-ecosystem-progress/). |
| 11 | + |
| 12 | +Cronut supports **in-memory** scheduling of jobs within a single JVM. JDBC and distributed jobstore are not supported. |
| 13 | + |
| 14 | +## Related Projects |
| 15 | + |
| 16 | +| Project | Desription | Clojars Project | |
| 17 | +|---------------------------------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| |
| 18 | +| [cronut](https://github.com/factorhouse/cronut) | Cronut with [Jakarta](https://en.wikipedia.org/wiki/Jakarta_EE) support | [](https://clojars.org/io.factorhouse/cronut-javax) | |
| 19 | +| [cronut-integrant](https://github.com/factorhouse/cronut-integrant) | [Integrant](https://github.com/weavejester/integrant) bindings for Cronut | [](https://clojars.org/io.factorhouse/cronut-integrant) | |
| 20 | + |
| 21 | +# Contents |
| 22 | + |
| 23 | +# Usage |
| 24 | + |
| 25 | +A quartz `scheduler` runs a `job` on a schedule defined by a `trigger`. |
| 26 | + |
| 27 | +## Scheduler |
| 28 | + |
| 29 | +Cronut provides access to the Quartz Scheduler, exposed via the `cronut/scheduler` function. |
| 30 | + |
| 31 | +Create a scheduler with the following configuration: |
| 32 | + |
| 33 | +1. `:concurrent-execution-disallowed?`: (optional, default false) - run all jobs with @DisableConcurrentExecution |
| 34 | +2. `:update-check?`: (optional, default false) - check for Quartz updates on system startup. |
| 35 | + |
| 36 | +````clojure |
| 37 | +(cronut/scheduler {:concurrent-execution-disallowed? true |
| 38 | + :update-check? false}) |
| 39 | +```` |
| 40 | + |
| 41 | +### Scheduler lifecycle |
| 42 | + |
| 43 | +Once created, you can: |
| 44 | + |
| 45 | +* `cronut/start`: start the scheduler |
| 46 | +* `cronut/start-delayed`: start the scheduler with a delay |
| 47 | +* `cronut/standy`: temporarily halt the firing of triggers by the scheduler |
| 48 | +* `cronut/shutdown`: stop the scheduler |
| 49 | +* `cronut/pause-all`: pause all triggers |
| 50 | +* `cronut/resume-all`: resume all triggers |
| 51 | +* `cronut/clear`: clear all scheduling data of jobs and triggers |
| 52 | + |
| 53 | +### Scheduling jobs |
| 54 | + |
| 55 | +To schedule jobs, you can |
| 56 | + |
| 57 | +* `cronut/schedule-job`: schedule a single job |
| 58 | +* `cronut/schedule-jobs`: schedule multiple jobs at once |
| 59 | +* `cronut/pause-job`: pause a job |
| 60 | +* `cronut/resume-job`: resume a paused job |
| 61 | +* `cronut/unschedule-job`: remove a trigger from the scheduler |
| 62 | +* `cronut/delete-job`: remove a job and all associated triggers from the scheduler |
| 63 | +* `cronut/pause-trigger`: pause a trigger |
| 64 | +* `cronut/resume-trigger`: resume a paused trigger |
| 65 | + |
| 66 | +## Jobs |
| 67 | + |
| 68 | +Each cronut job must implement the `org.quartz.Job` interface. |
| 69 | + |
| 70 | +The expectation being that every job will reify that interface either directly via `reify` or by returning a `defrecord` |
| 71 | +that implements the interface. |
| 72 | + |
| 73 | +Cronut supports further Quartz configuration of jobs (identity, description, recovery, and durability) by expecting |
| 74 | +those values to be assoc'd onto your job. You do not have to set them (in fact in most cases you can likely ignore |
| 75 | +them), however if you do want that control you will likely use the `defrecord` approach as opposed to `reify`. |
| 76 | + |
| 77 | +Concurrent execution can be controlled on a per-job bases with the `disallow-concurrent-execution?` flag. |
| 78 | + |
| 79 | +### Job example |
| 80 | + |
| 81 | +````clojure |
| 82 | +(defrecord TestDefrecordJobImpl [identity description recover? durable?] |
| 83 | + Job |
| 84 | + (execute [this _job-context] |
| 85 | + (log/info "Defrecord Impl:" this))) |
| 86 | + |
| 87 | + |
| 88 | +(let [scheduler (cronut/scheduler {:concurrent-execution-disallowed? true |
| 89 | + :update-check? false}) |
| 90 | + defrecord-job (map->TestDefrecordJobImpl {:identity ["name1" "group2"] |
| 91 | + :description "test job" |
| 92 | + :recover? true |
| 93 | + :durable? false}) |
| 94 | + reify-job (reify Job |
| 95 | + (execute [_this _job-context] |
| 96 | + (let [rand-id (str (UUID/randomUUID))] |
| 97 | + (log/info rand-id "Reified Impl"))))] |
| 98 | + |
| 99 | + (cronut/schedule-job scheduler (trigger/interval 1000) defrecord-job) |
| 100 | + |
| 101 | + (cronut/schedule-job scheduler |
| 102 | + (trigger/builder {:type :cron |
| 103 | + :cron "*/5 * * * * ?" |
| 104 | + :misfire :do-nothing}) |
| 105 | + reify-job)) |
| 106 | +```` |
| 107 | + |
| 108 | +## Triggers |
| 109 | + |
| 110 | +Cronut triggers are of type `org.quartz.Trigger`, the following functions are provided to simplify trigger creation: |
| 111 | + |
| 112 | +#### `cronut.trigger/cron`: Simple Cron Scheduling |
| 113 | + |
| 114 | +A job is scheduled to run on a cron by using the `cronut.trigger/cron` function with a valid cron expression. |
| 115 | + |
| 116 | +The job will start immediately when the system is initialized, and runs in the default system time-zone |
| 117 | + |
| 118 | +````clojure |
| 119 | +(cronut.trigger/cron "*/8 * * * * ?") |
| 120 | +```` |
| 121 | + |
| 122 | +#### `cronut.trigger/interval`: Simple Interval Scheduling |
| 123 | + |
| 124 | +A job is scheduled to run periodically by using the `cronut.trigger/interval` function with a milliseconds value |
| 125 | + |
| 126 | +````clojure |
| 127 | +(cronut.trigger/interval 3500) |
| 128 | +```` |
| 129 | + |
| 130 | +#### `cronut.trigger/builder`: Full trigger definition |
| 131 | + |
| 132 | +Both `cronut.trigger/cron` and `cronut.trigger/interval` are effectively shortcuts to full trigger definition with |
| 133 | +sensible defaults. |
| 134 | + |
| 135 | +The `cronut.trigger/builder` function supports the full set of Quartz configuration triggers: |
| 136 | + |
| 137 | +````clojure |
| 138 | +;; interval |
| 139 | +(cronut.trigger/builder {:type :simple |
| 140 | + :interval 3000 |
| 141 | + :repeat :forever |
| 142 | + :identity ["trigger-two" "test"] |
| 143 | + :description "sample simple trigger" |
| 144 | + :start #inst "2019-01-01T00:00:00.000-00:00" |
| 145 | + :end #inst "2019-02-01T00:00:00.000-00:00" |
| 146 | + :misfire :ignore |
| 147 | + :priority 5}) |
| 148 | + |
| 149 | +;;cron |
| 150 | +(cronut.trigger/builder {:type :cron |
| 151 | + :cron "*/6 * * * * ?" |
| 152 | + :identity ["trigger-five" "test"] |
| 153 | + :description "sample cron trigger" |
| 154 | + :start #inst "2018-01-01T00:00:00.000-00:00" |
| 155 | + :end #inst "2029-02-01T00:00:00.000-00:00" |
| 156 | + :time-zone "Australia/Melbourne" |
| 157 | + :misfire :fire-and-proceed |
| 158 | + :priority 4}) |
| 159 | +```` |
| 160 | + |
| 161 | +## Concurrent execution |
| 162 | + |
| 163 | +### Global concurrent execution |
| 164 | + |
| 165 | +Set `:concurrent-execution-disallowed?` on the scheduler to disable concurrent execution of all jobs. |
| 166 | + |
| 167 | +### Job-specific concurrent execution |
| 168 | + |
| 169 | +Set `:disallow-concurrent-execution?` on a specific job to disable concurrent execution of that job only. |
| 170 | + |
| 171 | +### Misfire configuration |
| 172 | + |
| 173 | +If you disable concurrent job execution ensure you understand Quartz Misfire options and remember to set |
| 174 | +`org.quartz.jobStore.misfireThreshold=[some ms value]` in your quartz.properties file. See Quartz documentation for more |
| 175 | +information. |
| 176 | + |
| 177 | +See our test-resources/config.edn and test-resources/org/quartz/quartz.properties for examples of misfire threshold and |
| 178 | +behaviour configuration. |
| 179 | + |
| 180 | +# Example system |
| 181 | + |
| 182 | +See: integration test source: [test/cronut/integration-test.clj](test/cronut/integration_test.clj). |
| 183 | + |
| 184 | +````clojure |
| 185 | +(ns cronut.integration-test |
| 186 | + (:require [clojure.core.async :as async] |
| 187 | + [clojure.tools.logging :as log] |
| 188 | + [cronut :as cronut] |
| 189 | + [cronut.trigger :as trigger]) |
| 190 | + (:import (java.util UUID) |
| 191 | + (org.quartz Job))) |
| 192 | + |
| 193 | +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallowConcurrentExecution?] |
| 194 | + Job |
| 195 | + (execute [this _job-context] |
| 196 | + (log/info "Defrecord Impl:" this))) |
| 197 | + |
| 198 | +(def reify-job (reify Job |
| 199 | + (execute [_this _job-context] |
| 200 | + (let [rand-id (str (UUID/randomUUID))] |
| 201 | + (log/info rand-id "Reified Impl (Job Delay 7s)") |
| 202 | + (async/<!! (async/timeout 7000)) |
| 203 | + (log/info rand-id "Finished"))))) |
| 204 | + |
| 205 | +;(do (require '[cronut.integration-test :as it]) |
| 206 | +; (it/test-system)) |
| 207 | +(defn test-system |
| 208 | + [] |
| 209 | + (let [scheduler (cronut/scheduler {:concurrent-execution-disallowed? true})] |
| 210 | + (cronut/clear scheduler) |
| 211 | + |
| 212 | + (async/<!! (async/timeout 2000)) |
| 213 | + |
| 214 | + (log/info "scheduling defrecord job on 1s interval") |
| 215 | + (cronut/schedule-job scheduler |
| 216 | + (trigger/interval 1000) |
| 217 | + (map->TestDefrecordJobImpl {:identity ["name1" "group2"] |
| 218 | + :description "test job" |
| 219 | + :recover? true |
| 220 | + :durable? false})) |
| 221 | + |
| 222 | + ;; demonstrate scheduler can start with jobs, and jobs can start after scheduler |
| 223 | + (cronut/start scheduler) |
| 224 | + |
| 225 | + (async/<!! (async/timeout 2000)) |
| 226 | + |
| 227 | + ;; demonstrates concurrency disallowed (every second job runs, 10s interval between jobs that should run every 5s) |
| 228 | + (log/info "scheduling reify/7s/no-misfire job on 5s interval") |
| 229 | + (cronut/schedule-job scheduler |
| 230 | + (trigger/builder {:type :cron |
| 231 | + :cron "*/5 * * * * ?" |
| 232 | + :misfire :do-nothing}) |
| 233 | + reify-job) |
| 234 | + |
| 235 | + (async/<!! (async/timeout 15000)) |
| 236 | + |
| 237 | + (log/info "deleting job group2.name1") |
| 238 | + (cronut/delete-job scheduler "name1" "group2") |
| 239 | + |
| 240 | + (async/<!! (async/timeout 15000)) |
| 241 | + |
| 242 | + (cronut/shutdown scheduler))) |
| 243 | +```` |
| 244 | + |
| 245 | +# License |
| 246 | + |
| 247 | +Distributed under the Apache 2.0 License. |
| 248 | + |
| 249 | +Copyright (c) [Factor House](https://factorhouse.io) |
0 commit comments