diff --git a/cronut-integrant/README.md b/cronut-integrant/README.md new file mode 100644 index 0000000..0724aaf --- /dev/null +++ b/cronut-integrant/README.md @@ -0,0 +1,439 @@ +# Cronut-Integrant: Integrant bindings for Cronut + +[![Cronut Test](https://github.com/factorhouse/cronut/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/factorhouse/cronut/actions/workflows/ci.yml) +[![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-integrant.svg)](https://clojars.org/io.factorhouse/cronut-integrant) + +# Summary + +Cronut-Integrant provides bindings for [Cronut](https://github.com/factorhouse/cronut) +to [Integrant](https://github.com/weavejester/integrant), the DI micro-framework. + +## Related Projects + +| Project | Desription | Clojars Project | +|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| [cronut](https://github.com/factorhouse/cronut) | Cronut with [Jakarta](https://en.wikipedia.org/wiki/Jakarta_EE) support (Primary) | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-javax.svg)](https://clojars.org/io.factorhouse/cronut) | +| [cronut-javax](https://github.com/factorhouse/cronut-javax) | Cronut with [Javax](https://jakarta.ee/blogs/javax-jakartaee-namespace-ecosystem-progress/) support (Legacy) | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-javax.svg)](https://clojars.org/io.factorhouse/cronut-javax) | + +# Contents + +- [Configuration](#configuration) + * [`:cronut/scheduler` definition](#cronutscheduler-definition) + + [Scheduler example](#scheduler-example) + * [`:job` definition](#job-definition) + + [Job example](#job-example) + * [`:trigger` definition](#trigger-definition) + + [`:trigger` tagged literals](#trigger-tagged-literals) + - [`#cronut/cron`: Simple Cron Scheduling](#cronutcron-simple-cron-scheduling) + - [`#cronut/interval`: Simple Interval Scheduling](#cronutinterval-simple-interval-scheduling) + - [`#cronut/trigger`: Full trigger definition](#cronuttrigger-full-trigger-definition) + * [Concurrent execution](#concurrent-execution) + + [Global concurrent execution](#global-concurrent-execution) + + [Job-specific concurrent execution](#job-specific-concurrent-execution) + + [Misfire configuration](#misfire-configuration) +- [System initialization](#system-initialization) +- [Example system](#example-system) + * [Configuration](#configuration-1) + * [Job definitions](#job-definitions) + * [Helper functions](#helper-functions) + * [Putting it together](#putting-it-together) + + [Starting the system](#starting-the-system) + + [Logs of the running system](#logs-of-the-running-system) + + [Stopping the system](#stopping-the-system) +- [License](#license) + +# Configuration + +A quartz `scheduler` runs a `job` on a schedule defined by a `trigger`. + +## `:cronut/scheduler` definition + +Cronut provides access to the Quartz Scheduler, exposed via Integrant with `:cronut/scheduler` + +The scheduler supports the following fields: + +1. `:schedule`: (required) - a sequence of 'items' to schedule, each being a map containing a :job and :trigger +2. `:concurrent-execution-disallowed?`: (optional, default false) - run all jobs with @DisableConcurrentExecution +3. `:update-check?`: (optional, default false) - check for Quartz updates on system startup + +### Scheduler example + +````clojure +:cronut/scheduler {:schedule [{:job #ig/ref :test.job/two + :trigger #cronut/interval 3500} + {:job #ig/ref :test.job/two + :trigger #cronut/cron "*/8 * * * * ?" + :misfire :do-nothing}] + :concurrent-execution-disallowed? true} +```` + +## `:job` definition + +The `:job` in every scheduled item must implement the org.quartz.Job interface + +The expectation being that every 'job' in your Integrant system will reify that interface, either directly via `reify` +or by returning a `defrecord` that implements the interface. e.g. + +````clojure +(defmethod ig/init-key :test.job/one + [_ config] + (reify Job + (execute [this job-context] + (log/info "Reified Impl:" config)))) + +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep] + Job + (execute [this job-context] + (log/info "Defrecord Impl:" this))) + +(defmethod ig/init-key :test.job/two + [_ config] + (map->TestDefrecordJobImpl config)) +```` + +Cronut supports further Quartz configuration of jobs (identity, description, recovery, and durability) by expecting +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 +them), however if you do want that control you will likely use the `defrecord` approach as opposed to `reify`. + +Concurrent execution can be controlled on a per-job bases with the `disallow-concurrent-execution?` flag. + +### Job example + +````clojure +:test.job/two {:identity ["job-two" "test"] + :description "test job" + :recover? true + :durable? false + :disallow-concurrent-execution? true + :dep-one #ig/ref :dep/one + :dep-two #ig/ref :test.job/one} +```` + +## `:trigger` definition + +The `:trigger` in every scheduled item must resolve to an org.quartz.Trigger of some variety or another, to ease that +resolution Cronut provides the following tagged literals: + +### `:trigger` tagged literals + +#### `#cronut/cron`: Simple Cron Scheduling + +A job is scheduled to run on a cron by using the `#cronut/cron` tagged literal followed by a valid cron expression + +The job will start immediately when the system is initialized, and runs in the default system time-zone + +````clojure +:trigger #cronut/cron "*/8 * * * * ?" +```` + +#### `#cronut/interval`: Simple Interval Scheduling + +A job is scheduled to run periodically by using the `#cronut/interval` tagged literal followed by a milliseconds value + +````clojure +:trigger #cronut/interval 3500 +```` + +#### `#cronut/trigger`: Full trigger definition + +Both `#cronut/cron` and `#cronut/interval` are effectively shortcuts to full trigger definition with sensible defaults. + +The `#cronut/trigger` tagged literal supports the full set of Quartz configuration triggers: + +````clojure +;; interval +:trigger #cronut/trigger {:type :simple + :interval 3000 + :repeat :forever + :identity ["trigger-two" "test"] + :description "sample simple trigger" + :start #inst "2019-01-01T00:00:00.000-00:00" + :end #inst "2019-02-01T00:00:00.000-00:00" + :misfire :ignore + :priority 5} + +;;cron +:trigger #cronut/trigger {:type :cron + :cron "*/6 * * * * ?" + :identity ["trigger-five" "test"] + :description "sample cron trigger" + :start #inst "2018-01-01T00:00:00.000-00:00" + :end #inst "2029-02-01T00:00:00.000-00:00" + :time-zone "Australia/Melbourne" + :misfire :fire-and-proceed + :priority 4} +```` + +## Concurrent execution + +### Global concurrent execution + +Set `:concurrent-execution-disallowed?` on the scheduler to disable concurrent execution of all jobs. + +### Job-specific concurrent execution + +Set `:disallow-concurrent-execution?` on a specific job to disable concurrent execution of that job only. + +### Misfire configuration + +If you disable concurrent job execution ensure you understand Quartz Misfire options and remember to set +`org.quartz.jobStore.misfireThreshold=[some ms value]` in your quartz.properties file. See Quartz documentation for more +information. + +See our test-resources/config.edn and test-resources/org/quartz/quartz.properties for examples of misfire threshold and +behaviour configuration. + +# System initialization + +When initializing an Integrant system you will need to provide the Cronut data readers. + +See: `cronut/data-readers` for convenience. + +````clojure +(def data-readers + {'cronut/trigger cronut/trigger-builder + 'cronut/cron cronut/shortcut-cron + 'cronut/interval cronut/shortcut-interval}) +```` + +e.g. + +````clojure +(defn init-system + "Convenience for starting integrant systems with cronut data-readers" + ([config] + (init-system config nil)) + ([config readers] + (ig/init (ig/read-string {:readers (merge cronut/data-readers readers)} config)))) +```` + +# Example system + +This repository contains an example system composed of of integratant configuration, job definitions, and helper +functions. + +## Configuration + +Integrant configuration source: [dev-resources/config.edn](dev-resources/config.edn). + +````clojure +{:dep/one {:a 1} + + :test.job/one {:dep-one #ig/ref :dep/one} + + :test.job/two {:identity ["name1" "group2"] + :description "test job" + :recover? true + :durable? false + :dep-one #ig/ref :dep/one + :dep-two #ig/ref :test.job/one} + + :test.job/three {} + + :cronut/scheduler {:update-check? false + :concurrent-execution-disallowed? true + :schedule [;; basic interval + {:job #ig/ref :test.job/one + :trigger #cronut/trigger {:type :simple + :interval 2 + :time-unit :seconds + :repeat :forever}} + + ;; full interval + {:job #ig/ref :test.job/two + :trigger #cronut/trigger {:type :simple + :interval 3000 + :repeat :forever + :identity ["trigger-two" "test"] + :description "test trigger" + :start #inst "2019-01-01T00:00:00.000-00:00" + :end #inst "2019-02-01T00:00:00.000-00:00" + :priority 5}} + + ;; shortcut interval + {:job #ig/ref :test.job/two + :trigger #cronut/interval 3500} + + ;; basic cron + {:job #ig/ref :test.job/two + :trigger #cronut/trigger {:type :cron + :cron "*/4 * * * * ?"}} + + ;; full cron + {:job #ig/ref :test.job/two + :trigger #cronut/trigger {:type :cron + :cron "*/6 * * * * ?" + :identity ["trigger-five" "test"] + :description "another-test trigger" + :start #inst "2018-01-01T00:00:00.000-00:00" + :end #inst "2029-02-01T00:00:00.000-00:00" + :time-zone "Australia/Melbourne" + :priority 4}} + + ;; shortcut cron + {:job #ig/ref :test.job/two + :trigger #cronut/cron "*/8 * * * * ?"} + + ;; Note: This job misfires because it takes 7 seconds to run, but runs every 5 seconds, and isn't allowed to run concurrently with {:disallowConcurrentExecution? true} + ;; So every second job fails to run, and is just ignored with the :do-nothing :misfire rule + {:job #ig/ref :test.job/three + :trigger #cronut/trigger {:type :cron + :cron "*/5 * * * * ?" + :misfire :do-nothing}}]}} + +```` + +## Job definitions + +Job definitions source: [test/cronut/integration-test.clj](test/cronut/integration_test.clj) + +```clojure +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallow-concurrent-execution?] + Job + (execute [this _job-context] + (log/info "Defrecord Impl:" this))) + +(defmethod ig/init-key :dep/one + [_ config] + config) + +(defmethod ig/init-key :test.job/one + [_ config] + (reify Job + (execute [_this _job-context] + (log/info "Reified Impl:" config)))) + +(defmethod ig/init-key :test.job/two + [_ config] + (map->TestDefrecordJobImpl config)) + +(defmethod ig/init-key :test.job/three + [_ config] + (reify Job + (execute [_this _job-context] + (let [rand-id (str (UUID/randomUUID))] + (log/info rand-id "Reified Impl (Job Delay 7s):" config) + (async/ +{:dep/one {:a 1}, + :test.job/one #object[cronut.integration_test$eval13104$fn$reify__13106 + 0x45425cf3 + "cronut.integration_test$eval13104$fn$reify__13106@45425cf3"], + :test.job/three #object[cronut.integration_test$eval13115$fn$reify__13117 + 0x7527011a + "cronut.integration_test$eval13115$fn$reify__13117@7527011a"], + :test.job/two #cronut.integration_test.TestDefrecordJobImpl{:identity ["test-group" "test-name"], + :description "test job", + :recover? true, + :durable? false, + :test-dep nil, + :disallow-concurrent-execution? nil, + :dep-one {:a 1}, + :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 + 0x45425cf3 + "cronut.integration_test$eval13104$fn$reify__13106@45425cf3"]}, + :cronut/scheduler #object[org.quartz.impl.StdScheduler 0x59a18142 "org.quartz.impl.StdScheduler@59a18142"]} +16:29:39.368 INFO [CronutScheduler_Worker-4] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:40.005 INFO [CronutScheduler_Worker-5] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:40.005 INFO [CronutScheduler_Worker-6] cronut.integration-test – 3979b197-5683-47a9-a267-dcaded343697 Reified Impl (Job Delay 7s): {} +16:29:40.006 INFO [CronutScheduler_Worker-1] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:40.876 INFO [CronutScheduler_Worker-2] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:41.368 INFO [CronutScheduler_Worker-3] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:42.004 INFO [CronutScheduler_Worker-4] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:43.364 INFO [CronutScheduler_Worker-5] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:44.007 INFO [CronutScheduler_Worker-1] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:44.375 INFO [CronutScheduler_Worker-2] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:45.368 INFO [CronutScheduler_Worker-3] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:47.011 INFO [CronutScheduler_Worker-6] cronut.integration-test – 3979b197-5683-47a9-a267-dcaded343697 Finished +16:29:47.368 INFO [CronutScheduler_Worker-4] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:47.875 INFO [CronutScheduler_Worker-5] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:48.008 INFO [CronutScheduler_Worker-1] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:48.010 INFO [CronutScheduler_Worker-2] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:48.011 INFO [CronutScheduler_Worker-3] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:49.370 INFO [CronutScheduler_Worker-6] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:50.004 INFO [CronutScheduler_Worker-4] cronut.integration-test – 299b73c8-97ad-4d85-848f-35960ced6362 Reified Impl (Job Delay 7s): {} +16:29:51.368 INFO [CronutScheduler_Worker-5] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:51.368 INFO [CronutScheduler_Worker-1] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:52.004 INFO [CronutScheduler_Worker-2] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:53.366 INFO [CronutScheduler_Worker-3] cronut.integration-test – Reified Impl: {:dep-one {:a 1}} +16:29:54.007 INFO [CronutScheduler_Worker-6] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +16:29:54.874 INFO [CronutScheduler_Worker-5] cronut.integration-test – Defrecord Impl: #cronut.integration_test.TestDefrecordJobImpl{:identity [test-group test-name], :description test job, :recover? true, :durable? false, :test-dep nil, :disallow-concurrent-execution? nil, :dep-one {:a 1}, :dep-two #object[cronut.integration_test$eval13104$fn$reify__13106 0x45425cf3 cronut.integration_test$eval13104$fn$reify__13106@45425cf3]} +``` + +### Stopping the system + +```clojure +(test/halt-system *1) +``` + +# License + +Distributed under the Apache 2.0 License. + +Copyright (c) [Factor House](https://factorhouse.io) diff --git a/cronut-integrant/dev-resources/config.edn b/cronut-integrant/dev-resources/config.edn index 7e60da1..446127f 100644 --- a/cronut-integrant/dev-resources/config.edn +++ b/cronut-integrant/dev-resources/config.edn @@ -2,7 +2,7 @@ :test.job/one {:dep-one #ig/ref :dep/one} - :test.job/two {:identity ["test-group" "test-name"] + :test.job/two {:identity ["name1" "group2"] :description "test job" :recover? true :durable? false diff --git a/cronut-integrant/project.clj b/cronut-integrant/project.clj index 6333d81..e2c7d6e 100644 --- a/cronut-integrant/project.clj +++ b/cronut-integrant/project.clj @@ -7,17 +7,17 @@ :license {:name "Apache 2.0 License" :url "https://github.com/factorhosue/slipway/blob/main/LICENSE"} - :plugins [[dev.weavejester/lein-cljfmt "0.13.1"]] + :plugins [[dev.weavejester/lein-cljfmt "0.13.4"]] - :dependencies [[org.clojure/clojure "1.12.1"] + :dependencies [[org.clojure/clojure "1.12.3"] [org.clojure/tools.logging "1.3.0"]] :profiles {:dev {:resource-paths ["dev-resources"] :dependencies [[integrant "0.13.1"] - [ch.qos.logback/logback-classic "1.5.18"] + [ch.qos.logback/logback-classic "1.5.19"] [org.slf4j/slf4j-api "2.0.17"] [org.clojure/core.async "1.8.741"] - [clj-kondo "2025.06.05"]]} + [clj-kondo "2025.09.22" :exclusions [org.clojure/tools.reader]]]} :jakarta {:dependencies [[io.factorhouse/cronut "1.0.0"]]} :javax {:dependencies [[io.factorhouse/cronut-javax "1.0.0"]]} :smoke {:pedantic? :abort}} diff --git a/cronut-integrant/test/cronut/integration_test.clj b/cronut-integrant/test/cronut/integration_test.clj index fc1cadd..4d0787b 100644 --- a/cronut-integrant/test/cronut/integration_test.clj +++ b/cronut-integrant/test/cronut/integration_test.clj @@ -7,7 +7,7 @@ (:import (java.util UUID) (org.quartz Job))) -(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallowConcurrentExecution?] +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallow-concurrent-execution?] Job (execute [this _job-context] (log/info "Defrecord Impl:" this))) @@ -36,10 +36,15 @@ (log/info rand-id "Finished"))))) (defn init-system - "Convenience for starting integrant systems with cronut data-readers" + "Example of starting integrant cronut systems with data-readers" ([] (init-system (slurp (io/resource "config.edn")))) ([config] (init-system config nil)) ([config readers] - (ig/init (ig/read-string {:readers (merge cig/data-readers readers)} config)))) \ No newline at end of file + (ig/init (ig/read-string {:readers (merge cig/data-readers readers)} config)))) + +(defn halt-system + "Example of stopping integrant cronut systems" + [system] + (ig/halt! system)) \ No newline at end of file diff --git a/cronut-javax/README.md b/cronut-javax/README.md new file mode 100644 index 0000000..326b34c --- /dev/null +++ b/cronut-javax/README.md @@ -0,0 +1,249 @@ +# Cronut Javax: A Clojure Companion to Quartz (Legacy Javax Support) + +[![Cronut Test](https://github.com/factorhouse/cronut/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/factorhouse/cronut/actions/workflows/ci.yml) +[![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut.svg)](https://clojars.org/io.factorhouse/cronut) + +# Summary + +[Cronut](https://github.com/factorhouse/cronut) provides a data-first [Clojure](https://clojure.org/) wrapper +for [Quartz Scheduler](https://github.com/quartz-scheduler/quartz) version `2.4.0`, compatible +with [Javax](https://jakarta.ee/blogs/javax-jakartaee-namespace-ecosystem-progress/). + +Cronut supports **in-memory** scheduling of jobs within a single JVM. JDBC and distributed jobstore are not supported. + +## Related Projects + +| Project | Desription | Clojars Project | +|---------------------------------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| [cronut](https://github.com/factorhouse/cronut) | Cronut with [Jakarta](https://en.wikipedia.org/wiki/Jakarta_EE) support | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-javax.svg)](https://clojars.org/io.factorhouse/cronut-javax) | +| [cronut-integrant](https://github.com/factorhouse/cronut-integrant) | [Integrant](https://github.com/weavejester/integrant) bindings for Cronut | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-integrant.svg)](https://clojars.org/io.factorhouse/cronut-integrant) | + +# Contents + +# Usage + +A quartz `scheduler` runs a `job` on a schedule defined by a `trigger`. + +## Scheduler + +Cronut provides access to the Quartz Scheduler, exposed via the `cronut/scheduler` function. + +Create a scheduler with the following configuration: + +1. `:concurrent-execution-disallowed?`: (optional, default false) - run all jobs with @DisableConcurrentExecution +2. `:update-check?`: (optional, default false) - check for Quartz updates on system startup. + +````clojure +(cronut/scheduler {:concurrent-execution-disallowed? true + :update-check? false}) +```` + +### Scheduler lifecycle + +Once created, you can: + +* `cronut/start`: start the scheduler +* `cronut/start-delayed`: start the scheduler with a delay +* `cronut/standy`: temporarily halt the firing of triggers by the scheduler +* `cronut/shutdown`: stop the scheduler +* `cronut/pause-all`: pause all triggers +* `cronut/resume-all`: resume all triggers +* `cronut/clear`: clear all scheduling data of jobs and triggers + +### Scheduling jobs + +To schedule jobs, you can + +* `cronut/schedule-job`: schedule a single job +* `cronut/schedule-jobs`: schedule multiple jobs at once +* `cronut/pause-job`: pause a job +* `cronut/resume-job`: resume a paused job +* `cronut/unschedule-job`: remove a trigger from the scheduler +* `cronut/delete-job`: remove a job and all associated triggers from the scheduler +* `cronut/pause-trigger`: pause a trigger +* `cronut/resume-trigger`: resume a paused trigger + +## Jobs + +Each cronut job must implement the `org.quartz.Job` interface. + +The expectation being that every job will reify that interface either directly via `reify` or by returning a `defrecord` +that implements the interface. + +Cronut supports further Quartz configuration of jobs (identity, description, recovery, and durability) by expecting +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 +them), however if you do want that control you will likely use the `defrecord` approach as opposed to `reify`. + +Concurrent execution can be controlled on a per-job bases with the `disallow-concurrent-execution?` flag. + +### Job example + +````clojure +(defrecord TestDefrecordJobImpl [identity description recover? durable?] + Job + (execute [this _job-context] + (log/info "Defrecord Impl:" this))) + + +(let [scheduler (cronut/scheduler {:concurrent-execution-disallowed? true + :update-check? false}) + defrecord-job (map->TestDefrecordJobImpl {:identity ["name1" "group2"] + :description "test job" + :recover? true + :durable? false}) + reify-job (reify Job + (execute [_this _job-context] + (let [rand-id (str (UUID/randomUUID))] + (log/info rand-id "Reified Impl"))))] + + (cronut/schedule-job scheduler (trigger/interval 1000) defrecord-job) + + (cronut/schedule-job scheduler + (trigger/builder {:type :cron + :cron "*/5 * * * * ?" + :misfire :do-nothing}) + reify-job)) +```` + +## Triggers + +Cronut triggers are of type `org.quartz.Trigger`, the following functions are provided to simplify trigger creation: + +#### `cronut.trigger/cron`: Simple Cron Scheduling + +A job is scheduled to run on a cron by using the `cronut.trigger/cron` function with a valid cron expression. + +The job will start immediately when the system is initialized, and runs in the default system time-zone + +````clojure +(cronut.trigger/cron "*/8 * * * * ?") +```` + +#### `cronut.trigger/interval`: Simple Interval Scheduling + +A job is scheduled to run periodically by using the `cronut.trigger/interval` function with a milliseconds value + +````clojure +(cronut.trigger/interval 3500) +```` + +#### `cronut.trigger/builder`: Full trigger definition + +Both `cronut.trigger/cron` and `cronut.trigger/interval` are effectively shortcuts to full trigger definition with +sensible defaults. + +The `cronut.trigger/builder` function supports the full set of Quartz configuration triggers: + +````clojure +;; interval +(cronut.trigger/builder {:type :simple + :interval 3000 + :repeat :forever + :identity ["trigger-two" "test"] + :description "sample simple trigger" + :start #inst "2019-01-01T00:00:00.000-00:00" + :end #inst "2019-02-01T00:00:00.000-00:00" + :misfire :ignore + :priority 5}) + +;;cron +(cronut.trigger/builder {:type :cron + :cron "*/6 * * * * ?" + :identity ["trigger-five" "test"] + :description "sample cron trigger" + :start #inst "2018-01-01T00:00:00.000-00:00" + :end #inst "2029-02-01T00:00:00.000-00:00" + :time-zone "Australia/Melbourne" + :misfire :fire-and-proceed + :priority 4}) +```` + +## Concurrent execution + +### Global concurrent execution + +Set `:concurrent-execution-disallowed?` on the scheduler to disable concurrent execution of all jobs. + +### Job-specific concurrent execution + +Set `:disallow-concurrent-execution?` on a specific job to disable concurrent execution of that job only. + +### Misfire configuration + +If you disable concurrent job execution ensure you understand Quartz Misfire options and remember to set +`org.quartz.jobStore.misfireThreshold=[some ms value]` in your quartz.properties file. See Quartz documentation for more +information. + +See our test-resources/config.edn and test-resources/org/quartz/quartz.properties for examples of misfire threshold and +behaviour configuration. + +# Example system + +See: integration test source: [test/cronut/integration-test.clj](test/cronut/integration_test.clj). + +````clojure +(ns cronut.integration-test + (:require [clojure.core.async :as async] + [clojure.tools.logging :as log] + [cronut :as cronut] + [cronut.trigger :as trigger]) + (:import (java.util UUID) + (org.quartz Job))) + +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallowConcurrentExecution?] + Job + (execute [this _job-context] + (log/info "Defrecord Impl:" this))) + +(def reify-job (reify Job + (execute [_this _job-context] + (let [rand-id (str (UUID/randomUUID))] + (log/info rand-id "Reified Impl (Job Delay 7s)") + (async/TestDefrecordJobImpl {:identity ["name1" "group2"] + :description "test job" + :recover? true + :durable? false})) + + ;; demonstrate scheduler can start with jobs, and jobs can start after scheduler + (cronut/start scheduler) + + (async/ (-> (JobBuilder/newJob (if (or concurrent-execution-disallowed? ;; global concurrency disallowed flag - disallow-concurrent-execution?) ;; job specific concurrency dissalowed flag + disallow-concurrent-execution?) ;; job specific concurrency disallowed flag SerialProxyJob ProxyJob)) (.setJobData (JobDataMap. {"job-impl" job}))) (seq identity) (.withIdentity (first identity) (second identity)) diff --git a/cronut-javax/src/cronut/trigger.clj b/cronut-javax/src/cronut/trigger.clj index bab3887..e52963e 100644 --- a/cronut-javax/src/cronut/trigger.clj +++ b/cronut-javax/src/cronut/trigger.clj @@ -53,13 +53,11 @@ (defmethod builder :simple [config] - (.withSchedule ^TriggerBuilder (base-builder config) - (simple-schedule config))) + (.withSchedule ^TriggerBuilder (base-builder config) (simple-schedule config))) (defmethod builder :cron [config] - (.withSchedule ^TriggerBuilder (base-builder config) - (cron-schedule config))) + (.withSchedule ^TriggerBuilder (base-builder config) (cron-schedule config))) (defn interval "Trigger immediately, at an interval-ms, run forever (well that's optimistic but you get the idea)" @@ -70,6 +68,7 @@ :repeat :forever})) (defn cron + "Trigger on a schedule defined by the cron expression" [cron] (builder {:type :cron - :cron cron})) + :cron cron})) \ No newline at end of file diff --git a/cronut-javax/test/cronut/integration_test.clj b/cronut-javax/test/cronut/integration_test.clj index 2756a10..56adce9 100644 --- a/cronut-javax/test/cronut/integration_test.clj +++ b/cronut-javax/test/cronut/integration_test.clj @@ -30,7 +30,7 @@ (log/info "scheduling defrecord job on 1s interval") (cronut/schedule-job scheduler (trigger/interval 1000) - (map->TestDefrecordJobImpl {:identity ["test-group" "test-name"] + (map->TestDefrecordJobImpl {:identity ["name1" "group2"] :description "test job" :recover? true :durable? false})) @@ -50,8 +50,8 @@ (async/TestDefrecordJobImpl {:identity ["test-group" "test-name"] + (map->TestDefrecordJobImpl {:identity ["name1" "group2"] :description "test job" :recover? true :durable? false}))] diff --git a/cronut/README.md b/cronut/README.md new file mode 100644 index 0000000..d664390 --- /dev/null +++ b/cronut/README.md @@ -0,0 +1,249 @@ +# Cronut: A Clojure Companion to Quartz + +[![Cronut Test](https://github.com/factorhouse/cronut/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/factorhouse/cronut/actions/workflows/ci.yml) +[![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut.svg)](https://clojars.org/io.factorhouse/cronut) + +# Summary + +[Cronut](https://github.com/factorhouse/cronut) provides a data-first [Clojure](https://clojure.org/) wrapper +for [Quartz Scheduler](https://github.com/quartz-scheduler/quartz) version `2.5.0`, compatible +with [Jakarta](https://en.wikipedia.org/wiki/Jakarta_EE). + +Cronut supports **in-memory** scheduling of jobs within a single JVM. JDBC and distributed jobstore are not supported. + +## Related Projects + +| Project | Desription | Clojars Project | +|---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| [cronut-javax](https://github.com/factorhouse/cronut-javax) | Cronut with [Javax](https://jakarta.ee/blogs/javax-jakartaee-namespace-ecosystem-progress/) support (Legacy) | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-javax.svg)](https://clojars.org/io.factorhouse/cronut-javax) | +| [cronut-integrant](https://github.com/factorhouse/cronut-integrant) | [Integrant](https://github.com/weavejester/integrant) bindings for Cronut | [![Clojars Project](https://img.shields.io/clojars/v/io.factorhouse/cronut-integrant.svg)](https://clojars.org/io.factorhouse/cronut-integrant) | + +# Contents + +# Usage + +A quartz `scheduler` runs a `job` on a schedule defined by a `trigger`. + +## Scheduler + +Cronut provides access to the Quartz Scheduler, exposed via the `cronut/scheduler` function. + +Create a scheduler with the following configuration: + +1. `:concurrent-execution-disallowed?`: (optional, default false) - run all jobs with @DisableConcurrentExecution +2. `:update-check?`: (optional, default false) - check for Quartz updates on system startup. + +````clojure +(cronut/scheduler {:concurrent-execution-disallowed? true + :update-check? false}) +```` + +### Scheduler lifecycle + +Once created, you can: + +* `cronut/start`: start the scheduler +* `cronut/start-delayed`: start the scheduler with a delay +* `cronut/standy`: temporarily halt the firing of triggers by the scheduler +* `cronut/shutdown`: stop the scheduler +* `cronut/pause-all`: pause all triggers +* `cronut/resume-all`: resume all triggers +* `cronut/clear`: clear all scheduling data of jobs and triggers + +### Scheduling jobs + +To schedule jobs, you can + +* `cronut/schedule-job`: schedule a single job +* `cronut/schedule-jobs`: schedule multiple jobs at once +* `cronut/pause-job`: pause a job +* `cronut/resume-job`: resume a paused job +* `cronut/unschedule-job`: remove a trigger from the scheduler +* `cronut/delete-job`: remove a job and all associated triggers from the scheduler +* `cronut/pause-trigger`: pause a trigger +* `cronut/resume-trigger`: resume a paused trigger + +## Jobs + +Each cronut job must implement the `org.quartz.Job` interface. + +The expectation being that every job will reify that interface either directly via `reify` or by returning a `defrecord` +that implements the interface. + +Cronut supports further Quartz configuration of jobs (identity, description, recovery, and durability) by expecting +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 +them), however if you do want that control you will likely use the `defrecord` approach as opposed to `reify`. + +Concurrent execution can be controlled on a per-job bases with the `disallow-concurrent-execution?` flag. + +### Job example + +````clojure +(defrecord TestDefrecordJobImpl [identity description recover? durable?] + Job + (execute [this _job-context] + (log/info "Defrecord Impl:" this))) + + +(let [scheduler (cronut/scheduler {:concurrent-execution-disallowed? true + :update-check? false}) + defrecord-job (map->TestDefrecordJobImpl {:identity ["name1" "group2"] + :description "test job" + :recover? true + :durable? false}) + reify-job (reify Job + (execute [_this _job-context] + (let [rand-id (str (UUID/randomUUID))] + (log/info rand-id "Reified Impl"))))] + + (cronut/schedule-job scheduler (trigger/interval 1000) defrecord-job) + + (cronut/schedule-job scheduler + (trigger/builder {:type :cron + :cron "*/5 * * * * ?" + :misfire :do-nothing}) + reify-job)) +```` + +## Triggers + +Cronut triggers are of type `org.quartz.Trigger`, the following functions are provided to simplify trigger creation: + +#### `cronut.trigger/cron`: Simple Cron Scheduling + +A job is scheduled to run on a cron by using the `cronut.trigger/cron` function with a valid cron expression. + +The job will start immediately when the system is initialized, and runs in the default system time-zone + +````clojure +(cronut.trigger/cron "*/8 * * * * ?") +```` + +#### `cronut.trigger/interval`: Simple Interval Scheduling + +A job is scheduled to run periodically by using the `cronut.trigger/interval` function with a milliseconds value + +````clojure +(cronut.trigger/interval 3500) +```` + +#### `cronut.trigger/builder`: Full trigger definition + +Both `cronut.trigger/cron` and `cronut.trigger/interval` are effectively shortcuts to full trigger definition with +sensible defaults. + +The `cronut.trigger/builder` function supports the full set of Quartz configuration triggers: + +````clojure +;; interval +(cronut.trigger/builder {:type :simple + :interval 3000 + :repeat :forever + :identity ["trigger-two" "test"] + :description "sample simple trigger" + :start #inst "2019-01-01T00:00:00.000-00:00" + :end #inst "2019-02-01T00:00:00.000-00:00" + :misfire :ignore + :priority 5}) + +;;cron +(cronut.trigger/builder {:type :cron + :cron "*/6 * * * * ?" + :identity ["trigger-five" "test"] + :description "sample cron trigger" + :start #inst "2018-01-01T00:00:00.000-00:00" + :end #inst "2029-02-01T00:00:00.000-00:00" + :time-zone "Australia/Melbourne" + :misfire :fire-and-proceed + :priority 4}) +```` + +## Concurrent execution + +### Global concurrent execution + +Set `:concurrent-execution-disallowed?` on the scheduler to disable concurrent execution of all jobs. + +### Job-specific concurrent execution + +Set `:disallow-concurrent-execution?` on a specific job to disable concurrent execution of that job only. + +### Misfire configuration + +If you disable concurrent job execution ensure you understand Quartz Misfire options and remember to set +`org.quartz.jobStore.misfireThreshold=[some ms value]` in your quartz.properties file. See Quartz documentation for more +information. + +See our test-resources/config.edn and test-resources/org/quartz/quartz.properties for examples of misfire threshold and +behaviour configuration. + +# Example system + +See: integration test source: [test/cronut/integration-test.clj](test/cronut/integration_test.clj). + +````clojure +(ns cronut.integration-test + (:require [clojure.core.async :as async] + [clojure.tools.logging :as log] + [cronut :as cronut] + [cronut.trigger :as trigger]) + (:import (java.util UUID) + (org.quartz Job))) + +(defrecord TestDefrecordJobImpl [identity description recover? durable? test-dep disallowConcurrentExecution?] + Job + (execute [this _job-context] + (log/info "Defrecord Impl:" this))) + +(def reify-job (reify Job + (execute [_this _job-context] + (let [rand-id (str (UUID/randomUUID))] + (log/info rand-id "Reified Impl (Job Delay 7s)") + (async/TestDefrecordJobImpl {:identity ["name1" "group2"] + :description "test job" + :recover? true + :durable? false})) + + ;; demonstrate scheduler can start with jobs, and jobs can start after scheduler + (cronut/start scheduler) + + (async/ (-> (JobBuilder/newJob (if (or concurrent-execution-disallowed? ;; global concurrency disallowed flag - disallow-concurrent-execution?) ;; job specific concurrency dissalowed flag + disallow-concurrent-execution?) ;; job specific concurrency disallowed flag SerialProxyJob ProxyJob)) (.setJobData (JobDataMap. {"job-impl" job}))) (seq identity) (.withIdentity (first identity) (second identity)) diff --git a/cronut/src/cronut/trigger.clj b/cronut/src/cronut/trigger.clj index bab3887..e52963e 100644 --- a/cronut/src/cronut/trigger.clj +++ b/cronut/src/cronut/trigger.clj @@ -53,13 +53,11 @@ (defmethod builder :simple [config] - (.withSchedule ^TriggerBuilder (base-builder config) - (simple-schedule config))) + (.withSchedule ^TriggerBuilder (base-builder config) (simple-schedule config))) (defmethod builder :cron [config] - (.withSchedule ^TriggerBuilder (base-builder config) - (cron-schedule config))) + (.withSchedule ^TriggerBuilder (base-builder config) (cron-schedule config))) (defn interval "Trigger immediately, at an interval-ms, run forever (well that's optimistic but you get the idea)" @@ -70,6 +68,7 @@ :repeat :forever})) (defn cron + "Trigger on a schedule defined by the cron expression" [cron] (builder {:type :cron - :cron cron})) + :cron cron})) \ No newline at end of file diff --git a/cronut/test/cronut/integration_test.clj b/cronut/test/cronut/integration_test.clj index 2756a10..56adce9 100644 --- a/cronut/test/cronut/integration_test.clj +++ b/cronut/test/cronut/integration_test.clj @@ -30,7 +30,7 @@ (log/info "scheduling defrecord job on 1s interval") (cronut/schedule-job scheduler (trigger/interval 1000) - (map->TestDefrecordJobImpl {:identity ["test-group" "test-name"] + (map->TestDefrecordJobImpl {:identity ["name1" "group2"] :description "test job" :recover? true :durable? false})) @@ -50,8 +50,8 @@ (async/TestDefrecordJobImpl {:identity ["test-group" "test-name"] + (map->TestDefrecordJobImpl {:identity ["name1" "group2"] :description "test job" :recover? true :durable? false}))]