zengod
zengod

Reputation: 1174

What's the simplest way to schedule a function to be called at midnight everyday with Clojure?

While one can use threads and set a timeout to run a function at set intervals from the time a new Thread has been started, how to make it so that the function runs at 00:00:00 starting from the next midnight?

Upvotes: 1

Views: 869

Answers (3)

rmcv
rmcv

Reputation: 1976

You can interop to any Java library like Quartz, cron4j or Java Timer itself. To make it more idiomatic to Clojure, you can also write a macro (or couple functions) to simplify things. For example, using a roll-your-own macro:

(deftask my-midnight-task
  "0 0 * * 2-6"
  (println "start to work")
  (do-something))

where "0 0 * * 2-6" is a cron like pattern used in cron4j. Implementation in here is using mount to take care of scheduling and descheduling:

(defonce ^Scheduler scheduler (doto (Scheduler.)
                                (.setDaemon true)
                                (.start)))

(defmacro deftask [name cron & body]
  `(defstate ~name
     :start (.schedule scheduler ~cron (fn [] ~@body))
     :stop  (.deschedule scheduler ~name)))

Upvotes: 1

Simon Polak
Simon Polak

Reputation: 1989

You could also take a look at chime library.

I did not test this code, but something like this.

(ns my.example
  (:require [chime :refer [chime-at]])
  (:import  [java.time Instant LocalTime ZonedDateTime ZoneId Period]))

(defn my-task []
  (println "Executing a task"))

(defn periodic-seq [^Instant start duration-or-period]
  (iterate #(.addTo duration-or-period %) start)) ;; produces a lazy-seq of Instants

;; generates infinite sequence of days. Change the time zone to the one you need. 
(def days 
  (periodic-seq (-> (LocalTime/of 23 0 0) 
                    (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                    .toInstant)
                (Period/ofDays 1)))

(chime-at days (fn [time] (my-task)))        

chime-at returns a zero arg function, that you can call to cancel the schedule. So you need to start the schedule like this.

(def cancel (chime-at days (fn [time] my-task)))
(cancel) 

Again, I haven't tested this code.

Upvotes: 2

Alan Thompson
Alan Thompson

Reputation: 29958

Just use a java.util.Timer: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Timer.html#schedule(java.util.TimerTask,java.util.Date)

public void schedule​(TimerTask task, Date time)

Schedules the specified task for execution at the specified time. If the time is in the past, the task is scheduled for immediate execution.

Parameters:
   task - task to be scheduled.
   time - time at which task is to be executed.

It looks like ScheduledThreadPoolExecutor is a bit newer and has some improvements. Esp see

scheduleAtFixedRate​(Runnable command, long initialDelay, 
                    long period, TimeUnit unit) 

Submits a periodic action that becomes enabled first after the given initial delay, and subsequently with the given period; that is, executions will commence after initialDelay, then initialDelay + period, then initialDelay + 2 * period, and so on.


You may also be interested in at-at library: https://github.com/overtone/at-at

Upvotes: 2

Related Questions