├── .gitignore ├── project.clj ├── test └── schejulure │ └── core_test.clj ├── README.md └── src └── schejulure └── core.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 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject schejulure "1.0.2-SNAPSHOT" 2 | :description "A simple long-term scheduling library inspired by cron and future" 3 | :url "http://www.github.com/AdamClements/schejulure" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.4.0"] 7 | [clj-time "0.6.0"]] 8 | :profiles {:dev {:dependencies [[midje "1.5.1"]] 9 | :plugins [[lein-midje "3.0.1"]]}}) 10 | -------------------------------------------------------------------------------- /test/schejulure/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns schejulure.core-test 2 | (:require [midje.sweet :refer :all] 3 | [clj-time.core :refer [date-time]] 4 | [schejulure.core :refer :all])) 5 | 6 | (fact "I have tests" 7 | (identity true) => true) 8 | 9 | (facts "cron-of basically works" 10 | (cron-of (date-time 1986 10 23 16 30)) => [30 16 23 10 4] 11 | (cron-of (date-time 1986 10 22 8 30)) => [30 8 22 10 3] 12 | (cron-of (date-time 2000 1 1 0 0)) => [0 0 1 1 6] 13 | (cron-of (date-time 2000 1 2 0 0)) => [0 0 2 1 7]) 14 | 15 | (facts "cron-match can match crons" 16 | (cron-match? [0 0 2 3 4] [[0 1 2] [0 1 2] [0 1 2] [0 1 2] [0 1 2]]) => false 17 | (cron-match? [0 0 2 1 1] [[0 1 2] [0 1 2] [0 1 2] [0 1 2] [0 1 2]]) => true 18 | (cron-match? [0 0 2 3 4] (cronmap->cronrange cron-defaults)) => true) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schejulure 2 | 3 | A simple cron-inspired library for clojure 4 | 5 | ## Usage 6 | 7 | Include the library in your leiningen project dependencies: 8 | 9 | ```clojure 10 | [schejulure "1.0.1"] 11 | ``` 12 | 13 | Then use it to schedule something: 14 | 15 | ```clojure 16 | (use 'schejulure.core) 17 | 18 | (def my-running-scheduler 19 | (schedule {:hour 12 :minute [0 15 30 45]} my-function 20 | {:hour (range 0 24 6) :minute 0 :day [:sat :sun]} batch-job)) 21 | ``` 22 | 23 | This will start running straight away. This actually returns a future, which can be manipulated in all the same ways as a normal clojure future. Unlike a lot of libraries there isn't one central stateful scheduler in an atom, you can run as many as you like. 24 | 25 | ```clojure 26 | (future-cancel my-running-scheduler) 27 | ``` 28 | 29 | The schedule map is modelled after crontabs, you can specify _{:minute :hour :date :month :day}_ 30 | Each of these takes either a single value, or a list of values which when matched should fire the function. This means that you can use clojure's range and other list functions to generate for example every 5 minutes (range 0 60 5) 31 | 32 | Exceptions will be caught and a stacktrace printed rather than affect 33 | the execution of subsequent scheduled tasks. If you wish to handle 34 | exceptions, you must catch them within the function you pass to the 35 | scheduler. 36 | 37 | The scheduler runs in a single thread, so long running tasks have the 38 | potential to block future tasks from executing. If you have any tasks 39 | which take a non-trivial amount of time to run, it's recommended you 40 | fire a future from within the scheduler, allowing it to continue with 41 | other tasks. 42 | 43 | ## Changelog 44 | 45 | ### 1.0.1 46 | * Change caught exceptions to Throwable so nothing can kill the scheduler inadvertently 47 | 48 | ### 0.1.4 49 | 50 | * Change the initial delay to automatically start on the next minute boundary on the clock rather than a minute after invocation. 51 | * Enhance docstrings 52 | * Add in convenience functions for next-minute current-minute and secs-to-next-minute boundary 53 | 54 | ### 0.1.3 55 | 56 | * Bugfix - Schejulure no longer observes the sabbath (i.e. it works on Sundays) 57 | * Add in convenience weekends vector 58 | 59 | ## License 60 | 61 | Copyright © 2012-2013 Adam Clements 62 | 63 | Distributed under the Eclipse Public License, the same as Clojure. 64 | -------------------------------------------------------------------------------- /src/schejulure/core.clj: -------------------------------------------------------------------------------- 1 | (ns schejulure.core 2 | (:require [clj-time.core :refer [date-time year minute hour day month day-of-week 3 | minutes in-seconds interval plus now]] 4 | [clj-time.local :refer [local-now]]) 5 | (:import [java.util.concurrent Executors TimeUnit])) 6 | 7 | (def pool (Executors/newScheduledThreadPool 1)) 8 | 9 | (defn current-minute 10 | "Given a DateTime, truncate to the current minute (removing seconds 11 | and millisecond components)" 12 | [time] 13 | (apply date-time ((juxt year month day hour minute) time))) 14 | 15 | (defn next-minute 16 | "Given a DateTime, gives the next upcoming minute boundary" 17 | [time] 18 | (plus (current-minute time) (-> 1 minutes))) 19 | 20 | (defn secs-to-next-minute 21 | "Given a time, will give the number of additional seconds required to 22 | move into the next minute" 23 | [time] 24 | (inc (in-seconds (interval time (next-minute time))))) 25 | 26 | (defn call-every-minute 27 | "Schedules a function to be called every minute, within a second of 28 | the minute boundary on the clock" 29 | [f] 30 | (.scheduleAtFixedRate pool f (secs-to-next-minute (now)) 60 TimeUnit/SECONDS)) 31 | 32 | (defn cron-of 33 | "Returns a cron-like vector. Note that the days range from 1-7 instead 34 | of traditional cron's 0-6. 35 | 36 | Do not use this! Use the far more convenient map format instead." 37 | [time] 38 | [(minute time) 39 | (hour time) 40 | (day time) 41 | (month time) 42 | (day-of-week time)]) 43 | 44 | (defn has? [coll item] (some #{item} coll)) 45 | (defn all? [coll] (every? identity coll)) 46 | 47 | (defn cron-match? 48 | "Gives whether every element of a vector (a cron) can be found in the 49 | corresponding element of a vector of vectors (a cron range). 50 | 51 | e.g. 52 | (cron-match? [0 1] [[0 1 2] [0 1 2]]) => true 53 | (cron-match? [0 5] [[0 1 2] [0 1 2]]) => false" 54 | [cron cron-range] 55 | (all? (map has? cron-range cron))) 56 | 57 | (def cron-defaults {:minute (range 0 60) 58 | :hour (range 0 24) 59 | :date (range 1 32) 60 | :month (range 1 13) 61 | :day (range 1 8)}) 62 | 63 | (def day->number {:mon 1, 1 1 64 | :tue 2, 2 2 65 | :wed 3, 3 3 66 | :thu 4, 4 4 67 | :fri 5, 5 5 68 | :sat 6, 6 6 69 | :sun 7, 7 7, 0 7}) 70 | 71 | (def weekdays [:mon :tue :wed :thu :fri]) 72 | (def weekends [:sat :sun]) 73 | 74 | (defn keyword-day->number 75 | "Translates keywords e.g. :mon into the appropriate clj-time integer 76 | representation" 77 | [x] 78 | (if (coll? x) (map day->number x) 79 | (list (day->number x)))) 80 | 81 | (defn cronmap->cronrange [cronmap] 82 | (map (fn [x] (if (coll? x) x (list x))) 83 | (-> (merge cron-defaults cronmap) 84 | (update-in [:day] keyword-day->number) 85 | ((juxt :minute :hour :date :month :day))))) 86 | 87 | (defn fire-scheduled 88 | "Given a map of firing times to functions, checks whether the current 89 | local time matches any of them and calls the ones that do, presumably 90 | for side effects" 91 | [scheduled-fns] 92 | (let [now (cron-of (local-now))] 93 | (doseq [[schedule f] scheduled-fns] 94 | (when (cron-match? now (cronmap->cronrange schedule)) 95 | (try (f) 96 | (catch Throwable e 97 | (println "Caught exception in scheduled action " f " at " now) 98 | (.printStackTrace e))))))) 99 | 100 | (defn schedule 101 | "Takes pairs of cron-maps with the function to call when that cron-map 102 | matches the current time. 103 | 104 | The default cron-map executes every minute of every day, add elements 105 | into the map to restrict this, for example {:day [:mon :tue] :hour 5} 106 | will execute every minute between 5 and 6am on mondays and tuesdays 107 | where {:minute [15 45]} will execute and quarter past and quarter to 108 | the hour, every hour every day. 109 | 110 | If an exception is thrown and uncaught by your, execution will 111 | continue rather than interrupt future scheduled executions, if you 112 | need to handle the error your function must catch it. 113 | 114 | All the scheduled tasks run in a single thread, therefore long 115 | running tasks may impact the execution of subsequent tasks. If your 116 | task takes a non trivial amount of time, have the scheduler fire off 117 | a future rather than running it directly." 118 | [& args] 119 | (let [scheduled-fns (partition 2 args)] 120 | (call-every-minute #(fire-scheduled scheduled-fns)))) 121 | --------------------------------------------------------------------------------