• Stars
    star
    537
  • Rank 82,649 (Top 2 %)
  • Language
    Clojure
  • Created over 11 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A really lightweight Clojure scheduler

Chime

Chime is a really lightweight Clojure scheduler.

Dependency

Add the following to your project.clj/deps.edn file:

[jarohen/chime "0.3.3"]
{jarohen/chime {:mvn/version "0.3.3"}}

The ‘Big Idea’™ behind Chime

The main goal of Chime was to create the simplest possible scheduler. Many scheduling libraries have gone before, most attempting to either mimic cron-style syntax, or creating whole DSLs of their own. This is all well and good, until your scheduling needs cannot be (easily) expressed using these syntaxes.

When returning to the grass roots of a what a scheduler actually is, we realised that a scheduler is really just a promise to execute a function at a (possibly infinite) sequence of times. So, that is exactly what Chime is - and no more!

Chime doesn’t really mind how you generate this sequence of times - in the spirit of composability you are free to choose whatever method you like! (yes, even including other cron-style/scheduling DSLs!)

When using Chime in other projects, I have settled on a couple of patterns (mainly involving the rather excellent time functions provided by java.time - more on this below.)

Usage

Chime consists of one main function, chime.core/chime-at.

chime-at is called with the sequence of times, and a callback function:

(require '[chime.core :as chime])
(import '[java.time Instant])

(let [now (Instant/now)]
  (chime/chime-at [(.plusSeconds now 2)
                   (.plusSeconds now 4)]
                  (fn [time]
                    (println "Chiming at" time))))

With chime-at, it is the caller’s responsibility to handle over-running jobs. chime-at will never execute jobs of the same scheduler in parallel or drop jobs. If a schedule is cancelled before a job is started, the job will not run.

chime-at returns an AutoCloseable that can be closed to cancel the schedule.

You can also pass an on-finished parameter to chime-at to run a callback when the schedule has finished (if it’s a finite schedule, of course!):

(let [now (Instant/now)]
  (chime/chime-at [(.plusSeconds now 2)
                   (.plusSeconds now 4)]

                  (fn [time]
                    (println "Chiming at" time))

                  {:on-finished (fn []
                                  (println "Schedule finished."))}))

Recurring schedules

To achieve recurring schedules, we can lazily generate an infinite sequence of times. This example runs every 5 minutes from now:

(import '[java.time Instant Duration])

(-> (chime/periodic-seq (Instant/now) (Duration/ofMinutes 5))
    rest) ; excludes *right now*

To start a recurring schedule at a particular time, you can combine this example with some standard Clojure functions. Let’s say you want to run a function at 8pm New York time every day. To generate the sequence of times, you’ll need to seed the call to periodic-seq with the next time you want the function to run:

(import '[java.time LocalTime ZonedDateTime ZoneId Period])

(chime/periodic-seq (-> (LocalTime/of 20 0 0)
                        (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                        .toInstant)
                    (Period/ofDays 1))

Complex schedules

Because there is no scheduling DSL included with Chime, the sorts of schedules that you can achieve are not limited to the scope of the DSL.

Instead, complex schedules can be expressed with liberal use of standard Clojure sequence-manipulation functions:

(import '[java.time ZonedDateTime ZoneId Period LocalTime DayOfWeek])

;; Every Tuesday and Friday:
(->> (chime/periodic-seq (-> (LocalTime/of 0 0)
                             (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                             .toInstant)
                         (Period/ofDays 1))

     (map #(.atZone % (ZoneId/of "America/New_York")))

     (filter (comp #{DayOfWeek/TUESDAY DayOfWeek/FRIDAY}
                   #(.getDayOfWeek %))))

;; Week-days
(->> (chime/periodic-seq ...)
     (map #(.atZone % (ZoneId/of "America/New_York")))
     (remove (comp #{DayOfWeek/SATURDAY DayOfWeek/SUNDAY}
                   #(.getDayOfWeek %))))

;; Last Monday of the month:
(->> (chime/periodic-seq ...)

     (map #(.atZone % (ZoneId/of "America/New_York")))

     ;; Get all the Mondays
     (filter (comp #{DayOfWeek/MONDAY}
                   #(.getDayOfWeek %)))

     ;; Split into months
     ;; (Make sure you use partition-by, not group-by -
     ;;  it's an infinite series!)
     (partition-by #(.getMonth %))

     ;; Only keep the last one in each month
     (map last))

;; 'Triple witching days':
;; (The third Fridays in March, June, September and December)
;; (see http://en.wikipedia.org/wiki/Triple_witching_day)

;; Here we have to revert the start day to the first day of the month
;; so that when we split by month, we know which Friday is the third
;; Friday.

(->> (chime/periodic-seq (-> (LocalTime/of 0 0)
                             (.adjustInto (-> (ZonedDateTime/now (ZoneId/of "America/New_York"))
                                              (.withDayOfMonth 1)))
                             .toInstant)
                         (Period/ofDays 1))

     (map #(.atZone % (ZoneId/of "America/New_York")))

     (filter (comp #{DayOfWeek/FRIDAY}
                   #(.getDayOfWeek %)))

     (filter (comp #{3 6 9 12}
                   #(.getMonthValue %)))

     ;; Split into months
     (partition-by #(.getMonthValue %))

     ;; Only keep the third one in each month
     (map #(nth % 2))

     (chime/without-past-times)))

Error handling

You can pass an error-handler to chime-at - a function that takes the exception as an argument. Return truthy from this function to continue the schedule, falsy to cancel it. By default, Chime will log the error and continue the schedule.

(chime-at [times...]
          do-task-fn
          {:error-handler (fn [e]
                            ;; log, alert, notify etc?
                            )})

core.async

If you already have Clojure’s core.async in your project, you may prefer chime.core-async/chime-ch

chime-ch is called with an ordered sequence of instants, and returns a channel that sends an event at each time in the sequence.

(require '[chime.core-async :refer [chime-ch]]
         '[clojure.core.async :as a :refer [<! go-loop]])

(let [now (Instant/now)
      chimes (chime-ch [(.plusSeconds now 2)
                        (.plusSeconds now 3)])]
  (a/<!! (go-loop []
           (when-let [msg (<! chimes)]
             (prn "Chiming at:" msg)
             (recur)))))

chime-ch uses an unbuffered channel, so cancelling a schedule is achieved simply by not reading from the channel.

You can also pass chime-ch a buffered channel as an optional argument. This is particularly useful if you need to specify the behaviour of the scheduler if one job overruns.

core.async has three main types of buffers: sliding, dropping and fixed. In these examples, imagining an hourly schedule, let’s say the 3pm run finishes at 5:10pm.

  • With a sliding-buffer (example below), the 4pm job would be cancelled, and the 5pm job started at 5:10.

  • With a dropping-buffer, the 4pm job would start at 5:10, but the 5pm job would be cancelled.

  • In the unbuffered example, above, the 4pm job would have been started at 5:10pm, and the 5pm job starting whenever that finished.

(require '[chime.core-async :refer [chime-ch]]
         '[clojure.core.async :as a :refer [<! go-loop]])

(let [chimes (chime-ch times {:ch (a/chan (a/sliding-buffer 1))})]
  (go-loop []
    (when-let [time (<! chimes)]
      ;; ...
      (recur))))

You can close! the channel returned by chime-ch to cancel the schedule.

Testing your integration with Chime

Testing time-dependent applications is always more challenging than other non-time-dependent systems. Chime makes this easier by allowing you to test the sequence of times independently from the execution of the scheduled job.

(Although, don’t forget to wrap your infinite sequences with (take x …​) when debugging!)

Bugs/thoughts/ideas/suggestions/patches etc

Please feel free to submit these through Github in the usual way!

Thanks!

Contributors

A big thanks to all of Chime’s contributors, a full list of whom are detailed in the Changelog.

License

Copyright © 2013+ James Henderson

Distributed under the Eclipse Public License, the same as Clojure.

Big thanks to Malcolm Sparks for providing the initial idea, as well as his other contributions and discussions.

More Repositories

1

chord

A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.
Clojure
438
star
2

nomad

A configuration library designed to allow Clojure applications to travel painlessly between different environments.
Clojure
213
star
3

yoyo

Yo-yo is a protocol-less, function composition-based alternative to Component
Clojure
87
star
4

phoenix

A plugin for configuring, co-ordinating and reloading Components
Clojure
69
star
5

simple-brepl

A really simple plugin to start CLJS browser REPLs, built atop 'Weasel'
Clojure
44
star
6

flow

Lightweight library to help you write dynamic CLJS webapps
Clojure
38
star
7

frodo

A lein plugin to start a Ring server via configuration in Nomad
Clojure
38
star
8

clidget

Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.
Clojure
24
star
9

graph-zip

A zipper library for Clojure that navigates graph structures
Clojure
19
star
10

bounce

Bounce is a protocol-less, function composition-based alternative to Component
Clojure
17
star
11

oak

A ClojureScript library to structure single-page apps - taking inspiration from the Elm Architecture
Clojure
13
star
12

with-open

A one-macro library to extend the behaviour of Clojure's 'with-open' without having to implement Closeable.
Clojure
12
star
13

splat

A Leiningen template to create ClojureScript single page web applications.
Clojure
12
star
14

selectable

An example CLJS app replicating David Nolan's 'CSP is Responsive Design' blog, with a slightly different approach.
Clojure
11
star
15

cljs-tetris

A Tetris clone written in ClojureScript + core.async
Clojure
8
star
16

embed-nrepl

A micro-library to start up an nREPL server with my opinionated defaults
Clojure
6
star
17

advent-of-code

Clojure
6
star
18

clojurex2013

My slides and references from 'Putting the Blocks Together' at ClojureX 2013
CSS
3
star
19

neo-zip

A Clojure library to query Neo4J databases using Graph-Zip syntax
Clojure
3
star
20

wiring

A Clojure library to configure and wire-up component-based applications
Clojure
2
star
21

multi-chat

A very basic multi-player chat webapp written using ClojureScript, WebSockets and Chord
Clojure
2
star
22

dot-emacs

My Emacs dotfiles
Emacs Lisp
1
star
23

eclj

A Clojure library to execute Clojure forms embedded inside text files - similar to ERB.
Clojure
1
star
24

ghost-middleware

Some Ring middleware to handle Google's AJAX shebang spec
Clojure
1
star
25

adventures

Clojure
1
star
26

datomic-arch-package

An attempt to package up Datomic on Arch Linux
Shell
1
star