-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathclon.lisp
320 lines (283 loc) · 13.2 KB
/
clon.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
(in-package :clon)
;;;; Generic interface
(defparameter *default-next-time-limit*
(encode-universal-time 0 0 0 1 1 3000)
"The default time limit for NEXT-TIME searches.")
(defgeneric next-time (schedule &key now allow-now-p limit)
(:documentation "Return the next time according to SCHEDULE or NIL
if there is no next time. If ALLOW-NOW-P the earliest possible time to
be returned is NOW, else it is usually NOW + the resolution of the
schedule. The default value of NOW is (GET-UNIVERSAL-TIME),
ALLOW-NOW-P is NIL and LIMIT is *DEFAULT-NEXT-TIME-LIMIT*"))
(defun make-scheduler (schedule &key (now (get-universal-time))
allow-now-p (limit *default-next-time-limit*))
"Return a `scheduler' function of no arguments that returns times
from NOW on by repeatedly calling NEXT-TIME on SCHEDULE. ALLOW-NOW-P
is passed to the first invocation of NEXT-TIME."
(lambda ()
(prog1
(setf now (next-time schedule :now now :allow-now-p allow-now-p
:limit limit))
(setf allow-now-p nil))))
(defun schedule-function (function scheduler &key name
(thread (bt:current-thread)))
"Create a timer just as with TRIVIAL-TIMERS:MAKE-TIMER but schedule
and reschedule FUNCTION according to SCHEDULER that is a function of
no parameters that returns a universal time or NIL. The returned timer
can be shut down with TRIVIAL-TIMERS:UNSCHEDULE-TIMER."
(let (timer)
(flet ((foo ()
(let ((next-time (funcall scheduler)))
(when next-time
(trivial-timers:schedule-timer timer next-time
:absolute-p t)))
(funcall function)))
(setf timer
(trivial-timers:make-timer #'foo
:name name
:thread thread)))
(let ((first-time (funcall scheduler)))
(when first-time
(trivial-timers:schedule-timer timer first-time :absolute-p t)))
timer))
;;;; Time utilities
;;; From sbcl's time.lisp
#-sbcl
(defun leap-years-before (year)
(let ((years (- year 1901)))
(+ (- (truncate years 4)
(truncate years 100))
(truncate (+ years 300) 400))))
(defun decode-universal-time* (time)
"Return the decoded time components as a list instead of multiple
values."
(multiple-value-list (decode-universal-time time)))
(defun encode-universal-time* (decoded-time)
"Encode DECODED-TIME that is a decoded time in list form such as one
that was returned by DECODE-UNIVERSAL-TIME*."
(apply #'encode-universal-time (subseq decoded-time 0 6)))
(defun days-of-month (decoded-time)
"Return the number of days in the month DECODED-TIME."
(let ((year (elt decoded-time 5))
(month (elt decoded-time 4)))
(+ (aref #(31 28 31 30 31 30 31 31 30 31 30 31) (1- month))
(if (= 2 month)
(- (leap-years-before (1+ year))
(leap-years-before year))
0))))
(defun min-valid-decoded-time-component (n)
"Return the smallest valid value for the Nth decoded time component."
(if (or (= n 3) (= n 4))
1
0))
(defun max-valid-decoded-time-component (n &optional decoded-time)
"Return the largest valid value for the Nth component of
DECODED-TIME or NIL if there is no limit. Passing DECODED-TIME is
necessary only for the day of month component because the number of
days in a month varies."
(ecase n
;; second and minute
((0 1) 59)
;; hour
((2) 23)
;; day of month
((3) (days-of-month decoded-time))
;; month
((4) 12)
;; year
((5) nil)
;; day of week
((6) 6)))
(defun valid-decoded-time-compenent-p (n value)
"See if value can ever be a valid value as the Nth component of a
decoded time."
(and (<= (min-valid-decoded-time-component n) value)
(let ((limit (max-valid-decoded-time-component n '(0 0 0 1 1 2000))))
(or (null limit) (<= value limit)))))
(defun zero-decoded-time-below (decoded-time n)
"Set the first N components of DECODED-TIME to their
MIN-VALID-DECODED-TIME-COMPONENT values."
(loop for i below n
do (setf (elt decoded-time i)
(min-valid-decoded-time-component i))))
(defun bump-decoded-time (time n &optional (m 1))
"Increment the Nth component of decoded TIME, handle overflows, zero
the lower components. Return changed decoded time and the highest index
that was changed."
(let ((max n))
(labels ((bump (time n)
(setf max (max max n))
(let ((limit (max-valid-decoded-time-component n time)))
(cond ((or (null limit) (< (elt time n) limit))
(incf (elt time n)))
(t
(setf (elt time n)
(min-valid-decoded-time-component n))
(bump time (1+ n)))))))
(loop repeat m do (bump time n))
(zero-decoded-time-below time max)
(values time max))))
;;;; Cron-like schedule
(defun next-bump (bumper decoded-time n)
"Invoke BUMPER on the Nth component of DECODED-TIME. Return its
value if it can ever be valid (not necessarily in the context of
DECODED-TIME) or else NIL."
(let* ((original (elt decoded-time n))
(next (cond ((null bumper) original)
((eql bumper '*) original)
((numberp bumper) bumper)
(t (funcall bumper original decoded-time n)))))
(when (and next (not (valid-decoded-time-compenent-p n next)))
(error "Invalid value ~S for decoded time component ~S." next n))
(if (or (null next)
(< next original)
(and (= n 3)
(< (days-of-month decoded-time) next)))
nil
next)))
(defun bump-day-of-month-and-day-of-week (dom-bumper dow-bumper decoded-time)
"Extra hair due to the circular dependency between DAY-OF-MONTH and
DAY-OF-WEEK bumpers. This function rolls the two bumpers into one."
;; First let DOM-BUMPER decide what's NEXT. If it is NIL we're done.
;; Else ask DOW-BUMPER what it thinks. If he likes it (i.e. returns
;; the same day of week) then NEXT is the result. Else skip to the
;; day of week it said or next Monday for NIL if it is still in the
;; same month. Repeat.
(let* ((decoded-time (copy-list decoded-time))
(last-day-of-month (days-of-month decoded-time)))
(flet ((skip-days (n)
(incf (elt decoded-time 3) n)
;; Keep DECODED-TIME consistent, update DAY-OF-WEEK.
(setf (elt decoded-time 6) (mod (+ (elt decoded-time 6) n) 7))))
(loop while (<= (elt decoded-time 3) last-day-of-month) do
(let* ((current-dom (elt decoded-time 3))
(next-dom (next-bump dom-bumper decoded-time 3)))
(unless next-dom
(return nil))
(skip-days (- next-dom current-dom))
(assert (<= next-dom last-day-of-month)))
(let* ((current-dow (elt decoded-time 6))
(next-dow (next-bump dow-bumper decoded-time 6)))
;; See if the dom is also a blessed dow.
(when (eql next-dow current-dow)
(return (elt decoded-time 3)))
;; Skip until the prescribed day or next Monday.
(skip-days (- (or next-dow 7) current-dow)))))))
(defclass cron-schedule ()
((bumpers :initarg :bumpers :reader bumpers
:documentation "The bumpers in decoded time component order."))
(:documentation "A cron-like schedule. See MAKE-CRON-SCHEDULE for details."))
(defun make-cron-schedule (&key second minute hour day-of-month
month year day-of-week)
"Construct a cron-like scheduler from the SECOND, MINUTE, etc
bumpers for components of decoded times (using the default time zone
for the time being). A bumper in its most generic from a function of
three arguments: the current value, the whole decoded time, and the
index of the current value in the decoded time. A bumper is expected
to return the smallest value that is valid for that component and not
less than the current value or NIL if it cannot find a valid value.
Returning a value that is smaller than the current one is the same as
returning NIL. A bumper can simply be a number which is equivalent to
\(CONSTANTLY NUMBER).
Bumpers are not allowed to depend on `lower' components of the decoded
time. The allowed dependency graph is this:
SECOND -> MINUTE -> HOUR -> (DAY-OF-MONTH <-> DAY-OF-WEEK) -> MONTH -> YEAR
That is, the SECOND bumper may look at the whole decoded time but the
MINUTE bumper may not look at seconds. DAY-OF-WEEK and DAY-OF-MONTH
may depend on each other to allow specifying the 'last Friday of the
month'.
The resolution of the schedule is defined implicitly by the lowest
bumper. NEXT-TIME always bumps the component of the decoded time that
belongs to the lowest bumper before trying to find mathces if its
LAST-TIME argument is not NIL. Of course, DAY-OF-WEEK is the odd one
out: it is the day-of-month component that is bumped if DAY-OF-WEEK is
the lowest bumper.
This scheme allows (MAKE-CRON-SCHEDULE :MONTH 12) trigger only once
per year instead of every second in December. For a more packed
schedule one can use the symbol '* as a bumper: (MAKE-CRON-SCHEDULE
:HOUR '* :MONTH 12) which makes hour the lowest bumper and the
schedule triggers every hour in December.
It is an error if all bumpers are NIL."
(let ((bumpers (list second minute hour day-of-month month year day-of-week)))
(unless (some #'identity bumpers)
(error "Cannot determine resolution of an empty schedule."))
(make-instance 'cron-schedule :bumpers bumpers)))
(defun bumper-index->component-index (i)
"Return the index of the decoded time component that is effect by
bumper I. Day of week is lumped together with day of month."
(if (= i 6) 3 i))
(defun lowest-component-index-with-a-bumper (bumpers)
"Return the the index of what is basically the root of current
dependency graph."
(loop for i upfrom 0
for bumper in bumpers
when bumper
minimize (bumper-index->component-index i)))
(defun bump-lowest-component (bumpers time)
"Bump the lowest component of decoded TIME that has a bumper. Return
it as a universal time."
(let ((decoded-time (decode-universal-time* time)))
(encode-universal-time*
(bump-decoded-time decoded-time
(lowest-component-index-with-a-bumper
bumpers)))))
(defmethod next-time ((schedule cron-schedule) &key (now (get-universal-time))
allow-now-p (limit *default-next-time-limit*))
(let ((bumpers (bumpers schedule)))
(unless allow-now-p
(setf now (bump-lowest-component bumpers now)))
(loop while (< now limit)
with n = 5
for decoded-time = (decode-universal-time* now)
for next = (if (= n 3)
(bump-day-of-month-and-day-of-week (elt bumpers 3)
(elt bumpers 6)
decoded-time)
(next-bump (elt bumpers n) decoded-time n))
do
(cond ((null next)
(when (= n 5)
;; The desired year is in the past, there is no next
;; time.
(return nil))
;; No valid value for this component, bump the next one
;; and come again.
(multiple-value-setq (decoded-time n)
(bump-decoded-time decoded-time (1+ n))))
(t
(when (< (elt decoded-time n) next)
(setf (elt decoded-time n) next)
(zero-decoded-time-below decoded-time n))
(decf n)))
(setf now (encode-universal-time* decoded-time))
(when (minusp n)
(return now)))))
;;;; The convenience case: typed cron schedule
(defun find-decoded-time-component-by-type (type value decoded-time n)
"Return the first valid value not less than VALUE that is of TYPE."
(loop with limit = (max-valid-decoded-time-component n decoded-time)
for x upfrom value upto limit
do (when (typep x type)
(return-from find-decoded-time-component-by-type x))))
(defun make-typed-cron-bumper (type)
"Return a bumper function suitable for MAKE-CRON-SCHEDULE that
returns the first valid value according to TYPE. Convenience function
on top of FIND-DECODED-TIME-COMPONENT-BY-TYPE."
(lambda (value decoded-time n)
(find-decoded-time-component-by-type type value decoded-time n)))
(defun make-typed-cron-schedule (&key second minute hour day-of-month
month year day-of-week)
"A convenience function much like MAKE-CRON-SCHEDULE but assumes
that no bumper can be a function designator so it must be a number,
the symbol * or a type specifier in which case it calls
MAKE-TYPED-CRON-BUMPER on it providing a terser syntax."
(flet ((convert-bumper (bumper)
(if (or (null bumper)
(typep bumper 'number)
(eq bumper '*))
bumper
(make-typed-cron-bumper bumper))))
(let ((bumpers (list second minute hour day-of-month month year
day-of-week)))
(make-instance 'cron-schedule
:bumpers (mapcar #'convert-bumper bumpers)))))