Chime
Chime是一个非常轻量级的Clojure调度程序。
我建议你去西西的“滴答”库西西库看看——有西西的支持,它可能会得到比我能给Chime更多的社区支持。
感谢您对我的帮助和支持!
詹姆斯
依赖
在project.clj文件中添加依赖:
[jarohen/chime "0.2.2"]
编钟背后的“大创意”
Chime的主要目标是创建尽可能简单的调度程序。许多调度库以前都有过类似的经历,大多数都试图模仿cron风格的语法,或者创建自己的整个dsl。这一切都很好,直到您的调度需求不能(容易地)使用这些语法表达。
当我们回到调度器的基本概念时,我们意识到调度器实际上只是一个承诺,在一个(可能是无限的)序列上执行一个函数。所以,这就是真正的钟声-没有更多!
Chime并不介意您如何生成这个时间序列——本着可组合性的精神,您可以自由选择您喜欢的任何方法!(是的,甚至包括其他cron样式/调度dsl !)
在其他项目中使用Chime时,我已经确定了几个模式(clj-time提供的非常优秀的时间函数——下面将详细介绍)。
使用
编钟由两个主要功能组成:chime-ch and chime-at.。
这里我们使用cljtime的时间函数来生成Joda时间序列。
chime-ch
使用Joda times的有序序列调用chime-ch,并返回一个通道,该通道每次在序列中发送一个事件。
(:require [chime :refer [chime-ch]]
[clj-time.core :as t]
[clojure.core.async :as a :refer [<! go-loop]])
(let [chimes (chime-ch [(-> 2 t/seconds t/from-now)
(-> 3 t/seconds t/from-now)])]
(a/<!! (go-loop []
(when-let [msg (<! chimes)]
(prn "Chiming at:" msg)
(recur)))))
chime-ch使用非缓冲通道,因此取消调度只需不从通道读取即可。
您还可以将chime-ch缓冲通道作为可选参数传递。如果您需要在一个作业超时时指定调度程序的行为,这一点特别有用。
core.async 有三种主要类型的缓冲区:sliding, dropping and fixed(滑动、删除和固定)。在这些例子中,
假设一个小时的时间表,假设下午3点的运行在下午5点10分结束。
- 使用一个sliding-buffer(滑动缓冲区)(如下面的示例),4pm作业将被取消,5pm作业将在5:10开始。
- 使用dropping-buffer, 4pm作业将在5:10开始,但是5pm作业将被取消。
- 在上面的非缓冲示例中,4pm作业应该在下午5:10开始,而5pm作业则在该作业结束时开始。
(:require [chime :refer [chime-ch]]
[clj-time.core :as t]
[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))))
你可以关闭!由chime-ch返回的频道取消了行程。
Chime-at
另一方面,调用chime-at的时间序列,并调用一个回调函数:
(:require [chime :refer [chime-at]]
[clj-time.core :as t])
(chime-at [(-> 2 t/seconds t/from-now)
(-> 4 t/seconds t/from-now)]
(fn [time]
(println "Chiming at" time)))
对于chime-at,调用者的责任是处理超负荷的作业。chime-at永远不会并行执行相同调度程序的作业或删除作业。如果在作业启动之前取消了调度,则作业将不会运行。
chime-at 返回一个零参数函数,可以调用该函数来取消调度。
您还可以将 on-finished 参数传递给 chime-at ,以便在计划完成时运行回调(当然,如果是一个有限的计划):
(chime-at [(-> 2 t/seconds t/from-now) (-> 4 t/seconds t/from-now)]
(fn [time]
(println "Chiming at" time))
{:on-finished (fn []
(println "Schedule finished."))})
反复出现的时间表
为了实现循环调度,我们可以使用新的(从0.5.0开始)clj-time periodico -seq函数惰性地生成无限次序列。这个例子从现在开始每5分钟运行一次:
(:require [clj-time.core :as t]
[clj-time.periodic :refer [periodic-seq]])
(rest ; excludes *right now*
(periodic-seq (t/now)
(-> 5 t/minutes)))
要在特定时间启动重复调度,您可以将此示例与一些标准Clojure函数结合使用。假设您想在纽约时间每天下午8点运行一个函数。要生成时间序列,您需要在下次运行函数时将调用种子周期seq:
(:require [clj-time.core :as t])
(:import [org.joda.time DateTimeZone])
(->> (periodic-seq (.. (t/now)
(withZone (DateTimeZone/forID "America/New_York"))
(withTime 20 0 0 0))
(-> 1 t/days)))
编钟确实会删除已经从您的时间序列前面经过的任何时间(条件是序列是有序的),所以不管今天下午8点是否已经过去,编钟都会优雅地处理这个问题。
复杂的时间表
因为Chime中没有包含调度DSL,所以您可以实现的调度类型并不局限于DSL的范围。
相反,复杂的调度可以通过自由使用标准Clojure序列操作函数来表示:
(:require [clj-time.core :as t])
(:import [org.joda.time DateTimeConstants DateTimeZone])
;; 每周二和周五:
(->> (periodic-seq (.. (t/now)
(withZone (DateTimeZone/forID "America/New_York"))
(withTime 0 0 0 0))
(-> 1 t/days))
(filter (comp #{DateTimeConstants/TUESDAY
DateTimeConstants/FRIDAY}
#(.getDayOfWeek %))))
;; 工作日
(->> (periodic-seq ...)
(remove (comp #{DateTimeConstants/SATURDAY
DateTimeConstants/SUNDAY}
#(.getDayOfWeek %))))
;; 每月最后一个星期一:
(->> (periodic-seq (.. (t/now)
(withZone (DateTimeZone/forID "America/New_York"))
(withTime 0 0 0 0))
(-> 1 t/days))
;; 获取所有的周一
(filter (comp #{DateTimeConstants/MONDAY}
#(.getDayOfWeek %)))
;; 分成几个月
;; (Make sure you use partition-by, not group-by -
;; it's an infinite series!)
(partition-by #(.getMonthOfYear %))
;; 每个月只保留最后一个
(map last))
;; '三重魅力天:
;; (三月、六月、九月及十二月的第三个星期五)
;; (see http://en.wikipedia.org/wiki/Triple_witching_day)
;;在这里,我们必须将开始日期还原为每月的第一天
;;所以当我们按月分开时,我们知道哪个星期五是第三个
;;星期五。(任何已经过去的时间都将被删除,as
;;之前)
(->> (periodic-seq (.. (t/now)
(withZone (DateTimeZone/forID "America/New_York"))
(withTime 0 0 0 0)
(withDayOfMonth 1)
(-> 1 t/days))
(filter (comp #{DateTimeConstants/FRIDAY}
#(.getDayOfWeek %)))
(filter (comp #{3 6 9 12}
#(.getMonthOfYear %)))
;; Split into months
(partition-by #(.getMonthOfYear %))
;; Only keep the third one in each month
(map #(nth % 2))))
这是一种与其他调度库完全不同的方法,因此我非常有兴趣听听您的想法!
错误处理
从0.1.1开始,您可以将错误处理程序传递给chime-at -a函数,该函数接受异常作为参数。您可以重新抛出它,以防止将来出现计划的任务;或者把它压扁,下次再试。
默认情况下,Chime将重新将错误抛出到线程的未捕获异常处理程序。
(chime-at [times...]
do-task-fn
{:error-handler (fn [e]
;; log, alert, notify etc?
)})
行为(t /now)
有时,您需要一个类似于“ every ”的时间表。这里的诱惑是创建一个时间序列:(periodic-seq (t/now) (t/minutes 5))——然而,这可能导致不确定性行为。有时,时钟会立即运行该函数,有时则不会。
造成这种情况的原因有两个因素:
- 钟声从你的序列中移除过去的时间。这样,当你想要一个像“每天早上6点”的时间表时,你可以通过(periodic-seq
(t/today-at 6 0) (t/days 1)),而不用担心今天早上6点是否已经过去了。 - 从您现在的呼叫到(t/now)到过去几次的报时支票之间有一点延迟。Chime(与joda-time类似)将时间解析为最近的毫秒,因此,如果这两个检查在同一毫秒内发生,您的调度将立即运行—如果没有,则不会运行。
解决这个问题的方法是将(t/now)排除在时间表之外——通过类似于(rest (periodici -seq (t/now) (t/minutes 5))的方法来实现。
测试您与Chime的集成
测试依赖于时间的应用程序总是比其他非依赖于时间的系统更具挑战性。通过允许您独立于计划作业的执行来测试时间序列,Chime使这一点更加容易。
(尽管,调试时不要忘记使用(take x…)来包装无限序列!)
错误/想法/意见/建议/补丁等等
请随意通过Github以通常的方式提交这些内容!
谢谢!
贡献者
非常感谢Chime的所有贡献者,他们的完整列表在变更日志中有详细说明。