Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,44 @@ Timely is a Clojure library for scheduling tasks according to a timetable, as an

The library is hosted at <a href="https://clojars.org/factual/timely">Clojars</a>. 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

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.

Expand Down Expand Up @@ -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)
```
12 changes: 5 additions & 7 deletions project.clj
Original file line number Diff line number Diff line change
@@ -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"]])
160 changes: 78 additions & 82 deletions src/timely/core.clj
Original file line number Diff line number Diff line change
@@ -1,123 +1,114 @@
(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))))))

(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
:wed 3
: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
representation"
[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
"Create a schedule representation based on parameters. Apply
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))

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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)
Expand All @@ -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))
Loading