scheduled 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/scheduled/cron_parser.rb +237 -235
- data/lib/scheduled/instrumenters.rb +13 -0
- data/lib/scheduled/version.rb +1 -1
- data/lib/scheduled.rb +153 -33
- metadata +8 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: db4f2341051e6e731d151c2c77742da414d297e42ea9bd5935d0621a36936c58
|
4
|
+
data.tar.gz: 1eb6ed8ce17b349fb007b84eeed4b1ab6da028e7fc08fa3af0a4e7e0551970a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2aeea0f548aeb7b8dc342b0b7d1e861182f32f752e51c9a5ff2448b3d525b09c96a46986cbdfdf35767ee63341499a5a061bb8d3ff704d1aa5918204907ef953
|
7
|
+
data.tar.gz: b67bfea285af8662eeea8901a55e92d39d2a28f3afb5fb6d2f4d97b3a1338ca64dc9e9faef7e2351e7e5c77e22be3eeba7f10aa3916eb629d0bb814212d7c43a
|
@@ -25,298 +25,300 @@
|
|
25
25
|
require 'set'
|
26
26
|
require 'date'
|
27
27
|
|
28
|
-
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
28
|
+
module Scheduled
|
29
|
+
# Parses cron expressions and computes the next occurence of the "job"
|
30
|
+
#
|
31
|
+
class CronParser
|
32
|
+
# internal "mutable" time representation
|
33
|
+
class InternalTime
|
34
|
+
attr_accessor :year, :month, :day, :hour, :min
|
35
|
+
attr_accessor :time_source
|
36
|
+
|
37
|
+
def initialize(time,time_source = Time)
|
38
|
+
@year = time.year
|
39
|
+
@month = time.month
|
40
|
+
@day = time.day
|
41
|
+
@hour = time.hour
|
42
|
+
@min = time.min
|
43
|
+
|
44
|
+
@time_source = time_source
|
45
|
+
end
|
42
46
|
|
43
|
-
|
44
|
-
|
47
|
+
def to_time
|
48
|
+
time_source.local(@year, @month, @day, @hour, @min, 0)
|
49
|
+
end
|
45
50
|
|
46
|
-
|
47
|
-
|
51
|
+
def inspect
|
52
|
+
[year, month, day, hour, min].inspect
|
53
|
+
end
|
48
54
|
end
|
49
55
|
|
50
|
-
|
51
|
-
|
56
|
+
SYMBOLS = {
|
57
|
+
"jan" => "1",
|
58
|
+
"feb" => "2",
|
59
|
+
"mar" => "3",
|
60
|
+
"apr" => "4",
|
61
|
+
"may" => "5",
|
62
|
+
"jun" => "6",
|
63
|
+
"jul" => "7",
|
64
|
+
"aug" => "8",
|
65
|
+
"sep" => "9",
|
66
|
+
"oct" => "10",
|
67
|
+
"nov" => "11",
|
68
|
+
"dec" => "12",
|
69
|
+
|
70
|
+
"sun" => "0",
|
71
|
+
"mon" => "1",
|
72
|
+
"tue" => "2",
|
73
|
+
"wed" => "3",
|
74
|
+
"thu" => "4",
|
75
|
+
"fri" => "5",
|
76
|
+
"sat" => "6"
|
77
|
+
}
|
78
|
+
|
79
|
+
def initialize(source,time_source = Time)
|
80
|
+
@source = interpret_vixieisms(source)
|
81
|
+
@time_source = time_source
|
82
|
+
validate_source
|
52
83
|
end
|
53
|
-
end
|
54
|
-
|
55
|
-
SYMBOLS = {
|
56
|
-
"jan" => "1",
|
57
|
-
"feb" => "2",
|
58
|
-
"mar" => "3",
|
59
|
-
"apr" => "4",
|
60
|
-
"may" => "5",
|
61
|
-
"jun" => "6",
|
62
|
-
"jul" => "7",
|
63
|
-
"aug" => "8",
|
64
|
-
"sep" => "9",
|
65
|
-
"oct" => "10",
|
66
|
-
"nov" => "11",
|
67
|
-
"dec" => "12",
|
68
|
-
|
69
|
-
"sun" => "0",
|
70
|
-
"mon" => "1",
|
71
|
-
"tue" => "2",
|
72
|
-
"wed" => "3",
|
73
|
-
"thu" => "4",
|
74
|
-
"fri" => "5",
|
75
|
-
"sat" => "6"
|
76
|
-
}
|
77
|
-
|
78
|
-
def initialize(source,time_source = Time)
|
79
|
-
@source = interpret_vixieisms(source)
|
80
|
-
@time_source = time_source
|
81
|
-
validate_source
|
82
|
-
end
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
85
|
+
def interpret_vixieisms(spec)
|
86
|
+
case spec
|
87
|
+
when '@reboot'
|
88
|
+
raise ArgumentError, "Can't predict last/next run of @reboot"
|
89
|
+
when '@yearly', '@annually'
|
90
|
+
'0 0 1 1 *'
|
91
|
+
when '@monthly'
|
92
|
+
'0 0 1 * *'
|
93
|
+
when '@weekly'
|
94
|
+
'0 0 * * 0'
|
95
|
+
when '@daily', '@midnight'
|
96
|
+
'0 0 * * *'
|
97
|
+
when '@hourly'
|
98
|
+
'0 * * * *'
|
99
|
+
else
|
100
|
+
spec
|
101
|
+
end
|
100
102
|
end
|
101
|
-
end
|
102
103
|
|
103
104
|
|
104
|
-
|
105
|
-
|
106
|
-
|
105
|
+
# returns the next occurence after the given date
|
106
|
+
def next(now = @time_source.now, num = 1)
|
107
|
+
t = InternalTime.new(now, @time_source)
|
107
108
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
unless time_specs[:month][0].include?(t.month)
|
110
|
+
nudge_month(t)
|
111
|
+
t.day = 0
|
112
|
+
end
|
112
113
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
114
|
+
unless interpolate_weekdays(t.year, t.month)[0].include?(t.day)
|
115
|
+
nudge_date(t)
|
116
|
+
t.hour = -1
|
117
|
+
end
|
117
118
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
unless time_specs[:hour][0].include?(t.hour)
|
120
|
+
nudge_hour(t)
|
121
|
+
t.min = -1
|
122
|
+
end
|
122
123
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
# always nudge the minute
|
125
|
+
nudge_minute(t)
|
126
|
+
t = t.to_time
|
127
|
+
if num > 1
|
128
|
+
recursive_calculate(:next,t,num)
|
129
|
+
else
|
130
|
+
t
|
131
|
+
end
|
130
132
|
end
|
131
|
-
end
|
132
133
|
|
133
|
-
|
134
|
-
|
135
|
-
|
134
|
+
# returns the last occurence before the given date
|
135
|
+
def last(now = @time_source.now, num=1)
|
136
|
+
t = InternalTime.new(now,@time_source)
|
136
137
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
138
|
+
unless time_specs[:month][0].include?(t.month)
|
139
|
+
nudge_month(t, :last)
|
140
|
+
t.day = 32
|
141
|
+
end
|
141
142
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
143
|
+
if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day)
|
144
|
+
nudge_date(t, :last)
|
145
|
+
t.hour = 24
|
146
|
+
end
|
146
147
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
148
|
+
unless time_specs[:hour][0].include?(t.hour)
|
149
|
+
nudge_hour(t, :last)
|
150
|
+
t.min = 60
|
151
|
+
end
|
151
152
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
153
|
+
# always nudge the minute
|
154
|
+
nudge_minute(t, :last)
|
155
|
+
t = t.to_time
|
156
|
+
if num > 1
|
157
|
+
recursive_calculate(:last,t,num)
|
158
|
+
else
|
159
|
+
t
|
160
|
+
end
|
159
161
|
end
|
160
|
-
end
|
161
162
|
|
162
163
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
else
|
170
|
-
if SUBELEMENT_REGEX === subel
|
171
|
-
if $5 # with range
|
172
|
-
stepped_range($1.to_i..$3.to_i, $5.to_i)
|
173
|
-
elsif $3 # range without step
|
174
|
-
stepped_range($1.to_i..$3.to_i, 1)
|
175
|
-
else # just a numeric
|
176
|
-
[$1.to_i]
|
177
|
-
end
|
164
|
+
SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$}
|
165
|
+
def parse_element(elem, allowed_range)
|
166
|
+
values = elem.split(',').map do |subel|
|
167
|
+
if subel =~ /^\*/
|
168
|
+
step = subel.length > 1 ? subel[2..-1].to_i : 1
|
169
|
+
stepped_range(allowed_range, step)
|
178
170
|
else
|
179
|
-
|
171
|
+
if SUBELEMENT_REGEX === subel
|
172
|
+
if $5 # with range
|
173
|
+
stepped_range($1.to_i..$3.to_i, $5.to_i)
|
174
|
+
elsif $3 # range without step
|
175
|
+
stepped_range($1.to_i..$3.to_i, 1)
|
176
|
+
else # just a numeric
|
177
|
+
[$1.to_i]
|
178
|
+
end
|
179
|
+
else
|
180
|
+
raise ArgumentError, "Bad Vixie-style specification #{subel}"
|
181
|
+
end
|
180
182
|
end
|
181
|
-
end
|
182
|
-
end.flatten.sort
|
183
|
+
end.flatten.sort
|
183
184
|
|
184
|
-
|
185
|
-
end
|
186
|
-
|
187
|
-
|
188
|
-
protected
|
189
|
-
|
190
|
-
def recursive_calculate(meth,time,num)
|
191
|
-
array = [time]
|
192
|
-
num.-(1).times do |num|
|
193
|
-
array << self.send(meth, array.last)
|
185
|
+
[Set.new(values), values, elem]
|
194
186
|
end
|
195
|
-
array
|
196
|
-
end
|
197
187
|
|
198
|
-
# returns a list of days which do both match time_spec[:dom] or time_spec[:dow]
|
199
|
-
def interpolate_weekdays(year, month)
|
200
|
-
@_interpolate_weekdays_cache ||= {}
|
201
|
-
@_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month)
|
202
|
-
end
|
203
188
|
|
204
|
-
|
205
|
-
t = Date.new(year, month, 1)
|
206
|
-
valid_mday, _, mday_field = time_specs[:dom]
|
207
|
-
valid_wday, _, wday_field = time_specs[:dow]
|
189
|
+
protected
|
208
190
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
191
|
+
def recursive_calculate(meth,time,num)
|
192
|
+
array = [time]
|
193
|
+
num.-(1).times do
|
194
|
+
array << self.send(meth, array.last)
|
195
|
+
end
|
196
|
+
array
|
214
197
|
end
|
215
|
-
# Careful: crontabs may use either 0 or 7 for Sunday:
|
216
|
-
valid_wday << 0 if valid_wday.include?(7)
|
217
198
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
199
|
+
# returns a list of days which do both match time_spec[:dom] or time_spec[:dow]
|
200
|
+
def interpolate_weekdays(year, month)
|
201
|
+
@_interpolate_weekdays_cache ||= {}
|
202
|
+
@_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month)
|
222
203
|
end
|
223
204
|
|
224
|
-
|
225
|
-
|
205
|
+
def interpolate_weekdays_without_cache(year, month)
|
206
|
+
t = Date.new(year, month, 1)
|
207
|
+
valid_mday, _, mday_field = time_specs[:dom]
|
208
|
+
valid_wday, _, wday_field = time_specs[:dow]
|
226
209
|
|
227
|
-
|
228
|
-
|
229
|
-
|
210
|
+
# Careful, if both DOW and DOM fields are non-wildcard,
|
211
|
+
# then we only need to match *one* for cron to run the job:
|
212
|
+
if not (mday_field == '*' and wday_field == '*')
|
213
|
+
valid_mday = [] if mday_field == '*'
|
214
|
+
valid_wday = [] if wday_field == '*'
|
215
|
+
end
|
216
|
+
# Careful: crontabs may use either 0 or 7 for Sunday:
|
217
|
+
valid_wday << 0 if valid_wday.include?(7)
|
230
218
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
219
|
+
result = []
|
220
|
+
while t.month == month
|
221
|
+
result << t.mday if valid_mday.include?(t.mday) || valid_wday.include?(t.wday)
|
222
|
+
t = t.succ
|
223
|
+
end
|
235
224
|
|
236
|
-
|
225
|
+
[Set.new(result), result]
|
226
|
+
end
|
237
227
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
end
|
228
|
+
def nudge_year(t, dir = :next)
|
229
|
+
t.year = t.year + (dir == :next ? 1 : -1)
|
230
|
+
end
|
242
231
|
|
243
|
-
|
244
|
-
|
245
|
-
|
232
|
+
def nudge_month(t, dir = :next)
|
233
|
+
spec = time_specs[:month][1]
|
234
|
+
next_value = find_best_next(t.month, spec, dir)
|
235
|
+
t.month = next_value || (dir == :next ? spec.first : spec.last)
|
246
236
|
|
247
|
-
|
248
|
-
spec = interpolate_weekdays(t.year, t.month)[1]
|
249
|
-
next_value = find_best_next(t.day, spec, dir)
|
250
|
-
t.day = next_value || (dir == :next ? spec.first : spec.last)
|
237
|
+
nudge_year(t, dir) if next_value.nil?
|
251
238
|
|
252
|
-
|
253
|
-
|
239
|
+
# we changed the month, so its likely that the date is incorrect now
|
240
|
+
valid_days = interpolate_weekdays(t.year, t.month)[1]
|
241
|
+
t.day = dir == :next ? valid_days.first : valid_days.last
|
242
|
+
end
|
254
243
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
t.hour = next_value || (dir == :next ? spec.first : spec.last)
|
244
|
+
def date_valid?(t, dir = :next)
|
245
|
+
interpolate_weekdays(t.year, t.month)[0].include?(t.day)
|
246
|
+
end
|
259
247
|
|
260
|
-
nudge_date(t, dir
|
261
|
-
|
248
|
+
def nudge_date(t, dir = :next, can_nudge_month = true)
|
249
|
+
spec = interpolate_weekdays(t.year, t.month)[1]
|
250
|
+
next_value = find_best_next(t.day, spec, dir)
|
251
|
+
t.day = next_value || (dir == :next ? spec.first : spec.last)
|
262
252
|
|
263
|
-
|
264
|
-
|
265
|
-
next_value = find_best_next(t.min, spec, dir)
|
266
|
-
t.min = next_value || (dir == :next ? spec.first : spec.last)
|
253
|
+
nudge_month(t, dir) if next_value.nil? && can_nudge_month
|
254
|
+
end
|
267
255
|
|
268
|
-
nudge_hour(t, dir
|
269
|
-
|
256
|
+
def nudge_hour(t, dir = :next)
|
257
|
+
spec = time_specs[:hour][1]
|
258
|
+
next_value = find_best_next(t.hour, spec, dir)
|
259
|
+
t.hour = next_value || (dir == :next ? spec.first : spec.last)
|
270
260
|
|
271
|
-
|
272
|
-
@time_specs ||= begin
|
273
|
-
# tokens now contains the 5 fields
|
274
|
-
tokens = substitute_parse_symbols(@source).split(/\s+/)
|
275
|
-
{
|
276
|
-
:minute => parse_element(tokens[0], 0..59), #minute
|
277
|
-
:hour => parse_element(tokens[1], 0..23), #hour
|
278
|
-
:dom => parse_element(tokens[2], 1..31), #DOM
|
279
|
-
:month => parse_element(tokens[3], 1..12), #mon
|
280
|
-
:dow => parse_element(tokens[4], 0..6) #DOW
|
281
|
-
}
|
261
|
+
nudge_date(t, dir) if next_value.nil?
|
282
262
|
end
|
283
|
-
end
|
284
263
|
|
285
|
-
|
286
|
-
|
287
|
-
|
264
|
+
def nudge_minute(t, dir = :next)
|
265
|
+
spec = time_specs[:minute][1]
|
266
|
+
next_value = find_best_next(t.min, spec, dir)
|
267
|
+
t.min = next_value || (dir == :next ? spec.first : spec.last)
|
268
|
+
|
269
|
+
nudge_hour(t, dir) if next_value.nil?
|
288
270
|
end
|
289
|
-
end
|
290
271
|
|
272
|
+
def time_specs
|
273
|
+
@time_specs ||= begin
|
274
|
+
# tokens now contains the 5 fields
|
275
|
+
tokens = substitute_parse_symbols(@source).split(/\s+/)
|
276
|
+
{
|
277
|
+
:minute => parse_element(tokens[0], 0..59), #minute
|
278
|
+
:hour => parse_element(tokens[1], 0..23), #hour
|
279
|
+
:dom => parse_element(tokens[2], 1..31), #DOM
|
280
|
+
:month => parse_element(tokens[3], 1..12), #mon
|
281
|
+
:dow => parse_element(tokens[4], 0..6) #DOW
|
282
|
+
}
|
283
|
+
end
|
284
|
+
end
|
291
285
|
|
292
|
-
|
293
|
-
|
286
|
+
def substitute_parse_symbols(str)
|
287
|
+
SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)|
|
288
|
+
s.gsub(symbol, replacement)
|
289
|
+
end
|
290
|
+
end
|
294
291
|
|
295
|
-
num = len.div(step)
|
296
|
-
result = (0..num).map { |i| rng.first + step * i }
|
297
292
|
|
298
|
-
|
299
|
-
|
300
|
-
end
|
293
|
+
def stepped_range(rng, step = 1)
|
294
|
+
len = rng.last - rng.first
|
301
295
|
|
296
|
+
num = len.div(step)
|
297
|
+
result = (0..num).map { |i| rng.first + step * i }
|
302
298
|
|
303
|
-
|
304
|
-
|
305
|
-
def find_best_next(current, allowed, dir)
|
306
|
-
if dir == :next
|
307
|
-
allowed.sort.find { |val| val > current }
|
308
|
-
else
|
309
|
-
allowed.sort.reverse.find { |val| val < current }
|
299
|
+
result.pop if result[-1] == rng.last and rng.exclude_end?
|
300
|
+
result
|
310
301
|
end
|
311
|
-
end
|
312
302
|
|
313
|
-
|
314
|
-
|
315
|
-
|
303
|
+
|
304
|
+
# returns the smallest element from allowed which is greater than current
|
305
|
+
# returns nil if no matching value was found
|
306
|
+
def find_best_next(current, allowed, dir)
|
307
|
+
if dir == :next
|
308
|
+
allowed.sort.find { |val| val > current }
|
309
|
+
else
|
310
|
+
allowed.sort.reverse.find { |val| val < current }
|
311
|
+
end
|
316
312
|
end
|
317
|
-
|
318
|
-
|
319
|
-
|
313
|
+
|
314
|
+
def validate_source
|
315
|
+
unless @source.respond_to?(:split)
|
316
|
+
raise ArgumentError, 'not a valid cronline'
|
317
|
+
end
|
318
|
+
source_length = @source.split(/\s+/).length
|
319
|
+
unless source_length >= 5 && source_length <= 6
|
320
|
+
raise ArgumentError, 'not a valid cronline'
|
321
|
+
end
|
320
322
|
end
|
321
323
|
end
|
322
324
|
end
|
data/lib/scheduled/version.rb
CHANGED
data/lib/scheduled.rb
CHANGED
@@ -1,60 +1,180 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
2
4
|
require "concurrent"
|
3
5
|
require "scheduled/cron_parser"
|
6
|
+
require "scheduled/instrumenters"
|
4
7
|
|
8
|
+
##
|
9
|
+
# Schedule jobs to run at specific intervals.
|
10
|
+
#
|
5
11
|
module Scheduled
|
12
|
+
# @api private
|
6
13
|
Job = Struct.new(:last_run)
|
7
14
|
|
8
|
-
|
15
|
+
# Context the job is run in
|
16
|
+
# @api private
|
17
|
+
Context = Struct.new(:logger)
|
9
18
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
19
|
+
# Default task logger implementation
|
20
|
+
DEFAULT_TASK_LOGGER = ->(logger, name) {
|
21
|
+
logger = logger.dup
|
22
|
+
logger.progname = name if logger.respond_to?(:progname=)
|
23
|
+
logger
|
24
|
+
}
|
25
|
+
private_constant :DEFAULT_TASK_LOGGER
|
26
|
+
|
27
|
+
@task_logger = DEFAULT_TASK_LOGGER
|
28
|
+
@logger = Logger.new($stdout, level: :info)
|
29
|
+
@instrumenter = Instrumenters::Noop
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# An object that when called creates a logger for the provided task
|
33
|
+
#
|
34
|
+
# @return [#call(original_logger, task_name)]
|
35
|
+
# a callable object that returns a a +Logger+ like instance which responds to +info+ and +debug+
|
36
|
+
# @example
|
37
|
+
# Scheduled.task_logger = ->(original_logger, task_name) {
|
38
|
+
# logger = original_logger.dup
|
39
|
+
# logger.progname = task_name
|
40
|
+
# logger
|
41
|
+
# }
|
42
|
+
attr_accessor :task_logger
|
43
|
+
|
44
|
+
# @return [#info, #debug]
|
45
|
+
# a +Logger+ like instance which responds to +info+ and +debug+
|
46
|
+
attr_accessor :logger
|
47
|
+
|
48
|
+
# @return [#instrument]
|
49
|
+
# an +ActiveSupport::Notifications+ like object which responds to +instrument+
|
50
|
+
attr_accessor :instrumenter
|
51
|
+
|
52
|
+
# An object that responds to +call+ and receives an exception as an argument.
|
53
|
+
attr_accessor :error_notifier
|
15
54
|
|
16
|
-
|
55
|
+
# Create task to run every interval.
|
56
|
+
#
|
57
|
+
# @param interval [Integer, String, #call]
|
58
|
+
# Interval to perform task.
|
59
|
+
#
|
60
|
+
# When provided as an +Integer+, is the number of seconds between task runs.
|
61
|
+
#
|
62
|
+
# When provided as a +String+, is a cron-formatted interval line.
|
63
|
+
#
|
64
|
+
# When provided as an object that responds to +#call+, will run when truthy.
|
65
|
+
#
|
66
|
+
# @param name [String, false]
|
67
|
+
# Name of task, used during logging. Will use block location and line number by
|
68
|
+
# default. Use +false+ to prevent a name being automatically assigned.
|
69
|
+
#
|
70
|
+
# @return [void]
|
71
|
+
#
|
72
|
+
# @example Run every 60 seconds
|
73
|
+
# Scheduled.every(60) { puts "Running every 60 seconds" }
|
74
|
+
#
|
75
|
+
# @example Run every day at 9:10 AM
|
76
|
+
# Scheduled.every("10 9 * * *") { puts "Performing billing" }
|
77
|
+
#
|
78
|
+
def every(interval, name: nil, &block)
|
79
|
+
name ||= block_name(block)
|
80
|
+
logger = logger_for_task(name, block)
|
81
|
+
context = Context.new(logger)
|
17
82
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
83
|
+
rescued_block = ->() do
|
84
|
+
instrumenter.instrument("scheduled.run", {name: name}) do |payload|
|
85
|
+
begin
|
86
|
+
result = context.instance_eval(&block)
|
87
|
+
payload[:result] = result
|
88
|
+
result
|
89
|
+
rescue => e
|
90
|
+
payload[:exception] = [e.class.to_s, e.message]
|
91
|
+
payload[:exception_object] = e
|
22
92
|
|
23
|
-
|
24
|
-
|
25
|
-
|
93
|
+
if error_notifier
|
94
|
+
error_notifier.call(e)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
if interval.is_a?(Integer)
|
101
|
+
logger.debug { "Running every #{interval} seconds" }
|
102
|
+
|
103
|
+
task = Concurrent::TimerTask.new(execution_interval: interval, run_now: true) do
|
104
|
+
rescued_block.call
|
26
105
|
end
|
27
106
|
|
28
107
|
task.execute
|
29
|
-
}
|
30
108
|
|
31
|
-
|
109
|
+
elsif interval.is_a?(String)
|
110
|
+
run = ->() {
|
111
|
+
now = Time.now
|
112
|
+
parsed_cron = CronParser.new(interval)
|
113
|
+
next_tick_delay = [1, (parsed_cron.next(now) - now).ceil].max
|
114
|
+
|
115
|
+
logger.debug { "Next run at #{now + next_tick_delay} (tick delay of #{next_tick_delay})" }
|
116
|
+
|
117
|
+
task = Concurrent::ScheduledTask.execute(next_tick_delay) do
|
118
|
+
rescued_block.call
|
119
|
+
run.call
|
120
|
+
end
|
32
121
|
|
33
|
-
|
34
|
-
|
122
|
+
task.execute
|
123
|
+
}
|
35
124
|
|
36
|
-
|
37
|
-
case interval.call(job)
|
38
|
-
when true
|
39
|
-
block.call
|
125
|
+
run.call
|
40
126
|
|
41
|
-
|
42
|
-
|
43
|
-
|
127
|
+
elsif interval.respond_to?(:call)
|
128
|
+
job = Job.new
|
129
|
+
|
130
|
+
task = Concurrent::TimerTask.new(execution_interval: 1, run_now: true) do |timer_task|
|
131
|
+
case interval.call(job)
|
132
|
+
when true
|
133
|
+
rescued_block.call
|
134
|
+
|
135
|
+
job.last_run = Time.now
|
136
|
+
when :cancel
|
137
|
+
logger.debug { "Received :cancel. Shutting down." }
|
138
|
+
timer_task.shutdown
|
139
|
+
end
|
44
140
|
end
|
141
|
+
|
142
|
+
task.execute
|
143
|
+
else
|
144
|
+
raise ArgumentError, "Unsupported value for interval"
|
45
145
|
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Run task scheduler indefinitely.
|
149
|
+
#
|
150
|
+
# @return [void]
|
151
|
+
def wait
|
152
|
+
trap("INT") { exit }
|
46
153
|
|
47
|
-
|
48
|
-
|
49
|
-
|
154
|
+
loop do
|
155
|
+
sleep 1
|
156
|
+
end
|
50
157
|
end
|
51
|
-
end
|
52
158
|
|
53
|
-
|
54
|
-
|
159
|
+
private
|
160
|
+
|
161
|
+
# Build a logger for the current task
|
162
|
+
# @api private
|
163
|
+
def logger_for_task(name, block)
|
164
|
+
return logger if name == false
|
55
165
|
|
56
|
-
|
57
|
-
sleep 1
|
166
|
+
task_logger.call(logger, name)
|
58
167
|
end
|
168
|
+
|
169
|
+
# Generate name for block
|
170
|
+
# @api private
|
171
|
+
def block_name(block)
|
172
|
+
file, line = block.source_location
|
173
|
+
"#{file}:#{line}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
Scheduled.error_notifier = proc do |error|
|
178
|
+
$stderr.puts error.full_message
|
59
179
|
end
|
60
180
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scheduled
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Daniels
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -24,21 +24,7 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.0'
|
27
|
-
|
28
|
-
name: rubygems-tasks
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0.2'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0.2'
|
41
|
-
description:
|
27
|
+
description:
|
42
28
|
email: adam@mediadrive.ca
|
43
29
|
executables: []
|
44
30
|
extensions: []
|
@@ -47,12 +33,13 @@ files:
|
|
47
33
|
- README.md
|
48
34
|
- lib/scheduled.rb
|
49
35
|
- lib/scheduled/cron_parser.rb
|
36
|
+
- lib/scheduled/instrumenters.rb
|
50
37
|
- lib/scheduled/version.rb
|
51
38
|
homepage: https://github.com/adam12/scheduled
|
52
39
|
licenses:
|
53
40
|
- MIT
|
54
41
|
metadata: {}
|
55
|
-
post_install_message:
|
42
|
+
post_install_message:
|
56
43
|
rdoc_options: []
|
57
44
|
require_paths:
|
58
45
|
- lib
|
@@ -67,9 +54,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
54
|
- !ruby/object:Gem::Version
|
68
55
|
version: '0'
|
69
56
|
requirements: []
|
70
|
-
|
71
|
-
|
72
|
-
signing_key:
|
57
|
+
rubygems_version: 3.4.19
|
58
|
+
signing_key:
|
73
59
|
specification_version: 4
|
74
60
|
summary: A very lightweight clock process with minimal dependencies and no magic.
|
75
61
|
test_files: []
|