diff --git a/README.md b/README.md index e760dbf..58c61ae 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,26 @@ Timely is a Clojure library for scheduling tasks according to a timetable, as an The library is hosted at Clojars. Just add this to your dependencies: -``` -[factual/timely "0.0.3"] -``` +[![Clojars Project](https://img.shields.io/clojars/v/factual/timely.svg)](https://clojars.org/factual/timely) ## Setup -``` +```clojure (ns yournamespace.core (:require [timely.core :as timely])) - (timely/start-scheduler) + +(def scheduler (timely/start-scheduler)) ``` ## Schedule DSL and Cron Schedules are a structured way to represent cron syntax and are created using a DSL which reads much like an English sentence. To get a cron string from a schedule, use schedule-to-cron. For example: - timely.core> (schedule-to-cron (each-minute)) - "* * * * *" - +```clojure +timely.core> (schedule-to-cron (each-minute)) +"* * * * *" +``` + See the "Define Schedules" section below for more examples of the schedule DSL. ## Define Schedules @@ -32,12 +33,17 @@ See the "Define Schedules" section below for more examples of the schedule DSL. Define a scheduled-item using a schedule and a function to be executed on the defined schedule. For example: ```clojure +(defn test-print-fn [id] + (fn [] + (println "Task" id "scheduled at: " (java.util.Date.)))) + ;; Daily at 12:00am -(scheduled-item (daily) - (test-print-fn 1)) +(scheduled-item + (daily) + (test-print-fn 1)) ``` -(daily) creates a schedule that runs each day at 12:00am. (test-print-fn 1) returns a function that will print a message. The combined scheduled-item will print the message each day at 12:00am. +`(daily)` creates a schedule that runs each day at 12:00am. `(test-print-fn 1)` returns a function that will print a message. The combined scheduled-item will print the message each day at 12:00am. Specific start and end times can be optionally defined to ensure a repeated schedule is only valid for a certain time frame. This is a feature recognized by the Timely scheduler but does not exist in cron string syntax. @@ -116,19 +122,27 @@ The following are further examples of the dsl for defining schedules: (end-time 1337107380000)) (test-print-fn "specific-time-range")) ``` - + ## Run Schedules -Use (start-scheduler) to enable scheduling in your application. +Use `start-scheduler` to start schedules in your application: -Use start-schedule and end-schedule to start and stop schedules in your application: +```clojure +(def scheduler (start-scheduler)) +``` +Use `end-schedule` to deschedule a task with the specified id: ```clojure -(start-scheduler) (let [item (scheduled-item (each-minute) (test-print-fn "Scheduled using start-schedule"))] - (let [sched-id (start-schedule item)] + (let [sched-id (start-schedule scheduler item)] (Thread/sleep (* 1000 60 2)) - (end-schedule sched-id))) + (end-schedule scheduler sched-id))) +``` + +Use `stop-scheduler` to shutdown the scheduler instance: + +```clojure +(stop-scheduler scheduler) ``` diff --git a/project.clj b/project.clj index a49438e..7d4bbec 100644 --- a/project.clj +++ b/project.clj @@ -1,8 +1,6 @@ -(defproject factual/timely "0.0.3" - :main timely.core +(defproject factual/timely "0.0.4" :description "Timely: A clojure library for defining schedules and running them as an alternative to cron" - :dependencies [[clj-time "0.4.1"] - [org.clojure/clojure "1.4.0"] - [org.clojure/tools.logging "0.2.3"] - [org.clojars.ghoseb/cron4j "2.2.1"]] - :dev-dependencies [[lein-clojars "0.6.0"]]) + :dependencies [[clj-time "0.14.2"] + [org.clojure/clojure "1.8.0"] + [org.clojure/tools.logging "0.4.0"] + [it.sauronsoftware.cron4j/cron4j "2.2.5"]]) diff --git a/src/timely/core.clj b/src/timely/core.clj index 78317ae..5bbeb9c 100644 --- a/src/timely/core.clj +++ b/src/timely/core.clj @@ -1,23 +1,24 @@ (ns timely.core - (:require [clj-time.core :as dates]) - (:require [clj-time.coerce :as dates-coerce]) - (:require [clojure.string :only (join)]) - (:import [it.sauronsoftware.cron4j Scheduler]) - (:use [clojure.tools.logging :only (info debug error)])) + (:require + [clj-time.core :as dates] + [clj-time.coerce :as dates-coerce] + [clojure.string :refer [join]] + [clojure.tools.logging :refer [info debug error]]) + (:import it.sauronsoftware.cron4j.Scheduler)) (def time-types - (hash-set :minute :hour :day :month :day-of-week)) + #{:minute :hour :day :month :day-of-week}) (defn valid-time-type "Convert a time type to a valid value, else an exception is thrown" [type] - (let [type-transformed (condp = type - :minutes :minute - :hours :hour - :days :day - :months :month - :days-of-week :day-of-week - type)] + (let [type-transformed (case type + :minutes :minute + :hours :hour + :days :day + :months :month + :days-of-week :day-of-week + type)] (if (contains? time-types type-transformed) type-transformed (throw ( Exception. (str "Not a valid time type: " type)))))) @@ -25,9 +26,8 @@ (defn to-day-of-week "Convert a named day of the week to a number representation" [day-of-week] - (if (= :all day-of-week) - day-of-week - (condp = day-of-week + (case day-of-week + :all :all :sun 0 :mon 1 :tue 2 @@ -35,63 +35,54 @@ :thu 4 :fri 5 :sat 6 - (throw ( Exception. (str "Not a valid day of the week: " day-of-week)))))) + (throw ( Exception. (str "Not a valid day of the week: " day-of-week))))) (defn to-month "Convert a named month to a number representation" [month] - (if (= :all month) - month - (condp = month - :jan 1 - :feb 2 - :mar 3 - :apr 4 - :may 5 - :jun 6 - :jul 7 - :aug 8 - :sep 9 - :oct 10 - :nov 11 - :dec 12 - (throw ( Exception. (str "Not a valid month: " month)))))) + (condp = month + :all :all + :jan 1 + :feb 2 + :mar 3 + :apr 4 + :may 5 + :jun 6 + :jul 7 + :aug 8 + :sep 9 + :oct 10 + :nov 11 + :dec 12 + (throw ( Exception. (str "Not a valid month: " month))))) + +(defn- number-in-range [id n from to] + (when-not (number? n) + (throw (Exception. (str id " value: " n " must be a number")))) + (when-not (and (>= n from) (<= n to)) + (throw (Exception. (str id " value: " n " is outside of accepted range of " from "-" to)))) + (long n)) (defn to-minute "Convert to a valid minute number representation" [minute] (if (= :all minute) minute - (if (instance? Long minute) - (if (and (>= minute 0) - (<= minute 59)) - minute - (throw ( Exception. (str "Minute is out of accepted range: " minute ". Accepted range is 0-59")))) - (throw ( Exception. (str "Not a valid minute: " minute)))))) + (number-in-range "Minute" minute 0 59))) (defn to-hour "Convert to a valid hour number representation" [hour] (if (= :all hour) hour - (if (instance? Long hour) - (if (and (>= hour 0) - (<= hour 23)) - hour - (throw ( Exception. (str "Hour is out of accepted range: " hour ". Accepted range is 0-23")))) - (throw ( Exception. (str "Not a valid hour: " hour)))))) + (number-in-range "Hour" hour 0 23))) (defn to-day "Convert to a valid day number representation" [day] (if (= :all day) day - (if (instance? Long day) - (if (and (>= day 1) - (<= day 31)) - day - (throw ( Exception. (str "Day is out of accepted range: " day ". Accepted range is 1-31")))) - (throw ( Exception. (str "Not a valid day: " day)))))) + (number-in-range "Day" day 1 31))) (defn to-date-number "Convert a named date field value of type \"type\" to a number @@ -99,14 +90,14 @@ [type value] (if (contains? time-types type) (if (map? value) - (reduce #(assoc %1 (first %2) (to-date-number type (second %2))) {} value) - (condp = type - :day-of-week (to-day-of-week value) - :month (to-month value) - :minute (to-minute value) - :hour (to-hour value) - :day (to-day value) - value)) + (reduce (fn [m [k v]] (assoc m k (to-date-number type v))) {} value) + (case type + :day-of-week (to-day-of-week value) + :month (to-month value) + :minute (to-minute value) + :hour (to-hour value) + :day (to-day value) + value)) (throw ( Exception. (str "Not a valid time type: " type))))) (defn create-schedule @@ -114,10 +105,10 @@ filters: at, on, each, start-time, end-time" [minute hour day month day-of-week & filters] (apply merge - {:minute (to-date-number :minute minute) - :hour (to-date-number :hour hour) - :day (to-date-number :day day) - :month (to-date-number :month month) + {:minute (to-date-number :minute minute) + :hour (to-date-number :hour hour) + :day (to-date-number :day day) + :month (to-date-number :month month) :day-of-week (to-date-number :day-of-week day-of-week)} filters)) @@ -297,29 +288,29 @@ [timestamp] (let [date (dates/to-time-zone (dates-coerce/from-long timestamp) (dates/default-time-zone))] - (clojure.string/join " " - [ (dates/minute date) - (dates/hour date) - (dates/day date) - (dates/month date) - (dates/day-of-week date)]))) + (join " " + [(dates/minute date) + (dates/hour date) + (dates/day date) + (dates/month date) + (dates/day-of-week date)]))) (defn to-cron-entry "Convert a schedule date field value representation to a cron entry" [item] (cond (= :all item) "*" - (or (seq? item) (vector? item)) (clojure.string/join "," (map to-cron-entry item)) + (or (seq? item) (vector? item)) (join "," (map to-cron-entry item)) (map? item) (if-let [interval (:interval item)] (str "*/" interval) (str (:start item) "-" (:end item))) - (instance? Number item) item + (number? item) item :else (throw ( Exception. (str "Error in converting to cron: " item))))) (defn schedule-to-cron "Create a cron string from a schedule" [sched] - (clojure.string/join " " (map to-cron-entry [(sched :minute) (sched :hour) (sched :day) (sched :month) (sched :day-of-week)]))) + (join " " (map to-cron-entry [(sched :minute) (sched :hour) (sched :day) (sched :month) (sched :day-of-week)]))) (defn scheduled-item "Create a scheduled item using a schedule and a function to execute @@ -328,16 +319,13 @@ on intervals defined in the schedule." {:schedule schedule :work work})) -;; Single scheduler for Timely -(def SCHEDULER (Scheduler.)) - (defn end-schedule "Removes a schedule from Timely by descheduling based on a schedule id. The schedule id is a unique identifier that was generated upon starting a schedule." - [sched-id] + [scheduler sched-id] (info "Ending schedule:" sched-id) - (.deschedule SCHEDULER sched-id)) + (.deschedule scheduler sched-id)) (defn process-scheduled-item "Executes work for a scheduled item, but only if within optionally @@ -357,8 +345,8 @@ on intervals defined in the schedule." (defn begin-schedule "Begin a schedule, returning a unique id for the added schedule." - [work cron start-time end-time] - (.schedule SCHEDULER cron #(process-scheduled-item work start-time end-time))) + [scheduler work cron start-time end-time] + (.schedule scheduler cron #(process-scheduled-item work start-time end-time))) (defn to-date-obj [timestamp] @@ -369,7 +357,7 @@ on intervals defined in the schedule." "Adds the specified schedule to the scheduler based on start/end time restrictions. Returns a unique identifier for this schedule that can be used to later deschedule." - [{:keys [schedule work]}] + [scheduler {:keys [schedule work]}] (let [start_time (to-date-obj (:start-time schedule)) end_time (to-date-obj (:end-time schedule)) cron (schedule-to-cron schedule) @@ -379,8 +367,16 @@ on intervals defined in the schedule." (info "End date is before current time, not scheduling:" cron) (do (info "Starting schedule:" cron) - (begin-schedule work cron start_time end_time))))) + (begin-schedule scheduler work cron start_time end_time))))) (defn start-scheduler + "creates a new scheduler and starts it + returns an instance of the scheduler + that can be stopped using stop-scheduler" [] - (.start SCHEDULER)) + (doto (Scheduler.) (.start))) + +(defn stop-scheduler + "stops a running scheduler" + [scheduler] + (.stop scheduler)) diff --git a/test/timely/test/core.clj b/test/timely/test/core.clj index 71831ae..a638bff 100644 --- a/test/timely/test/core.clj +++ b/test/timely/test/core.clj @@ -1,11 +1,13 @@ (ns timely.test.core - (:use [timely.core]) - (:use [clojure.test]) - (:require [clj-time.core :as dates]) - (:require [clj-time.coerce :as dates-coerce])) + (:require [timely.core :refer :all] + [clojure.test :refer :all] + [clj-time.core :as dates] + [clj-time.coerce :as dates-coerce])) ;; Demo and testing +(def scheduler (start-scheduler)) + (defn to-utc-timestamp "Convert from clj-time date to a timestamp in the utc timezone" [date] @@ -28,11 +30,11 @@ [] (start-scheduler) (let [item (scheduled-item - (each-minute) - (test-print-fn "Scheduled using start-schedule"))] - (let [sched-id (start-schedule item)] + (each-minute) + (test-print-fn "Scheduled using start-schedule"))] + (let [sched-id (start-schedule scheduler item)] (Thread/sleep (* 1000 60 2)) - (end-schedule sched-id))) + (end-schedule scheduler sched-id))) (while true (Thread/sleep (* 1000 60))))