├── .gitignore ├── .gitmodules ├── .midje.clj ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── project.clj ├── src └── cronj │ ├── core.clj │ ├── data │ ├── scheduler.clj │ ├── tab.clj │ ├── task.clj │ └── timer.clj │ └── simulation.clj └── test ├── cronj ├── test_core.clj ├── test_scheduler.clj ├── test_simulation.clj ├── test_tab.clj ├── test_task.clj ├── test_timer.clj └── test_timetable.clj └── midje_doc ├── cronj_api.clj └── cronj_guide.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | .DS_Store 12 | .#* 13 | index.html 14 | .lein-repl-history 15 | .nrepl-port 16 | \#* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs"] 2 | path = docs 3 | url = https://github.com/zcaudate/cronj.git 4 | branch = gh-pages 5 | -------------------------------------------------------------------------------- /.midje.clj: -------------------------------------------------------------------------------- 1 | #_(change-defaults :emitter 'midje.emission.plugins.progress 2 | :print-level :print-facts 3 | ) -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | script: lein2 midje 4 | notifications: 5 | email: 6 | recipients: 7 | - z@caudate.me -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | *** v1.4.4 3 | - hara "2.1.12" 4 | 5 | *** v1.4.3 6 | - bug fix for `reschedule-task` 7 | - hara "2.1.5" 8 | 9 | *** v1.4.1 10 | - added clojure.tools.logging (thanks [pyr](https://github.com/pyr)) 11 | - hara "2.1.3" 12 | 13 | *** v1.4.0 14 | - hara "2.1.2" 15 | 16 | *** v1.2.1 17 | - hara "1.0.1" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cronj 2 | 3 | A simple to use, cron-inspired task scheduler. Please see [Immutability, time and testable task schedulers](http://z.caudate.me/immutability-time-and-task-schedulers/) for motivation. 4 | 5 | [![Build Status](https://travis-ci.org/zcaudate/cronj.png?branch=master)](https://travis-ci.org/zcaudate/cronj) 6 | 7 | ## Deprecation Notice 8 | 9 | This library has been integrated into the larger [hara](http://docs.caudate.me/hara) ecosystem. 10 | 11 | it has been renamed to `hara.io.scheduler`: 12 | - [scheduler docs](http://docs.caudate.me/hara/hara-io-scheduler.html) 13 | - see [guide](http://docs.caudate.me/hara/hara-io-scheduler.html#cronj) for how to upgrade 14 | 15 | ## Installation: 16 | 17 | In project.clj, add to dependencies: 18 | 19 | [im.chit/cronj "1.4.4"] 20 | 21 | ### Documentation 22 | 23 | See main site at: 24 | 25 | http://docs.caudate.me/cronj/ 26 | 27 | ### Contributors 28 | 29 | - [Pierre-Yves Ritschard](https://github.com/pyr) 30 | - [Vadim Platonov](https://github.com/dm3) 31 | - [Chris Zheng](https://github.com/zcaudate) 32 | 33 | ## License 34 | Copyright © 2013 Chris Zheng 35 | 36 | Distributed under the MIT License 37 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject im.chit/cronj "1.4.4" 2 | :description "A simple to use, cron-inspiried task scheduler" 3 | :url "http://github.com/zcaudate/cronj" 4 | :license {:name "The MIT License" 5 | :url "http://http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.6.0"] 7 | [org.clojure/tools.logging "0.3.0"] 8 | [clj-time "0.8.0"] 9 | [im.chit/hara.common.primitives "2.1.12"] 10 | [im.chit/hara.namespace.import "2.1.12"] 11 | [im.chit/hara.ova "2.1.12"] 12 | [im.chit/hara.concurrent.latch "2.1.12"]] 13 | :profiles {:dev {:dependencies [[midje "1.6.3"]] 14 | :plugins [[lein-midje "3.1.3"] 15 | [lein-midje-doc "0.0.24"]]}} 16 | :documentation {:files {"docs/index" 17 | {:input "test/midje_doc/cronj_guide.clj" 18 | :title "cronj" 19 | :sub-title "task scheduling and simulation" 20 | :author "Chris Zheng" 21 | :email "z@caudate.me" 22 | :tracking "UA-31320512-2"}}}) 23 | -------------------------------------------------------------------------------- /src/cronj/core.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.core 2 | (:require [hara.ova :as ova] 3 | [hara.common.watch :as watch] 4 | [hara.namespace.import :as ns] 5 | [cronj.data.task :as tk] 6 | [cronj.data.timer :as tm] 7 | [cronj.data.scheduler :as ts])) 8 | 9 | (ns/import cronj.simulation [simulate simulate-st local-time]) 10 | (ns/import clj-time.local [local-now]) 11 | 12 | (declare install-watch cronj) 13 | 14 | (defn cronj [& args] 15 | (let [scheduler (ts/scheduler) 16 | timer (tm/timer) 17 | margs (apply hash-map args) 18 | interval (:interval margs) 19 | entries (:entries margs)] 20 | (if interval (swap! timer assoc :interval interval)) 21 | (doseq [tsce (map ts/task-entry entries)] 22 | (ts/schedule-task scheduler tsce)) 23 | (install-watch timer scheduler) 24 | {:timer timer :scheduler scheduler})) 25 | 26 | (defn- install-watch [timer tsc] 27 | (watch/add timer :time-watch 28 | (fn [_ rf _ _] 29 | (let [r @rf] 30 | (ts/signal-tick tsc (:last-check-time r)))) 31 | {:diff true 32 | :select :last-check})) 33 | 34 | ;;--------- timer functions -------------- 35 | 36 | (defn start! [cnj] (tm/start! (:timer cnj))) 37 | 38 | (defn stop! [cnj] (tm/stop! (:timer cnj))) 39 | 40 | (defn stopped? [cnj] (tm/stopped? (:timer cnj))) 41 | 42 | (defn running? [cnj] (tm/running? (:timer cnj))) 43 | 44 | (defn uptime [cnj] (tm/uptime (:timer cnj))) 45 | 46 | ;;--------- scheduler functions ----------- 47 | 48 | (defn schedule-task 49 | "Task must be created using the cronj.data.task/task constructor." 50 | [cnj task schedule & [enabled? opts]] 51 | {:pre [(tk/task? task)]} 52 | (ts/schedule-task (:scheduler cnj) task schedule enabled? opts)) 53 | 54 | (defn unschedule-task [cnj task-id] 55 | (ts/unschedule-task (:scheduler cnj) task-id)) 56 | 57 | (defn reschedule-task [cnj task-id schedule] 58 | (ts/reschedule-task (:scheduler cnj) task-id schedule)) 59 | 60 | (defn empty-tasks [cnj] 61 | (doseq [id (ts/task-ids (:scheduler cnj))] 62 | (ts/unschedule-task (:scheduler cnj) id))) 63 | 64 | (defn enable-task [cnj task-id] 65 | (ts/enable-task (:scheduler cnj) task-id)) 66 | 67 | (defn disable-task [cnj task-id] 68 | (ts/disable-task (:scheduler cnj) task-id)) 69 | 70 | (defn task-enabled? [cnj task-id] 71 | (ts/task-enabled? (:scheduler cnj) task-id)) 72 | 73 | (defn task-disabled? [cnj task-id] 74 | (ts/task-disabled? (:scheduler cnj) task-id)) 75 | 76 | ;;---------- task related functions ------- 77 | 78 | (defn get-ids [cnj] 79 | (ts/task-ids (:scheduler cnj))) 80 | 81 | (defn get-task [cnj task-id] 82 | (ts/get-task (:scheduler cnj) task-id)) 83 | 84 | (defn get-threads 85 | ([cnj] 86 | (ts/task-threads (:scheduler cnj))) 87 | ([cnj task-id] 88 | (tk/running (-> (get-task cnj task-id) :task)))) 89 | 90 | (defn exec! 91 | ([cnj task-id] 92 | (exec! cnj task-id (local-now))) 93 | ([cnj task-id dt] 94 | (let [opts (-> (get-task cnj task-id) :opts)] 95 | (exec! cnj task-id dt opts))) 96 | ([cnj task-id dt opts] 97 | (tk/exec! (-> (get-task cnj task-id) :task) dt opts))) 98 | 99 | (defn kill! 100 | ([cnj] 101 | (doseq [id (get-ids cnj)] 102 | (tk/kill-all! (-> (get-task cnj id) :task)))) 103 | ([cnj task-id] 104 | (tk/kill-all! (-> (get-task cnj task-id) :task))) 105 | ([cnj task-id tid] 106 | (tk/kill! (-> (get-task cnj task-id) :task) tid))) 107 | 108 | ;;--------- system level ---------- 109 | 110 | (defn shutdown! [cnj] 111 | (stop! cnj) 112 | (kill! cnj)) 113 | 114 | (defn restart! [cnj] 115 | (shutdown! cnj) 116 | (start! cnj)) 117 | -------------------------------------------------------------------------------- /src/cronj/data/scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.data.scheduler 2 | (:require [hara.concurrent.latch :refer [latch]] 3 | [hara.common.error :refer [suppress]] 4 | [hara.common.primitives :refer [F]] 5 | [hara.ova :as ova] 6 | [cronj.data.tab :as tab] 7 | [cronj.data.task :as tk])) 8 | 9 | (defn scheduler [] (ova/ova)) 10 | 11 | (defn set-tab-array-fn [schedule] 12 | (suppress (tab/parse-tab schedule) tab/nil-array)) 13 | 14 | (defn task-entry 15 | ([m] 16 | (let [task (tk/task (select-keys m [:id :handler :desc :pre-hook :post-hook]))] 17 | (task-entry task (:schedule m) (:enabled m) (:opts m)))) 18 | ([task schedule & [enabled? opts]] 19 | {:task task 20 | :schedule schedule 21 | :tab-array (set-tab-array-fn schedule) 22 | :enabled (if (nil? enabled?) true 23 | enabled?) 24 | :opts (or opts {}) 25 | :output (atom nil)})) 26 | 27 | (defn unschedule-task [tsc task-id] 28 | (dosync 29 | (doseq [entry (ova/select tsc [[:task :id] task-id])] 30 | (tk/kill-all! (:task entry))) 31 | (ova/remove! tsc [[:task :id] task-id]))) 32 | 33 | (defn reschedule-task [tsc task-id schedule] 34 | (dosync 35 | (ova/!> tsc [[:task :id] task-id] 36 | (merge 37 | {:schedule schedule 38 | :tab-array (set-tab-array-fn schedule)})))) 39 | 40 | (defn schedule-task 41 | ([tsc tsce] 42 | (dosync 43 | (unschedule-task tsc (:id (:task tsce))) 44 | (ova/insert! tsc tsce))) 45 | ([tsc task schedule & [enabled? opts]] 46 | (schedule-task tsc (task-entry task schedule enabled? opts)))) 47 | 48 | (defn enable-task [tsc task-id] 49 | (dosync 50 | (ova/!> tsc [[:task :id] task-id] 51 | (merge {:enabled true})))) 52 | 53 | (defn disable-task [tsc task-id] 54 | (dosync 55 | (ova/!> tsc [[:task :id] task-id] 56 | (merge{:enabled false})))) 57 | 58 | (defn task-enabled? [tsc task-id] 59 | (:enabled (first (ova/select tsc [[:task :id] task-id])))) 60 | 61 | (defn task-disabled? [tsc task-id] 62 | (not (task-enabled? tsc task-id))) 63 | 64 | (defn- set-exec-output [dt entry] 65 | (reset! (:output entry) 66 | {:id (-> entry :task :id) 67 | :dt dt 68 | :exec (tk/exec! (:task entry) 69 | (tab/truncate-ms dt) (:opts entry))})) 70 | 71 | (defn- select-tasks [tsc task-id] 72 | (if task-id 73 | (ova/select tsc [[:task :id] task-id :enabled true]) 74 | (ova/select tsc [:enabled true]))) 75 | 76 | (defn signal-tick 77 | ([tsc dt] (signal-tick tsc nil dt)) 78 | ([tsc task-id dt] 79 | (doseq [entry (select-tasks tsc task-id)] 80 | (when (tab/match-array? (tab/to-dt-array dt) (:tab-array entry)) 81 | (set-exec-output dt entry))))) 82 | 83 | (defn trigger-tick 84 | ([tsc dt] [trigger-tick tsc nil dt]) 85 | ([tsc task-id dt] 86 | (doseq [entry (select-tasks tsc task-id)] 87 | (set-exec-output dt entry)))) 88 | 89 | (defn get-task [tsc task-id] 90 | (first (ova/select tsc [[:task :id] task-id]))) 91 | 92 | (defn task-ids [tsc] 93 | (map #(:id (:task %)) (ova/selectv tsc))) 94 | 95 | (defn task-threads [tsc] 96 | (map (fn [e] 97 | (if-let [task (:task e)] 98 | {:id (:id task) 99 | :running (tk/running task)})) 100 | (ova/selectv tsc))) 101 | -------------------------------------------------------------------------------- /src/cronj/data/tab.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.data.tab 2 | (:require [clojure.string :refer [split]] 3 | [hara.common.error :refer [error suppress]] 4 | [hara.common.primitives :refer [F]] 5 | [clj-time.core :as t] 6 | [clj-time.local :as lt])) 7 | 8 | (def SCHEDULE-ELEMENTS [:second :minute :hour :day-of-week :day :month :year]) 9 | 10 | ;; There are 2 different representations of cronj tab data: 11 | ;; string: (for humans) " * 2,4 2-9 /8 ... " 12 | ;; array: (for efficiency) [ (-*) [2 4 6] (-- 2 9) (-- 8) ... ] 13 | ;; 14 | ;; :tab-str :tab-arr 15 | ;; +---------+ +---------+ 16 | ;; | | | | 17 | ;; | | | | 18 | ;; | string | -----------> | array | 19 | ;; | | parse-tab | | 20 | ;; | | | | 21 | ;; +---------+ +---------+ 22 | ;; for human used in 23 | ;; use to add cronj-loop 24 | ;; tasks 25 | 26 | ;; Methods for type conversion 27 | (defn- to-int [x] (Integer/parseInt x)) 28 | 29 | ;; Array Representation 30 | (defn- *- 31 | ([] :*) 32 | ([s] (fn [v] (zero? (mod v s)))) 33 | ([a b] (fn [v] (and (>= v a) (<= v b)))) 34 | ([a b s] (fn [v] (and (>= v a) 35 | (<= v b) 36 | (zero? (mod (- v a) s)))))) 37 | 38 | ;; String to Array Methods 39 | (defn- parse-tab-elem [^String es] 40 | (cond (= es "*") :* 41 | (re-find #"^\d+$" es) (to-int es) 42 | (re-find #"^/\d+$" es) (*- (to-int (.substring es 1))) 43 | (re-find #"^\d+-\d+$" es) 44 | (apply *- 45 | (sort (map to-int (split es #"-")))) 46 | (re-find #"^\d+-\d+/\d$" es) 47 | (apply *- 48 | (map to-int (split es #"[-/]"))) 49 | :else (error es " is not in the right format."))) 50 | 51 | (defn- parse-tab-group [s] 52 | (let [e-toks (re-seq #"[^,]+" s)] 53 | (map parse-tab-elem e-toks))) 54 | 55 | (defn parse-tab [s] 56 | (let [c-toks (re-seq #"[^\s]+" s) 57 | len-c (count c-toks) 58 | sch-c (count SCHEDULE-ELEMENTS)] 59 | (cond (= sch-c len-c) (map parse-tab-group c-toks) 60 | (= (dec sch-c) len-c) (map parse-tab-group (cons "0" c-toks)) 61 | :else 62 | (error "The schedule " s 63 | " does not have the correct number of elements.")))) 64 | 65 | (defn valid-tab? [s] 66 | (suppress (if (parse-tab s) true))) 67 | 68 | ;; dt-arr methods 69 | (defn to-dt-array [dt] 70 | (map #(% dt) 71 | [t/second t/minute t/hour t/day-of-week t/day t/month t/year])) 72 | 73 | (defn truncate-ms [dt] 74 | (lt/to-local-date-time 75 | (apply t/date-time 76 | (map #(% dt) 77 | [t/year t/month t/day t/hour t/minute t/second])))) 78 | 79 | (defn- match-elem? [dt-e tab-e] 80 | (cond (= tab-e :*) true 81 | (= tab-e dt-e) true 82 | (fn? tab-e) (tab-e dt-e) 83 | (sequential? tab-e) (some #(match-elem? dt-e %) tab-e) 84 | :else false)) 85 | 86 | (defn match-array? [dt-array tab-array] 87 | (every? true? 88 | (map match-elem? dt-array tab-array))) 89 | 90 | (def nil-array [F F F F F F F]) 91 | -------------------------------------------------------------------------------- /src/cronj/data/task.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.data.task 2 | (:require [hara.ova :as ova] 3 | [hara.common.error :refer [error suppress]] 4 | [hara.common.checks :refer [hash-map?]] 5 | [clojure.tools.logging :as log])) 6 | 7 | (def REQUIRED-TASK-KEYS [:id :handler]) 8 | (def ALL-TASK-KEYS [:id :desc :handler :running :last-exec :last-successful :pre-hook :post-hook]) 9 | 10 | (defn- has-tid? [ova id] 11 | (ova/has? ova [:tid id])) 12 | 13 | (defn task? [t] 14 | (:running t)) 15 | 16 | (defn task 17 | ([m] (into {:desc "" :running (ova/ova) :last-exec (ref nil) 18 | :last-successful (ref nil)} 19 | m)) 20 | 21 | ([id handler & opts] 22 | (->> {:id id :handler handler} 23 | (into (apply hash-map opts)) 24 | (into {:desc "" :running (ova/ova) :last-exec (ref nil) 25 | :last-successful (ref nil)})))) 26 | 27 | (defn last-exec [task] 28 | @(:last-exec task)) 29 | 30 | (defn last-successful [task] 31 | @(:last-successful task)) 32 | 33 | (defn running [task] 34 | (if task 35 | (->> (ova/selectv (:running task)) 36 | (map #(select-keys % [:tid :opts]))))) 37 | 38 | (defn- register-thread [task tid threadp opts] 39 | (when (not (has-tid? (:running task) tid)) 40 | (dosync (ova/insert! (:running task) {:tid tid :thread threadp :opts opts}) 41 | (ref-set (:last-exec task) tid)) 42 | task)) 43 | 44 | (defn- deregister-thread 45 | ([task tid] (deregister-thread task tid true)) 46 | ([task tid finished?] 47 | (if (has-tid? (:running task) tid) 48 | (dosync (ova/remove! (:running task) [:tid tid]) 49 | (if finished? 50 | (ref-set (:last-successful task) tid)))) 51 | task)) 52 | 53 | (defn- exec-hook [hook tid opts] 54 | (if (fn? hook) (hook tid opts) opts)) 55 | 56 | (defn- exec-fn [regp tid handler opts] 57 | (suppress 58 | (if-let [task @regp] 59 | (let [post (:post-hook task) 60 | result (handler tid opts)] 61 | (exec-hook post tid (assoc opts :result result)) 62 | (deregister-thread task tid true)) 63 | (log/info "Task registration failed for " tid)))) 64 | 65 | (defn exec-main [task tid opts] 66 | (let [pre (:pre-hook task) 67 | handler (:handler task) 68 | opts (or opts {}) 69 | fopts (exec-hook pre tid opts) 70 | regp (promise) 71 | threadp (future (exec-fn regp tid handler fopts))] 72 | (deliver regp (register-thread task tid threadp fopts)) 73 | [regp threadp])) 74 | 75 | (defn exec! [task tid & [opts]] 76 | (cond (has-tid? (:running task) tid) 77 | (log/info "There is already a thread with tid: " tid "running.") 78 | 79 | (not (or (nil? opts) (hash-map? opts))) 80 | (error "The opts argument has to be a hashmap, not " opts) 81 | 82 | :else 83 | (exec-main task tid opts))) 84 | 85 | (defn kill! [task tid] 86 | (let [thrds (ova/selectv (:running task) [:tid tid])] 87 | (if-let [thrd (first thrds)] 88 | (do (future-cancel (:thread thrd)) 89 | (deregister-thread task tid false)) 90 | (log/info "Thread" tid "not running")))) 91 | 92 | (defn kill-all! [task] 93 | (dosync 94 | (doseq [tid (map :tid (ova/selectv (:running task)))] 95 | (kill! task tid)))) 96 | 97 | (defn reinit! [task] 98 | (dosync 99 | (kill-all! task) 100 | (alter (:last-exec task) 101 | (fn [_] nil)) 102 | (alter (:last-successful task) 103 | (fn [_] nil))) 104 | task) 105 | 106 | (defn <# [task] 107 | (-> 108 | (select-keys task [:id :desc :last-exec :last-successful]) 109 | (assoc :running (running task) 110 | :last-exec @(:last-exec task) 111 | :last-successful @(:last-successful task)))) 112 | -------------------------------------------------------------------------------- /src/cronj/data/timer.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.data.timer 2 | (:require [clj-time.core :as t] 3 | [clj-time.local :as lt] 4 | [cronj.data.tab :as tab] 5 | [clojure.tools.logging :as log])) 6 | 7 | (def DEFAULT-INTERVAL 1) 8 | 9 | (defn timer [& [interval]] 10 | (atom {:thread nil 11 | :start-time nil 12 | :last-check nil 13 | :last-check-time nil 14 | :interval (or interval DEFAULT-INTERVAL)})) 15 | 16 | (defn- timer-fn [timer recur?] 17 | (let [last-array (@timer :last-check) 18 | current-time (lt/local-now) 19 | current-array (tab/to-dt-array current-time)] 20 | (cond 21 | (or (not= last-array current-array) 22 | (nil? last-array)) 23 | (swap! timer assoc 24 | :last-check-time current-time 25 | :last-check current-array) 26 | 27 | :else 28 | (let [interval (@timer :interval) 29 | sleep-time (- 1000 30 | (t/milli current-time) 31 | interval)] 32 | (if (< 0 sleep-time) 33 | (Thread/sleep sleep-time) 34 | (Thread/sleep interval)))) 35 | (if recur? 36 | (recur timer true)))) 37 | 38 | (defn stopped? [timer] 39 | (let [x (:thread @timer)] 40 | (or (nil? x) 41 | (true? x) 42 | (future-done? x) 43 | (future-cancelled? x)))) 44 | 45 | (def running? (comp not stopped?)) 46 | 47 | (defn uptime [timer] 48 | (let [start (:start-time @timer) 49 | current (:last-check-time @timer)] 50 | (if (and start current) 51 | (- (clj-time.coerce/to-long current) 52 | (clj-time.coerce/to-long start))))) 53 | 54 | (defn start! 55 | ([timer] (start! timer DEFAULT-INTERVAL true)) 56 | ([timer interval recur?] 57 | (cond 58 | (stopped? timer) 59 | (swap! timer assoc 60 | :start-time (lt/local-now) 61 | :interval interval 62 | :thread (future (timer-fn timer recur?))) 63 | :else 64 | (log/info "The timer is already running.")))) 65 | 66 | (defn trigger! 67 | [timer] (start! timer DEFAULT-INTERVAL false)) 68 | 69 | (defn stop! [timer] 70 | (if-not (stopped? timer) 71 | (swap! timer (fn [m] 72 | (-> (update-in m [:thread] future-cancel) 73 | (assoc :thread nil 74 | :start-time nil)))) 75 | (log/info "The timer is already stopped."))) 76 | 77 | (defn restart! 78 | ([timer] (restart! timer DEFAULT-INTERVAL)) 79 | ([timer interval] 80 | (stop! timer) 81 | (start! timer interval))) 82 | -------------------------------------------------------------------------------- /src/cronj/simulation.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.simulation 2 | (:require [hara.ova :as ova] 3 | [clj-time.core :as t] 4 | [clj-time.local :as lt] 5 | [cronj.data.tab :as tab] 6 | [cronj.data.scheduler :as ts])) 7 | 8 | (defn local-time [& args] 9 | (lt/to-local-date-time 10 | (apply t/date-time args))) 11 | 12 | ;; Speed Up Execution. 13 | 14 | (defn- simulate-loop [cnj start end interval pause] 15 | (if-not (t/before? end start) 16 | (do 17 | (ts/signal-tick (:scheduler cnj) start) 18 | (if pause (Thread/sleep pause)) 19 | (recur cnj (t/plus start interval) end interval pause)))) 20 | 21 | (defn simulate [cnj start end & [interval pause]] 22 | (let [interval (cond (nil? interval) 23 | (t/seconds 1) 24 | 25 | (integer? interval) 26 | (t/seconds interval) 27 | 28 | :else interval) 29 | pause (or pause 0)] 30 | (simulate-loop cnj start end interval pause))) 31 | 32 | ;; Single Threaded Simulation. 33 | 34 | (defn exec-st [task dt] 35 | (let [pre (:pre-hook task) 36 | post (:post-hook task) 37 | handler (:handler (:task task)) 38 | opts (or (:opts task) {}) 39 | exec-hook (fn [hook dt opts] 40 | (if (fn? hook) (hook dt opts) opts)) 41 | fopts (exec-hook pre dt opts) 42 | result (handler dt fopts)] 43 | (exec-hook post dt (assoc fopts :result result)))) 44 | 45 | (defn- simulate-st-loop [cnj start end interval pause] 46 | (if-not (t/before? end start) 47 | (let [dt-array (tab/to-dt-array start)] 48 | (doseq [entry (ova/select (:scheduler cnj) [:enabled true])] 49 | (if (tab/match-array? dt-array (:tab-array entry)) 50 | (exec-st entry start))) 51 | (if pause (Thread/sleep pause)) 52 | (recur cnj (t/plus start interval) end interval pause)))) 53 | 54 | (defn simulate-st [cnj start end & [interval pause]] 55 | (let [interval (cond (nil? interval) 56 | (t/seconds 1) 57 | 58 | (integer? interval) 59 | (t/seconds interval) 60 | 61 | :else interval) 62 | pause (or pause 0)] 63 | (simulate-st-loop cnj start end interval pause))) 64 | -------------------------------------------------------------------------------- /test/cronj/test_core.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-core 2 | (:use midje.sweet 3 | ;;hara.checkers 4 | ) 5 | (:require [cronj.core :as cj] 6 | [cronj.data.scheduler :as ts] 7 | [cronj.data.task :as tk] 8 | [cronj.data.timer :as tm] :reload)) 9 | 10 | (defn has-length [counts] 11 | (fn [obj] 12 | (some #(= % (count obj)) counts))) 13 | 14 | (def cnj (cj/cronj 15 | :interval 2 16 | :entries [{:id :t1 17 | :handler (fn [dt opts] (println dt) (Thread/sleep 1000)) 18 | :schedule "* * * * * * *" 19 | :enabled true 20 | :opts {:home "/home/cronj"}} 21 | 22 | {:id :t2 23 | :handler (fn [dt opts] (println dt) (Thread/sleep 5000)) 24 | :schedule "* * * * * * *" 25 | :enabled true 26 | :opts {:ex "example"}}])) 27 | 28 | (facts "Initialization of cronj" 29 | ;; scheduler queries 30 | (cj/get-ids cnj) => [:t1 :t2] 31 | (cj/get-threads cnj) => [{:id :t1 :running ()} {:id :t2 :running ()}] 32 | (cj/task-enabled? cnj :t1) => true 33 | (cj/task-disabled? cnj :t2) => false 34 | 35 | ;; timer queries 36 | (cj/running? cnj) => false 37 | (cj/stopped? cnj) => true 38 | (cj/uptime cnj) => nil 39 | 40 | ;; task queries 41 | (cj/get-threads cnj :t1) => () 42 | (cj/get-threads cnj :t2) => () 43 | 44 | ;; watch is installed 45 | (.getWatches (:timer cnj)) => #(contains? % :time-watch) 46 | ) 47 | 48 | 49 | (facts "Enabling and disabling tasks" 50 | (cj/task-enabled? cnj :t1) => true 51 | 52 | (do "disable task" 53 | (cj/disable-task cnj :t1) 54 | (cj/task-enabled? cnj :t1)) 55 | => false 56 | 57 | (do "enable task" 58 | (cj/enable-task cnj :t1) 59 | (cj/task-enabled? cnj :t1)) 60 | => true) 61 | 62 | (facts "Scheduling and unscheduling new tasks" 63 | (let [cnj (cj/cronj)] 64 | 65 | (do "start up" 66 | (cj/start! cnj)) 67 | 68 | (cj/get-threads cnj :t-temp) => nil? 69 | (cj/running? cnj) => true 70 | 71 | (do "add a task" 72 | (cj/schedule-task cnj (tk/task 73 | {:id :t-temp 74 | :handler (fn [dt opts] (Thread/sleep 2000))}) 75 | "* * * * * * *")) 76 | (Thread/sleep 1000) 77 | (cj/get-threads cnj :t-temp) => (has-length #{1}) 78 | (cj/get-ids cnj) => [:t-temp] 79 | (cj/task-enabled? cnj :t-temp) => true 80 | (cj/running? cnj) => true 81 | 82 | (do "remove a task" 83 | (cj/unschedule-task cnj :t-temp)) 84 | (cj/get-ids cnj) => [] 85 | (cj/running? cnj) => true 86 | 87 | (do "clean up" 88 | (cj/shutdown! cnj)))) 89 | 90 | 91 | (facts "Starting, stopping and killing" 92 | (let [cnj (cj/cronj 93 | :interval 2 94 | :entries [{:id :t1 95 | :handler (fn [dt opts] (Thread/sleep 1000)) 96 | :schedule "* * * * * * *" 97 | :enabled true 98 | :opts {:home "/home/cronj"}} 99 | 100 | {:id :t2 101 | :handler (fn [dt opts] (Thread/sleep 5000)) 102 | :schedule "* * * * * * *" 103 | :enabled true 104 | :opts {:ex "example"}}])] 105 | (do "testing running" 106 | (cj/disable-task cnj :t1) 107 | (cj/start! cnj) 108 | (Thread/sleep 1000)) 109 | 110 | (cj/get-threads cnj :t1) => () 111 | (cj/get-threads cnj :t2) => (has-length #{1 2}) 112 | 113 | (do "sleep 3 secs" 114 | (Thread/sleep 3000)) 115 | 116 | (cj/get-threads cnj :t1) => () 117 | (cj/get-threads cnj :t2) => (has-length #{4 5}) 118 | 119 | (do "kill threads" 120 | (cj/kill! cnj :t2)) 121 | 122 | (cj/get-threads cnj :t2) => (has-length #{0 1}) 123 | 124 | (do "clean up" 125 | (cj/shutdown! cnj)) 126 | 127 | (cj/get-ids cnj) => [:t1 :t2] 128 | 129 | (do "empty cronj" 130 | (cj/empty-tasks cnj)) 131 | 132 | (cj/get-ids cnj) => [])) 133 | -------------------------------------------------------------------------------- /test/cronj/test_scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-scheduler 2 | (:use midje.sweet 3 | ;;hara.checkers 4 | ) 5 | (:require [hara.ova :as ova] 6 | [clj-time.local :as lt] 7 | [clj-time.core :as t] 8 | [cronj.data.task :as tk] 9 | [cronj.data.scheduler :as ts] :reload)) 10 | 11 | (facts "timesheet scheduling" 12 | (let [tscb (ts/scheduler) 13 | tk1 (tk/task :1 (fn [& _])) 14 | tk2 (tk/task :2 (fn [& _]))] 15 | 16 | (count tscb) => 0 17 | 18 | (ts/schedule-task tscb tk1 "* * * * * * *") 19 | (fact "should have 1 task" (count tscb) => 1) 20 | 21 | (ts/schedule-task tscb tk1 "* * * * * * *") 22 | (fact "should still have 1 task" (count tscb) => 1) 23 | 24 | (ts/schedule-task tscb tk2 "* * * * * * *") 25 | (fact "should have 2 tasks" 26 | (count tscb) => 2 27 | (ts/task-ids tscb) => (just [:1 :2] :in-any-order) 28 | (ts/task-threads tscb) => (just [{:id :1 :running []} 29 | {:id :2 :running []}] 30 | :in-any-order)) 31 | 32 | (ts/schedule-task tscb tk2 "* * * * * * *") 33 | (fact "should still have 2 tasks" (count tscb) => 2) 34 | 35 | (ts/reschedule-task tscb :1 "0 * * * * * *") 36 | (fact "only tk1 should be rescheduled" 37 | (:schedule (ts/get-task tscb :1)) => "0 * * * * * *" 38 | (:schedule (ts/get-task tscb :2)) => "* * * * * * *") 39 | 40 | (ts/unschedule-task tscb :2) 41 | (fact "should still have 1 task" (count tscb) => 1) 42 | 43 | (ts/unschedule-task tscb :1) 44 | (fact "should still have no tasks" (count tscb) => 0))) 45 | 46 | (facts "timesheet triggering" 47 | (let [out (atom nil) 48 | tscb (ts/scheduler) 49 | tk1 (tk/task :1 (fn [& _] (reset! out :1))) 50 | tk2 (tk/task :2 (fn [& _] (reset! out :2))) 51 | dt1 (t/from-time-zone (t/date-time 2002 1 1 1 1 1) (t/default-time-zone)) 52 | dt2 (t/from-time-zone (t/date-time 2002 1 1 1 1 2) (t/default-time-zone)) 53 | _ (ts/schedule-task tscb tk1 "1-60/2 * * * * * *") 54 | _ (ts/schedule-task tscb tk2 "2-60/2 * * * * * *")] 55 | 56 | (fact "initialization" 57 | @out => nil 58 | (count tscb) => 2 59 | (ts/task-enabled? tscb :1) => true 60 | (ts/task-enabled? tscb :2) => true) 61 | 62 | (ts/signal-tick tscb dt1) 63 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 64 | @job1 "out should be :1" 65 | @out => :1) 66 | (ts/signal-tick tscb dt2) 67 | (let [[reg2 job2] (-> (tscb [:task :id] :2) :output deref :exec)] 68 | @job2 "out should be :2" 69 | @out => :2) 70 | 71 | (ts/disable-task tscb :1) 72 | (ts/signal-tick tscb dt1) 73 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 74 | @job1 "out should be :2 as tk1 is disabled" 75 | @out => :2 76 | (ts/task-enabled? tscb :1) => false 77 | (ts/task-enabled? tscb :2) => true) 78 | 79 | (ts/enable-task tscb :1) 80 | (ts/signal-tick tscb dt1) 81 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 82 | @job1 "out should be :1 as tk1 is enabled" 83 | @out => :1 84 | (ts/task-enabled? tscb :1) => true 85 | (ts/task-enabled? tscb :2) => true) 86 | )) 87 | 88 | 89 | (facts "longer running task" 90 | (let [tscb (ts/scheduler) 91 | tk1 (tk/task :1 (fn [& _] (Thread/sleep 100))) 92 | dt1 (t/from-time-zone (t/date-time 2002 1 1 1 1 1) (t/default-time-zone)) 93 | dt2 (t/from-time-zone (t/date-time 2002 1 1 1 1 2) (t/default-time-zone)) 94 | _ (ts/schedule-task tscb tk1 "* * * * * * *")] 95 | (fact "should have 1 task, no threads running" 96 | (count tscb) => 1 97 | (ts/task-threads tscb) => [{:id :1 :running []}]) 98 | 99 | (ts/signal-tick tscb dt1) 100 | (Thread/sleep 10) 101 | (fact "one thread should be running" 102 | (ts/task-threads tscb) => [{:id :1 :running [{:tid dt1 :opts {}}]}]) 103 | 104 | (ts/signal-tick tscb dt2) (Thread/sleep 10) 105 | (fact "two threads should be running" 106 | (ts/task-threads tscb) => [{:id :1 :running [{:tid dt1 :opts {}} {:tid dt2 :opts {}}]}]) 107 | 108 | (Thread/sleep 100) 109 | (fact "no threads running" 110 | (ts/task-threads tscb) => [{:id :1 :running []}]))) 111 | -------------------------------------------------------------------------------- /test/cronj/test_simulation.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-simulation 2 | (:use midje.sweet 3 | ;;hara.checkers 4 | ) 5 | (:require [clj-time.core :as t] 6 | [clj-time.local :as lt] 7 | [hara.ova :as ova] 8 | [cronj.core :as cj] 9 | [cronj.data.scheduler :as ts] 10 | [cronj.simulation :as sm] :reload)) 11 | 12 | ;; Test for 13 | 14 | (def ^:dynamic *holder* (atom [])) 15 | (def ^:dynamic *t1* (lt/to-local-date-time (t/date-time 2000 1 1 1 1))) 16 | (def ^:dynamic *t2* (lt/to-local-date-time (t/date-time 2000 1 1 1 2))) 17 | 18 | (defn conj-dt-fn [dt opts] 19 | (let [r (:atom opts)] 20 | (swap! r conj dt))) 21 | 22 | (def ^:dynamic *cnj* (cj/cronj 23 | :entries [{:id :conj 24 | :handler conj-dt-fn 25 | :schedule "* * * * * * *" 26 | :opts {:atom *holder*} 27 | :enabled true}])) 28 | 29 | 30 | (fact 31 | (do "Reset Store" 32 | (reset! *holder* [])) 33 | 34 | (cj/disable-task *cnj* :conj) 35 | (ts/signal-tick (:scheduler *cnj*) :conj *t1*) 36 | (Thread/sleep 10) 37 | (count @*holder*) => 0 38 | 39 | (cj/enable-task *cnj* :conj) 40 | (ts/signal-tick (:scheduler *cnj*) :conj *t1*) 41 | 42 | (Thread/sleep 10) 43 | (count @*holder*) => 1 44 | (first @*holder*) => *t1*) 45 | 46 | (fact 47 | (do "Simulate using single threaded execution" 48 | (reset! *holder* []) 49 | (time (sm/simulate-st *cnj* *t1* *t2* (t/seconds 1)))) 50 | 51 | (Thread/sleep 10) 52 | (count @*holder*) => 61 53 | (first @*holder*) => *t1* 54 | (last @*holder*) => *t2*) 55 | 56 | (fact 57 | (do "Simulate using single threaded execution with task disabled" 58 | (reset! *holder* []) 59 | (cj/disable-task *cnj* :conj) 60 | (time (sm/simulate-st *cnj* *t1* *t2* (t/seconds 1)))) 61 | 62 | (Thread/sleep 10) 63 | (count @*holder*) => 0 64 | (first @*holder*) => nil 65 | (last @*holder*) => nil 66 | 67 | ;; Cleanup 68 | (cj/enable-task *cnj* :conj)) 69 | 70 | (fact 71 | (do "Simulate using multi-threaded execution with a pause of 1" 72 | (reset! *holder* []) 73 | (cj/shutdown! *cnj*) 74 | (time (sm/simulate *cnj* *t1* *t2* (t/seconds 2)))) 75 | 76 | (Thread/sleep 1000) 77 | (count @*holder*) => 31 78 | (first @*holder*) => *t1* 79 | (last @*holder*) => *t2*) 80 | 81 | (fact 82 | (do "Simulate using multi-threaded execution with a pause of 1" 83 | (reset! *holder* []) 84 | (cj/shutdown! *cnj*) 85 | (time (sm/simulate *cnj* *t1* *t2* (t/seconds 2) 1))) 86 | 87 | (Thread/sleep 10) 88 | (count @*holder*) => 31 89 | (first @*holder*) => *t1* 90 | (last @*holder*) => *t2*) 91 | 92 | 93 | ;; Additional code for mocking 94 | 95 | (comment 96 | (def cnj (cj/cronj 97 | :entries [{:id :t1 98 | :handler (fn [dt opts] (println "job 1" dt)) 99 | :schedule "/2 * * * * * *" 100 | :enabled true} 101 | 102 | {:id :t2 103 | :handler (fn [dt opts] (println "job 2" dt)) 104 | :schedule "/4 * * * * * *" 105 | :enabled true}])) 106 | 107 | (time (sm/simulate-st cnj 108 | (lt/to-local-date-time (t/date-time 2000 1 1 1 1)) 109 | (lt/to-local-date-time (t/date-time 2000 1 1 1 2)) 110 | (t/seconds 1))) 111 | 112 | (time (sm/simulate cnj 113 | (lt/to-local-date-time (t/date-time 2000 1 1 1 1)) 114 | (lt/to-local-date-time (t/date-time 2000 1 1 1 2)) 115 | (t/seconds 1))) 116 | 117 | 118 | ) 119 | 120 | ;;(exec-st (first (:scheduler cnj)) (t/date-time 2000 1 1 1)) 121 | 122 | ;;(ts/signal-tick (:scheduler cnj) (lt/to-local-date-time (t/date-time 2000 1 1 1 1))) 123 | 124 | ;;(:handler (:task (first (:scheduler cnj)))) 125 | -------------------------------------------------------------------------------- /test/cronj/test_tab.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-tab 2 | (:use midje.sweet) 3 | (:require [clj-time.core :as t] 4 | [cronj.data.tab :as tb] :reload)) 5 | 6 | (def test-num (range 60)) 7 | (def -* #'tb/*-) 8 | (def every-5-seconds-0 [(-* 0 60 5) (-*) (-*) (-*) (-*) (-*) (-*)]) 9 | (def every-5-seconds-1 [(-* 5) (-*) (-*) (-*) (-*) (-*) (-*)]) 10 | 11 | ;; Unit Tests 12 | (fact "*- takes a string and returns something" 13 | (#'tb/*-) => :* 14 | (map (#'tb/*- 2) test-num) => (map even? test-num) 15 | (map (#'tb/*- 0 10) test-num) => (map (fn [x] (and (>= x 0) (<= x 10))) test-num)) 16 | 17 | (tabular 18 | (fact "Testing the 'to-time-arrayay' function" 19 | (tb/to-dt-array ?time) => ?expected) 20 | 21 | ?time ?expected 22 | (t/epoch) [0 0 0 4 1 1 1970] 23 | (t/date-time 2000) [0 0 0 6 1 1 2000]) 24 | 25 | (fact "parse-str takes a string and creates matches" 26 | (tb/parse-tab "* * * * * * *") => '[(:*) (:*) (:*) (:*) (:*) (:*) (:*)] 27 | (tb/parse-tab "* * * * * *") => '[(0) (:*) (:*) (:*) (:*) (:*) (:*)] 28 | (tb/parse-tab "* * * * *") => (throws Exception) 29 | (tb/parse-tab "* * * * * * * *") => (throws Exception) 30 | (tb/parse-tab "1,2 1,5 * * 1 * *") => '[(1 2) (1 5) (:*) (:*) (1) (:*) (:*)] 31 | (tb/parse-tab "1,2 * * 1 * *") => '[(0) (1 2) (:*) (:*) (1) (:*) (:*)]) 32 | 33 | (tabular 34 | (fact "Testing the 'match-entry?' function" 35 | (#'tb/match-elem? ?t-entry ?c-entry) => ?expected) 36 | ?t-entry ?c-entry ?expected 37 | 1 :* true 38 | 1 [:*] true 39 | 1 [2 3 4] falsey 40 | 1 [1 2 3] true 41 | 1 [2 3 4 :*] true 42 | 1 (-*) true 43 | 1 [(-*)] true 44 | 30 (-* 30 40) true 45 | 29 (-* 30 40) falsey 46 | 40 (-* 30 40) true 47 | 30 (-* 30 40 5) true 48 | 40 (-* 30 40 5) true 49 | 35 (-* 30 40 5) true 50 | 36 (-* 30 40 5) falsey 51 | 30 (-* 5) true 52 | 30 (-* 2) true 53 | 35 (-* 5) true 54 | 35 (-* 2) falsey) 55 | 56 | (tabular 57 | (fact "Testing the 'match-array?' function" 58 | (tb/match-array? ?t-array ?c-array) => ?expected) 59 | 60 | ?t-array ?c-array ?expected 61 | [30 14 0 4 26 7 2012] every-5-seconds-0 true 62 | [31 14 0 4 26 7 2012] every-5-seconds-0 false 63 | [30 14 0 4 26 7 2012] every-5-seconds-1 true 64 | [31 14 0 4 26 7 2012] every-5-seconds-1 false) 65 | 66 | #_(fact "assoc-tab and tab-str" 67 | (tb/tab-str (tb/assoc-tab {} "* * * * * * *")) => "* * * * * * *") 68 | -------------------------------------------------------------------------------- /test/cronj/test_task.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-task 2 | (:use midje.sweet) 3 | (:require [hara.ova :as ova] 4 | [clj-time.local :as lt] 5 | [cronj.data.task :as t] :reload)) 6 | 7 | (facts "initial values" 8 | (let [mt (t/task :test-task (fn [& _]))] 9 | (fact "there are no tasks running" 10 | (t/running mt) => ()) ;; 11 | (fact "there should be no last executed id" 12 | (t/last-exec mt) => nil) 13 | (fact "there should be no last successful id" 14 | (t/last-successful mt) => nil) 15 | (fact "output print friendly form" 16 | (t/<# mt) => {:id :test-task 17 | :desc "" 18 | :running () 19 | :last-exec nil 20 | :last-successful nil}))) 21 | 22 | (facts "task execution" 23 | "Setup the data object as well as the tasks that manipulate the data object" 24 | (let [mda (atom nil) 25 | mt1 (t/task :mt1 (fn [& _] (reset! mda 1))) 26 | mt2 (t/task :mt2 (fn [& _] (reset! mda 2)))] 27 | 28 | (let [[mt1-reg mt1-done] (t/exec! mt1 :mt1-first)] 29 | @mt1-reg @mt1-done) 30 | (facts "mt1-first" 31 | (fact "data should be 1" 32 | (deref mda) => 1) 33 | (fact "last-exec should be :mt1-first" 34 | (t/last-exec mt1) => :mt1-first) 35 | (fact "last-successful should be :mt1-first" 36 | (t/last-successful mt1) => :mt1-first) 37 | (fact "output print friendly form" 38 | (t/<# mt1) => {:id :mt1 39 | :desc "" 40 | :running () 41 | :last-exec :mt1-first 42 | :last-successful :mt1-first})) 43 | 44 | (let [[mt2-reg mt2-done] (t/exec! mt2 :mt2-first)] 45 | @mt2-reg @mt2-done) 46 | 47 | (facts "atfer mt2-first" 48 | (fact "data should be 2" 49 | (deref mda) => 2) 50 | (fact "last-exec should be :mt2-first" 51 | (t/last-exec mt2) => :mt2-first) 52 | (fact "last-successful should be :mt2-first" 53 | (t/last-successful mt2) => :mt2-first)) 54 | 55 | (let [[mt1-reg mt1-done] (t/exec! mt1 :mt1-second)] 56 | @mt1-reg @mt1-done) 57 | (facts "after mt1-second" 58 | (fact "data should be 1" 59 | (deref mda) => 1) 60 | (fact "last-exec should be :mt1-second" 61 | (t/last-exec mt1) => :mt1-second) 62 | (fact "last-successful should be :mt1-second" 63 | (t/last-successful mt1) => :mt1-second)))) 64 | 65 | (facts "longer task execution behaviour" 66 | (let [job-100ms (t/task :1 (fn [& _] (Thread/sleep 10))) 67 | [reg job] (t/exec! job-100ms :test)] 68 | @reg 69 | (fact "When the job is still running, it will be in the list of running jobs" 70 | (t/running job-100ms) => '({:tid :test :opts {}})) 71 | @job 72 | (fact "When the job has finished, it will not appear 73 | in the list of running jobs" 74 | (t/running job-100ms) => '()))) 75 | 76 | (facts "multiple threading execution behaviour" 77 | (let [job (t/task :job (fn [& _] (Thread/sleep 20))) 78 | [reg1 job1] (t/exec! job :1) 79 | _ (Thread/sleep 10) 80 | [reg2 job2] (t/exec! job :2)] 81 | (do @reg1 @reg2 82 | (fact "All the jobs are running" 83 | (t/running job) => '({:tid :1 :opts {}} {:tid :2 :opts {}}) 84 | (t/last-exec job) => :2 85 | (t/last-successful job) => nil) 86 | @job1 87 | (fact "Job 1 will finish" 88 | (t/running job) => '({:tid :2 :opts {}}) 89 | (t/last-successful job) => :1) 90 | @job2 91 | (fact "Job 2 will finish" 92 | (t/running job) => '() 93 | (t/last-successful job) => :2)))) 94 | 95 | (facts "kill!" 96 | (let [job (t/task :job (fn [& _] (Thread/sleep 2000000))) 97 | [reg1 job1] (t/exec! job :1)] 98 | (do @reg1 99 | (fact "Check that the job is running" 100 | (t/running job) => '({:tid :1 :opts {}}) 101 | (t/last-exec job) => :1 102 | (t/last-successful job) => nil) 103 | (t/kill! job :1) 104 | (fact "Job 1 will finish" 105 | (t/running job) => '() 106 | (t/last-exec job) => :1 107 | (t/last-successful job) => nil)))) 108 | 109 | (facts "kill-all!" 110 | (let [job (t/task :job (fn [_ opts] (Thread/sleep 2000000))) 111 | [reg1 job1] (t/exec! job :1 {:a1 1 :a2 2}) 112 | [reg2 job2] (t/exec! job :2)] 113 | (do @reg1 114 | @reg2 115 | (fact "Check that the job is running" 116 | (t/running job) => '({:tid :1 :opts {:a1 1 :a2 2}} {:tid :2 :opts {}}) 117 | (t/last-exec job) => :2 118 | (t/last-successful job) => nil) 119 | (t/kill-all! job) 120 | (fact "Job 1 will finish" 121 | (t/running job) => '() 122 | (t/last-exec job) => :2 123 | (t/last-successful job) => nil) 124 | (t/reinit! job) 125 | (fact "Reinitialise tasks" 126 | (t/running job) => '() 127 | (t/last-exec job) => nil 128 | (t/last-successful job) => nil)))) 129 | -------------------------------------------------------------------------------- /test/cronj/test_timer.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-timer 2 | (:use midje.sweet 3 | ;;hara.checkers 4 | ) 5 | (:require [hara.ova :as ova] 6 | [clj-time.local :as lt] 7 | [clj-time.core :as t] 8 | [cronj.data.tab :as tab] 9 | [cronj.data.timer :as tm] :reload)) 10 | 11 | (fact "test timer-fn" 12 | (let [tmr (tm/timer) 13 | dt (tab/truncate-ms (lt/local-now)) 14 | dt-array (tab/to-dt-array dt) 15 | _ (#'tm/timer-fn tmr false)] 16 | (fact "last-check should update" 17 | (:last-check @tmr) => dt-array 18 | (tab/truncate-ms (:last-check-time @tmr)) => dt))) 19 | 20 | 21 | (fact "test stop and start functions" 22 | (let [tmr (tm/timer)] 23 | (fact "initialization" 24 | (tm/stopped? tmr) => true 25 | (tm/running? tmr) => false) 26 | 27 | (tm/start! tmr) (Thread/sleep 10) 28 | (fact "start!" 29 | (tm/stopped? tmr) => false 30 | (tm/running? tmr) => true) 31 | 32 | (tm/stop! tmr) (Thread/sleep 10) 33 | (fact "stop!" 34 | (tm/stopped? tmr) => true 35 | (tm/running? tmr) => false))) 36 | -------------------------------------------------------------------------------- /test/cronj/test_timetable.clj: -------------------------------------------------------------------------------- 1 | (ns cronj.test-scheduler 2 | (:use midje.sweet 3 | hara.checkers) 4 | (:require [ova.core :as v] 5 | [clj-time.local :as lt] 6 | [clj-time.core :as t] 7 | [cronj.data.task :as tk] 8 | [cronj.data.scheduler :as ts] :reload)) 9 | 10 | (facts "timesheet scheduling" 11 | (let [tscb (ts/scheduler) 12 | tk1 (tk/task :1 (fn [& _])) 13 | tk2 (tk/task :2 (fn [& _]))] 14 | 15 | (count tscb) => 0 16 | 17 | (ts/schedule-task tscb tk1 "* * * * * * *") 18 | (fact "should have 1 task" (count tscb) => 1) 19 | 20 | (ts/schedule-task tscb tk1 "* * * * * * *") 21 | (fact "should still have 1 task" (count tscb) => 1) 22 | 23 | (ts/schedule-task tscb tk2 "* * * * * * *") 24 | (fact "should have 2 tasks" 25 | (count tscb) => 2 26 | (ts/task-ids tscb) => (just [:1 :2] :in-any-order) 27 | (ts/task-threads tscb) => (just [{:id :1 :running []} 28 | {:id :2 :running []}] 29 | :in-any-order)) 30 | 31 | (ts/schedule-task tscb tk2 "* * * * * * *") 32 | (fact "should still have 2 tasks" (count tscb) => 2) 33 | 34 | (ts/unschedule-task tscb :2) 35 | (fact "should still have 1 task" (count tscb) => 1) 36 | 37 | (ts/unschedule-task tscb :1) 38 | (fact "should still have no tasks" (count tscb) => 0))) 39 | 40 | (facts "timesheet triggering" 41 | (let [out (atom nil) 42 | tscb (ts/scheduler) 43 | tk1 (tk/task :1 (fn [& _] (reset! out :1))) 44 | tk2 (tk/task :2 (fn [& _] (reset! out :2))) 45 | dt1 (t/from-time-zone (t/date-time 2002 1 1 1 1 1) (t/default-time-zone)) 46 | dt2 (t/from-time-zone (t/date-time 2002 1 1 1 1 2) (t/default-time-zone)) 47 | _ (ts/schedule-task tscb tk1 "1-60/2 * * * * * *") 48 | _ (ts/schedule-task tscb tk2 "2-60/2 * * * * * *")] 49 | 50 | (fact "initialization" 51 | out => (is-atom nil) 52 | (count tscb) => 2 53 | (ts/task-enabled? tscb :1) => true 54 | (ts/task-enabled? tscb :2) => true) 55 | 56 | (ts/signal-tick tscb dt1) 57 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 58 | @job1 "out should be :1" 59 | out => (is-atom :1)) 60 | (ts/signal-tick tscb dt2) 61 | (let [[reg2 job2] (-> (tscb [:task :id] :2) :output deref :exec)] 62 | @job2 "out should be :2" 63 | out => (is-atom :2)) 64 | 65 | (ts/disable-task tscb :1) 66 | (ts/signal-tick tscb dt1) 67 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 68 | @job1 "out should be :2 as tk1 is disabled" 69 | out => (is-atom :2) 70 | (ts/task-enabled? tscb :1) => false 71 | (ts/task-enabled? tscb :2) => true) 72 | 73 | (ts/enable-task tscb :1) 74 | (ts/signal-tick tscb dt1) 75 | (let [[reg1 job1] (-> (tscb [:task :id] :1) :output deref :exec)] 76 | @job1 "out should be :1 as tk1 is enabled" 77 | out => (is-atom :1) 78 | (ts/task-enabled? tscb :1) => true 79 | (ts/task-enabled? tscb :2) => true) 80 | )) 81 | 82 | 83 | (facts "longer running task" 84 | (let [tscb (ts/scheduler) 85 | tk1 (tk/task :1 (fn [& _] (Thread/sleep 100))) 86 | dt1 (t/from-time-zone (t/date-time 2002 1 1 1 1 1) (t/default-time-zone)) 87 | dt2 (t/from-time-zone (t/date-time 2002 1 1 1 1 2) (t/default-time-zone)) 88 | _ (ts/schedule-task tscb tk1 "* * * * * * *")] 89 | (fact "should have 1 task, no threads running" 90 | (count tscb) => 1 91 | (ts/task-threads tscb) => [{:id :1 :running []}]) 92 | 93 | (ts/signal-tick tscb dt1) 94 | (Thread/sleep 10) 95 | (fact "one thread should be running" 96 | (ts/task-threads tscb) => [{:id :1 :running [{:tid dt1 :opts {}}]}]) 97 | 98 | (ts/signal-tick tscb dt2) (Thread/sleep 10) 99 | (fact "two threads should be running" 100 | (ts/task-threads tscb) => [{:id :1 :running [{:tid dt1 :opts {}} {:tid dt2 :opts {}}]}]) 101 | 102 | (Thread/sleep 100) 103 | (fact "no threads running" 104 | (ts/task-threads tscb) => [{:id :1 :running []}]))) 105 | -------------------------------------------------------------------------------- /test/midje_doc/cronj_api.clj: -------------------------------------------------------------------------------- 1 | (ns midje-doc.cronj-api 2 | (:require [cronj.core :refer :all] 3 | [midje.sweet :refer :all])) 4 | 5 | [[:chapter {:title "API Reference"}]] 6 | 7 | [[:section {:title "cronj"}]] 8 | 9 | "`cronj` constructs a task-scheduler object." 10 | 11 | [[{:numbered false}]] 12 | (comment 13 | (cronj :entries )) 14 | 15 | "An simple example:" 16 | 17 | [[{:numbered false}]] 18 | (def cnj 19 | (cronj :entries 20 | [{:id "print-task" 21 | :handler (fn [t opts] (println (:output opts) ": " t)) 22 | :schedule "/2 * * * * * *" 23 | :opts {:output "Hello There"}}])) 24 | 25 | [[:subsection {:title "crontab"}]] 26 | 27 | "Each `cronj` task has a `:schedule` entry. The value is a string specifying when it is supposed to run. The string is of the same format as `crontab` - seven elements seperated by spaces. The elements are used to match the time, expressed as seven numbers: 28 | 29 | second minute hour day-of-week day-of-month month year 30 | 31 | The rules for a match between the crontab and the current time are: 32 | 33 | - `A` means match on `A` 34 | - `*` means match on any number 35 | - `E1,E2` means match on both `E1` and `E2` 36 | - `A-B` means match on any number between `A` and `B` inclusive 37 | - `/N` means match on any number divisible by `N` 38 | - `A-B/N` means match on any number divisible by `N` between `A` and `B` inclusive 39 | 40 | Where `A`, `B` and `N` are numbers; `E1` and `E2` are expressions. All seven elements in the string have to match in order for the task to be triggered. 41 | " 42 | 43 | [[{:numbered false}]] 44 | (comment 45 | 46 | ;; Triggered every 5 seconds 47 | 48 | "/5 * * * * * *" 49 | 50 | 51 | ;; Triggered every 5 seconds between 32 and 60 seconds 52 | 53 | "32-60/5 * * * * * *" 54 | 55 | ;; Triggered every 5 seconds on the 9th aand 10th 56 | ;; minute of every hour on every Friday from June 57 | ;; to August between years 2012 to 2020. 58 | 59 | "/5 9,10 * 5 * 6-8 2012-2020") 60 | 61 | 62 | [[:section {:title "system commands"}]] 63 | 64 | "System commands mainly work with the cronj timer." 65 | 66 | [[:subsection {:title "start!"}]] 67 | 68 | "Starts up the timer such that tasks are launched at the scheduled time" 69 | 70 | [[{:numbered false}]] 71 | (comment 72 | (start! )) 73 | 74 | [[:subsection {:title "stop!"}]] 75 | 76 | "Stops the timer. New task threads will not be launched. However, exisiting tasks threads will not be killed and finish naturally." 77 | 78 | [[{:numbered false}]] 79 | (comment 80 | (stop! )) 81 | 82 | [[:subsection {:title "shutdown!"}]] 83 | 84 | "Stops the timer. New task threads will not be launched. Exisiting tasks threads will be killed immediately" 85 | 86 | [[{:numbered false}]] 87 | (comment 88 | (shutdown! )) 89 | 90 | [[:subsection {:title "restart!"}]] 91 | 92 | "Restarts the timer, killing all exisiting threads." 93 | 94 | [[{:numbered false}]] 95 | (comment 96 | (restart! )) 97 | 98 | [[:subsection {:title "stopped?"}]] 99 | 100 | "Checks whether the timer is stopped" 101 | 102 | [[{:numbered false}]] 103 | (comment 104 | (stopped? )) 105 | 106 | [[:subsection {:title "running?"}]] 107 | 108 | "Checks whether the timer is running. Complement of stopped?" 109 | 110 | [[{:numbered false}]] 111 | (comment 112 | (running? )) 113 | 114 | [[:subsection {:title "uptime"}]] 115 | 116 | "Checks how long the timer has been running. Returns a long representing the time in msecs." 117 | 118 | [[{:numbered false}]] 119 | (comment 120 | (uptime )) 121 | 122 | [[:section {:title "task scheduling"}]] 123 | 124 | [[:subsection {:title "enable-task"}]] 125 | 126 | "If a task has been disabled, meaning that the task will not run at its allocated time, `enable-task` will enable it." 127 | 128 | [[{:numbered false}]] 129 | (comment 130 | (enable-task ) 165 | 166 | (simulate )) 167 | 168 | [[:subsection {:title "simulate-st"}]] 169 | 170 | "Simulate the timer over start-time and end-time. Just like `simulate` but all tasks are executed on a single thread (only should be used on non-blocking handlers)" 171 | 172 | [[{:numbered false}]] 173 | (comment 174 | (simulate-st ) 175 | 176 | (simulate-st )) 177 | 178 | 179 | [[:section {:title "thread management"}]] 180 | 181 | [[:subsection {:title "get-ids"}]] 182 | 183 | "Return a list of all task ids:" 184 | 185 | [[{:numbered false}]] 186 | (comment 187 | (get-ids )) 188 | 189 | [[:subsection {:title "get-task"}]] 190 | 191 | "Return the task entry by id" 192 | 193 | [[{:numbered false}]] 194 | (comment 195 | (get-task ) 196 | 197 | ;; Example Output: 198 | (get-task cnj "print-task") 199 | => {:task {:desc "" 200 | :running "" 201 | :last-exec "#" 202 | :last-successful "#" 203 | :handler "#" 204 | :id "print-task"} 205 | :schedule "/2 * * * * * *" 206 | :tab-array (("") 207 | (:*) (:*) (:*) (:*) (:*) (:*)) 208 | :enabled true 209 | :opts {:output "Hello There"} 210 | :output "#"}) 211 | 212 | [[:subsection {:title "get-threads"}]] 213 | 214 | "Returns a list of running threads. See [Task Management](#task-management) for examples." 215 | 216 | [[{:numbered false}]] 217 | (comment 218 | (get-threads ) ;; Gets all running threads in 219 | 220 | (get-threads ) ;; Gets all threads for in 221 | ) 222 | 223 | [[:subsection {:title "exec!"}]] 224 | 225 | "Launches a thread for the task, irrespective of whether the task has not been scheduled or that it has been disabled." 226 | 227 | [[{:numbered false}]] 228 | (comment 229 | (exec! ) ;; launches a new thread for using 230 | ;; the current time and default opts 231 | 232 | (exec!
) ;; launches a new thread for using 233 | ;; the time as
and default opts 234 | 235 | (exec!
) ;; launches a new thread for using 236 | ;; the time as
and opts as 237 | ) 238 | 239 | [[:subsection {:title "kill!"}]] 240 | 241 | "Kills running threads. See [Task Management](#task-management) for examples." 242 | 243 | [[{:numbered false}]] 244 | (comment 245 | (kill! ) ;; Kills all running threads 246 | 247 | (kill! ) ;; Kills all running threads for 248 | 249 | (kill!
) ;; Kills only thread started at
for 250 | ) 251 | -------------------------------------------------------------------------------- /test/midje_doc/cronj_guide.clj: -------------------------------------------------------------------------------- 1 | (ns midje-doc.cronj-guide 2 | (:require [cronj.core :refer :all] 3 | [midje.sweet :refer :all])) 4 | 5 | [[:chapter {:title "Installation"}]] 6 | 7 | "Add to `project.clj` dependencies: 8 | 9 | `[im.chit/cronj `\"`{{PROJECT.version}}`\"`]`" 10 | 11 | "All functions are in the `cronj.core` namespace." 12 | 13 | [[{:numbered false}]] 14 | (comment (use 'cronj.core)) 15 | 16 | 17 | [[:chapter {:title "Background"}]] 18 | 19 | " 20 | `cronj` was built for a project of mine back in 2012. The system needed to record video footage from multiple ip-cameras in fifteen minute blocks, as well as to save pictures from each camera (one picture every second). All saved files needed a timestamp allowing for easy file management and retrieval. 21 | 22 | At that time, `quartzite`, `at-at` and `monotony` were the most popular options. After coming up with a list of design features and weighing up all options, I decided to write my own instead. As a core component of the original project, `cronj` has been operational now since October 2012. A couple of major rewrites and api rejuggling were done, but the api has been very stable from version `0.6` onwards. 23 | 24 | There are now many more scheduling libraries in the clojure world: 25 | 26 | - [at-at](https://github.com/overtone/at-at) 27 | - [chime](https://github.com/james-henderson/chime) 28 | - [clj-cronlike](https://github.com/kognate/clj-cronlike) 29 | - [cron4j](http://www.sauronsoftware.it/projects/cron4j) 30 | - [monotony](https://github.com/aredington/monotony) 31 | - [quartzite](https://github.com/michaelklishin/quartzite) 32 | - [schejulure](https://github.com/AdamClements/schejulure) 33 | 34 | With so many options, and so many different ways to define task schedules, why choose `cronj`? I have listed a number of [design](#design) decisions that make it beneficial. However, for those that are impatient, cut to the chase, by skipping to the [simulations](#running-simulations) section. 35 | 36 | " 37 | 38 | [[:chapter {:title "Design"}]] 39 | 40 | "`cronj` was built around a concept of a **task**. A task has two components: 41 | - A `handler` (what is to be done) 42 | - A `schedule` (when it should be done) 43 | 44 | Tasks are *triggered* by a `scheduler` who in-turn is notified of the current time by a `timer`. If a task was scheduled to run at that time, it's `handler` would be run in a seperate thread. 45 | " 46 | 47 | [[{:numbered false}]] 48 | (comment 49 | ; cronj schedule 50 | ; -------------- +-------------------------+ 51 | ; scheduler watches | '* 8 /2 7-9 2,3 * *' | 52 | ; the timer and +-------------------------+ 53 | ; triggers tasks | :sec [:*] | 54 | ; to execute at | :min [:# 8] | 55 | ; the scheduled time | :hour [:| 2] | 56 | ; | :dayw [:- 7 9] | 57 | ; | :daym [:# 2] [:# 3] | 58 | ; | :month [:*] | 59 | ; | :year [:*] | 60 | ; +-----------+-------------+ 61 | ; task | XXXXXXXXX 62 | ; +-----------------+ +-----------+-----+ XX XX 63 | ; |:id | | | |\ XX timer XX 64 | ; |:desc +---+-+:task | | \ X X 65 | ; |:handler | | :schedule+ | \ X :start-time X 66 | ; |:pre-hook | | :enabled | entry X :thread X+----+ 67 | ; |:post-hook | | :opts | `. X :last-check X | 68 | ; |:enabled | | | \ X :interval X | 69 | ; |:args | _-------._--------, \ XX XX | 70 | ; |:running | `-._ `.. `. \ XX XX + 71 | ; |:last-exec | `-._ `-._ `. \ XXXXXXXXX watch 72 | ; |:last-successful | `-._ `-._ `. `. + 73 | ; +----------+------+ `-._ `-. `. \ | 74 | ; +----+----+----`-._-+-`-.`.--->----+----+----+----+----+----+ 75 | ; | | | | `-.. | `. | | | | | | | 76 | ; +----+----+----+----+--`-.---'+----+----+----+----+----+----+ 77 | ; scheduler 78 | ) 79 | 80 | 81 | [[:section {:title "Seperation of Concerns"}]] 82 | " 83 | A task handler is just a function taking two arguments: 84 | " 85 | [[{:numbered false}]] 86 | (comment 87 | (fn [t opts] 88 | (... perform a task ...))) 89 | 90 | " 91 | **`t`** represents the time at which the handler was called. This solves the problem of *time synchronisation*. For example, I may have three tasks scheduled to run at a same time: 92 | 93 | - perform a calculation and write the result to the database 94 | - perform a http call and write result to the database 95 | - load some files, write to single output then store file location to the database. 96 | 97 | All these tasks will end at different times. To retrospectively reasoning about how all three tasks were synced, each handler is required to accept the triggred time `t` as an argument. 98 | 99 | **`opts`** is a hashmap, for example `{:path '/app/videos'}`. It has been found that user customisations such as server addresses and filenames, along with job schedules are usually specified at the top-most tier of the application whilst handler logic is usually in the middle-tier. Having an extra `opts` argument allow for better seperation of concerns and more readable code. 100 | 101 | " 102 | 103 | [[:section {:title "Thread Management"}]] 104 | " 105 | In reviewing other scheduling libraries, it was found that fully-featured thread management capabilities were lacking. `cronj` was designed with these features in mind: 106 | 107 | - tasks can be triggered to start manually at any time. 108 | - tasks can start at the next scheduled time before the previous thread has finished running so that multiple threads can be running simultaneously for a single task. 109 | - *pre-* and *post-* hooks can be defined for better seperation of setup/notification/cleanup code from handler body. 110 | - running threads can be listed. 111 | - normal and abnormal termination: 112 | - kill a running thread 113 | - kill all running threads in a task 114 | - kill all threads 115 | - disable task but let running threads finish 116 | - stop timer but let running threads finish 117 | - shutdown timer, kill all running threads 118 | " 119 | 120 | [[:section {:title "Simulation Testing"}]] 121 | 122 | " 123 | Because the `timer` and the `scheduler` modules have been completely decoupled, it was very easy to add a simulation component into `cronj`. Simulation has some very handy features: 124 | - Simulate how the entire system would behave over a long periods of time 125 | - Generating test inputs for other applications. 126 | - Both single and multi-threaded execution strategies are supported. 127 | " 128 | 129 | [[:chapter {:title "Walkthrough"}]] 130 | 131 | "In this section all the important and novel features and use cases for `cronj` will be shown. Interesting examples include: [simulation](#running-simulations), [task management](#task-management) and [hooks](#hooks)." 132 | 133 | [[:section {:title "Creating a Task"}]] 134 | 135 | "`print-handler` outputs the value of `{:output opts}` and the time `t`." 136 | 137 | [[{:numbered false}]] 138 | (defn print-handler [t opts] 139 | (println (:output opts) ": " t)) 140 | 141 | "`print-task` defines the actual task to be run. Note that it is just a map. 142 | - `:handler` set to `print-handler` 143 | - `:schedule` set for task to run every `2` seconds 144 | - `:opts` can be customised 145 | " 146 | 147 | [[{:numbered false}]] 148 | (def print-task 149 | {:id "print-task" 150 | :handler print-handler 151 | :schedule "/2 * * * * * *" 152 | :opts {:output "Hello There"}}) 153 | 154 | [[:section {:title "Running Tasks"}]] 155 | 156 | "Once the task is defined, `cronj` is called to create the task-scheduler (`cj`)." 157 | 158 | [[{:numbered false}]] 159 | (def cj (cronj :entries [print-task])) 160 | 161 | "Calling `start!` on `cj` will start the timer and `print-handler` will be triggered every two seconds. Calling `stop!` on `cj` will stop all outputs" 162 | 163 | [[{:numbered false}]] 164 | (comment 165 | (start! cj) 166 | 167 | ;; > Hello There : # 168 | 169 | .... wait 2 secs ... 170 | 171 | ;; > Hello There : # 172 | 173 | .... wait 2 secs ... 174 | 175 | ;; > Hello There : # 176 | 177 | .... wait 2 secs ... 178 | 179 | ;; > Hello There : # 180 | 181 | (stop! cj)) 182 | 183 | [[:section {:title "Running Simulations"}]] 184 | 185 | "Simulations are a great way to check your application for errors as they provide constant time inputs. This allows an entire system to be tested for correctness. How `simulate` works is that it decouples the `timer` from the `scheduler` and tricks the `scheduler` to trigger on the range of date inputs provided." 186 | 187 | [[:subsection {:title "Y2K Revisited"}]] 188 | "For instance, we wish to test that our `print-handler` method was not affected by the Y2K Bug. `T1` and `T2` are defined as start and end times:" 189 | 190 | [[{:numbered false}]] 191 | (def T1 (local-time 1999 12 31 23 59 58)) 192 | 193 | (def T2 (local-time 2000 1 1 0 0 2)) 194 | 195 | "We can simulate events by calling `simulate` on `cj` with a start and end time. The function will trigger registered tasks to run beginning at T1, incrementing by 1 sec each time until T2. Note that in this example, there are three threads created for `print-handler`. The printed output may be out of order because of indeterminancy of threads (we can fix this later)." 196 | [[{:numbered false}]] 197 | (comment 198 | (simulate cj T1 T2) 199 | 200 | .... instantly ... 201 | 202 | ;; > Hello There : # 203 | ;; > Hello There : # ;; out of order 204 | ;; > Hello There : # 205 | ) 206 | 207 | [[:subsection {:title "Single Threaded"}]] 208 | "To keep ordering of the `println` outputs, `simulate-st` can be used. This will run `print-handler` calls on a single thread and so will keep order of outputs. Because of the sequential nature of this type of simulation, it is advised that `simulate-st` be used only if there are no significant pauses or thread blocking in the tasks." 209 | [[{:numbered false}]] 210 | (comment 211 | (simulate-st cj T1 T2) 212 | 213 | .... instantly ... 214 | 215 | ;; > Hello There : # 216 | ;; > Hello There : # 217 | ;; > Hello There : # 218 | ) 219 | 220 | [[:subsection {:title "Interval and Pause"}]] 221 | "Two other arguments for `simulate` and `simulate-st` are: 222 | - the time interval `(in secs)` between the current time-point and the next time-point (the default is 1) 223 | - the pause `(in ms)` to take in triggering the next time-point (the default is 0) 224 | 225 | It can be seen that we can simulate the actual speed of outputs by keeping the interval as 1 and increasing the pause time to 1000ms" 226 | 227 | [[{:numbered false}]] 228 | (comment 229 | (simulate cj T1 T2 1 1000) 230 | 231 | ;; > Hello There : # 232 | 233 | .... wait 2 secs ... 234 | 235 | ;; > Hello There : # 236 | 237 | .... wait 2 secs ... 238 | 239 | ;; > Hello There : # 240 | ) 241 | 242 | [[:subsection {:title "Speeding Up"}]] 243 | "In the following example, the interval has been increased to 2 seconds whilst the pause time has decreased to 100ms. This results in a 20x increase in the speed of outputs." 244 | [[{:numbered false}]] 245 | (comment 246 | (simulate cj T1 T2 2 100) 247 | 248 | ;; > Hello There : # 249 | 250 | .... wait 100 msecs ... 251 | 252 | ;; > Hello There : # 253 | 254 | .... wait 100 msecs ... 255 | 256 | ;; > Hello There : # 257 | ) 258 | 259 | "Being able to adjust these simulation parameters are really powerful testing tools and saves an incredible amount of time in development. For example, we can quickly test the year long output of a task that is scheduled to run once an hour very quickly by making the interval 3600 seconds and the pause time to the same length of time that the task takes to finish. 260 | 261 | Through simulations, task-scheduling can now be tested and entire systems just got easier to manage and reason about!" 262 | 263 | [[:section {:title "Task Management"}]] 264 | 265 | "Task management capabilities of `cronj` will be demonstrated by first creating a `cronj` object with two task entries labeled `l1` and `l2` doing nothing but sleeping for a long time:" 266 | 267 | [[{:numbered false}]] 268 | (def cj 269 | (cronj :entries 270 | [{:id :l1 271 | :handler (fn [dt opts] (Thread/sleep 30000000000000)) 272 | :schedule "/2 * * * * * *" 273 | :opts {:data "foo"}} 274 | {:id :l2 275 | :handler (fn [dt opts] (Thread/sleep 30000000000000)) 276 | :schedule "0-2 * * * * * *" 277 | :opts {:data "bar"}}])) 278 | 279 | [[:subsection {:title "Showing Threads"}]] 280 | 281 | " 282 | The task will be triggered using the `exec!` command. This is done for play purposes. Normal use would involve calling `get-threads` after `start!` has been called." 283 | 284 | [[{:numbered false}]] 285 | (fact 286 | (get-threads cj :l1) ;; See that there are no threads running 287 | => [] ;; - :l1 is empty 288 | 289 | (get-threads cj :l2) 290 | => [] ;; - :l2 is empty 291 | 292 | (exec! cj :l1 T1) ;; Launch :l1 with time of T1 293 | 294 | (get-threads cj :l1) 295 | => [{:tid T1 :opts {:data "foo"}}] ;; l1 now has one running thread 296 | 297 | (exec! cj :l1 T2) ;; Launch :l2 with time of T2 298 | 299 | (get-threads cj :l1) 300 | => [{:tid T1 :opts {:data "foo"}} ;; l1 now has two running threads 301 | {:tid T2 :opts {:data "foo"}}] 302 | 303 | 304 | (exec! cj :l2 T2 {:data "new"}) ;; Launch :l2 with time of T2 305 | (get-threads cj :l2) 306 | => [{:tid T2 :opts {:data "new"}}] ;; l2 now has one running thread 307 | 308 | (get-threads cj) ;; if no id is given, all running threads can be seen 309 | => [{:id :l1, :running [{:tid T1 :opts {:data "foo"}} 310 | {:tid T2 :opts {:data "foo"}}]} 311 | {:id :l2, :running [{:tid T2 :opts {:data "new"}}]}]) 312 | 313 | [[:subsection {:title "Killing Threads"}]] 314 | 315 | [[{:numbered false}]] 316 | (fact 317 | (kill! cj :l1 T1) ;; Kill :l1 thread starting at time T1 318 | (get-threads cj :l1) 319 | => [{:opts {:data "foo"}, :tid T2}] ;; l1 now has one running thread 320 | 321 | (kill! cj :l1) ;; Kill all :l1 threads 322 | (get-threads cj :l1) 323 | => [] ;; l1 now has no threads 324 | 325 | (kill! cj) ;; Kill everything in cj 326 | (get-threads cj) 327 | => [{:id :l1, :running []} ;; All threads have been killed 328 | {:id :l2, :running []}]) 329 | 330 | 331 | 332 | [[:section {:title "Pre and Post Hooks" :tag "hooks"}]] 333 | 334 | "Having pre- and post- hook entries allow additional processing to be done outside of the handler. They also have the same function signature as the task handler. An example below can be seen where data is passed from one handler to another:" 335 | 336 | [[{:numbered false}]] 337 | (comment 338 | (def cj 339 | (cronj 340 | :entries [{:id :hook 341 | :desc "This is showing how a hook example should work" 342 | :handler (fn [dt opts] 343 | (println "In handle, opts:" opts) 344 | (Thread/sleep 1000) ;; Do Something 345 | :handler-result) 346 | :pre-hook (fn [dt opts] 347 | (println "In pre-hook," "opts:" opts) 348 | (assoc opts :pre-hook :pre-hook-data)) 349 | :post-hook (fn [dt opts] 350 | (println "In post-hook, opts: " opts)) 351 | :opts {:data "stuff"} 352 | :schedule "* * * * * * *"}])) 353 | 354 | (exec! cj :hook T1) 355 | 356 | ;; > In pre-hook, opts: {:data stuff} 357 | ;; > In handle, opts: {:data stuff, :pre-hook :pre-hook-data} 358 | 359 | .... wait 1000 msecs .... 360 | 361 | ;; > In post-hook, opts: {:data stuff, :pre-hook :pre-hook-data, :result :handler-result} 362 | ) 363 | 364 | "As could be seen, the `:pre-hook` function can modify opts for use in the handler function while `:pre-hook` can take the result of the main handler and do something with it. I use it mostly for logging purposes." 365 | 366 | [[:file {:src "test/midje_doc/cronj_api.clj"}]] 367 | 368 | [[:chapter {:title "End Notes"}]] 369 | 370 | "For any feedback, requests and comments, please feel free to lodge an issue on github or contact me directly. 371 | 372 | Chris. 373 | " 374 | --------------------------------------------------------------------------------