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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 688c45f27921579d66859e13503929c9de904061
4
- data.tar.gz: cc72fa658f792a10ec443ea1dd79f8ef4e069448
2
+ SHA256:
3
+ metadata.gz: db4f2341051e6e731d151c2c77742da414d297e42ea9bd5935d0621a36936c58
4
+ data.tar.gz: 1eb6ed8ce17b349fb007b84eeed4b1ab6da028e7fc08fa3af0a4e7e0551970a1
5
5
  SHA512:
6
- metadata.gz: f254da84808cdaa0d9c1985c52dac01ba120958241d94fcc0c7438d6c6397616b391aa5ec87e70f79242c7643a74c968a49e7e0135fc13081d17c4d3ccf01601
7
- data.tar.gz: 61e96fa4874cd744cebd95c21914681511b9567b8b3e8e6d27090655707e83b6e95ab539a266ba2b3ec323d2ee29c7ec7e04064b9daf6a919bc3470a253612f8
6
+ metadata.gz: 2aeea0f548aeb7b8dc342b0b7d1e861182f32f752e51c9a5ff2448b3d525b09c96a46986cbdfdf35767ee63341499a5a061bb8d3ff704d1aa5918204907ef953
7
+ data.tar.gz: b67bfea285af8662eeea8901a55e92d39d2a28f3afb5fb6d2f4d97b3a1338ca64dc9e9faef7e2351e7e5c77e22be3eeba7f10aa3916eb629d0bb814212d7c43a
@@ -25,298 +25,300 @@
25
25
  require 'set'
26
26
  require 'date'
27
27
 
28
- # Parses cron expressions and computes the next occurence of the "job"
29
- #
30
- class CronParser
31
- # internal "mutable" time representation
32
- class InternalTime
33
- attr_accessor :year, :month, :day, :hour, :min
34
- attr_accessor :time_source
35
-
36
- def initialize(time,time_source = Time)
37
- @year = time.year
38
- @month = time.month
39
- @day = time.day
40
- @hour = time.hour
41
- @min = time.min
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
- @time_source = time_source
44
- end
47
+ def to_time
48
+ time_source.local(@year, @month, @day, @hour, @min, 0)
49
+ end
45
50
 
46
- def to_time
47
- time_source.local(@year, @month, @day, @hour, @min, 0)
51
+ def inspect
52
+ [year, month, day, hour, min].inspect
53
+ end
48
54
  end
49
55
 
50
- def inspect
51
- [year, month, day, hour, min].inspect
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
- def interpret_vixieisms(spec)
85
- case spec
86
- when '@reboot'
87
- raise ArgumentError, "Can't predict last/next run of @reboot"
88
- when '@yearly', '@annually'
89
- '0 0 1 1 *'
90
- when '@monthly'
91
- '0 0 1 * *'
92
- when '@weekly'
93
- '0 0 * * 0'
94
- when '@daily', '@midnight'
95
- '0 0 * * *'
96
- when '@hourly'
97
- '0 * * * *'
98
- else
99
- spec
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
- # returns the next occurence after the given date
105
- def next(now = @time_source.now, num = 1)
106
- t = InternalTime.new(now, @time_source)
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
- unless time_specs[:month][0].include?(t.month)
109
- nudge_month(t)
110
- t.day = 0
111
- end
109
+ unless time_specs[:month][0].include?(t.month)
110
+ nudge_month(t)
111
+ t.day = 0
112
+ end
112
113
 
113
- unless interpolate_weekdays(t.year, t.month)[0].include?(t.day)
114
- nudge_date(t)
115
- t.hour = -1
116
- end
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
- unless time_specs[:hour][0].include?(t.hour)
119
- nudge_hour(t)
120
- t.min = -1
121
- end
119
+ unless time_specs[:hour][0].include?(t.hour)
120
+ nudge_hour(t)
121
+ t.min = -1
122
+ end
122
123
 
123
- # always nudge the minute
124
- nudge_minute(t)
125
- t = t.to_time
126
- if num > 1
127
- recursive_calculate(:next,t,num)
128
- else
129
- t
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
- # returns the last occurence before the given date
134
- def last(now = @time_source.now, num=1)
135
- t = InternalTime.new(now,@time_source)
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
- unless time_specs[:month][0].include?(t.month)
138
- nudge_month(t, :last)
139
- t.day = 32
140
- end
138
+ unless time_specs[:month][0].include?(t.month)
139
+ nudge_month(t, :last)
140
+ t.day = 32
141
+ end
141
142
 
142
- if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day)
143
- nudge_date(t, :last)
144
- t.hour = 24
145
- end
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
- unless time_specs[:hour][0].include?(t.hour)
148
- nudge_hour(t, :last)
149
- t.min = 60
150
- end
148
+ unless time_specs[:hour][0].include?(t.hour)
149
+ nudge_hour(t, :last)
150
+ t.min = 60
151
+ end
151
152
 
152
- # always nudge the minute
153
- nudge_minute(t, :last)
154
- t = t.to_time
155
- if num > 1
156
- recursive_calculate(:last,t,num)
157
- else
158
- t
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
- SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$}
164
- def parse_element(elem, allowed_range)
165
- values = elem.split(',').map do |subel|
166
- if subel =~ /^\*/
167
- step = subel.length > 1 ? subel[2..-1].to_i : 1
168
- stepped_range(allowed_range, step)
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
- raise ArgumentError, "Bad Vixie-style specification #{subel}"
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
- [Set.new(values), values, elem]
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
- def interpolate_weekdays_without_cache(year, month)
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
- # Careful, if both DOW and DOM fields are non-wildcard,
210
- # then we only need to match *one* for cron to run the job:
211
- if not (mday_field == '*' and wday_field == '*')
212
- valid_mday = [] if mday_field == '*'
213
- valid_wday = [] if wday_field == '*'
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
- result = []
219
- while t.month == month
220
- result << t.mday if valid_mday.include?(t.mday) || valid_wday.include?(t.wday)
221
- t = t.succ
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
- [Set.new(result), result]
225
- end
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
- def nudge_year(t, dir = :next)
228
- t.year = t.year + (dir == :next ? 1 : -1)
229
- end
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
- def nudge_month(t, dir = :next)
232
- spec = time_specs[:month][1]
233
- next_value = find_best_next(t.month, spec, dir)
234
- t.month = next_value || (dir == :next ? spec.first : spec.last)
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
- nudge_year(t, dir) if next_value.nil?
225
+ [Set.new(result), result]
226
+ end
237
227
 
238
- # we changed the month, so its likely that the date is incorrect now
239
- valid_days = interpolate_weekdays(t.year, t.month)[1]
240
- t.day = dir == :next ? valid_days.first : valid_days.last
241
- end
228
+ def nudge_year(t, dir = :next)
229
+ t.year = t.year + (dir == :next ? 1 : -1)
230
+ end
242
231
 
243
- def date_valid?(t, dir = :next)
244
- interpolate_weekdays(t.year, t.month)[0].include?(t.day)
245
- end
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
- def nudge_date(t, dir = :next, can_nudge_month = true)
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
- nudge_month(t, dir) if next_value.nil? && can_nudge_month
253
- end
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
- def nudge_hour(t, dir = :next)
256
- spec = time_specs[:hour][1]
257
- next_value = find_best_next(t.hour, spec, dir)
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) if next_value.nil?
261
- end
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
- def nudge_minute(t, dir = :next)
264
- spec = time_specs[:minute][1]
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) if next_value.nil?
269
- end
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
- def time_specs
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
- def substitute_parse_symbols(str)
286
- SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)|
287
- s.gsub(symbol, replacement)
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
- def stepped_range(rng, step = 1)
293
- len = rng.last - rng.first
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
- result.pop if result[-1] == rng.last and rng.exclude_end?
299
- result
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
- # returns the smallest element from allowed which is greater than current
304
- # returns nil if no matching value was found
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
- def validate_source
314
- unless @source.respond_to?(:split)
315
- raise ArgumentError, 'not a valid cronline'
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
- source_length = @source.split(/\s+/).length
318
- unless source_length >= 5 && source_length <= 6
319
- raise ArgumentError, 'not a valid cronline'
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
@@ -0,0 +1,13 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Scheduled
4
+ module Instrumenters
5
+ ##
6
+ # An Instrumentor that performs work without measurement
7
+ class Noop
8
+ def self.instrument(name, payload = {})
9
+ yield payload if block_given?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Scheduled
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
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
- module_function
15
+ # Context the job is run in
16
+ # @api private
17
+ Context = Struct.new(:logger)
9
18
 
10
- def every(interval, &block)
11
- if interval.is_a?(Integer)
12
- task = Concurrent::TimerTask.new(execution_interval: interval, run_now: true) do
13
- block.call
14
- end
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
- task.execute
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
- elsif interval.is_a?(String)
19
- run = ->() {
20
- parsed_cron = CronParser.new(interval)
21
- next_tick_delay = parsed_cron.next(Time.now) - Time.now
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
- task = Concurrent::ScheduledTask.execute(next_tick_delay) do
24
- block.call
25
- run.call
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
- run.call
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
- elsif interval.respond_to?(:call)
34
- job = Job.new
122
+ task.execute
123
+ }
35
124
 
36
- task = Concurrent::TimerTask.new(execution_interval: 1, run_now: true) do |timer_task|
37
- case interval.call(job)
38
- when true
39
- block.call
125
+ run.call
40
126
 
41
- job.last_run = Time.now
42
- when :cancel
43
- timer_task.shutdown
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
- task.execute
48
- else
49
- raise ArgumentError, "Unsupported value for interval"
154
+ loop do
155
+ sleep 1
156
+ end
50
157
  end
51
- end
52
158
 
53
- def wait
54
- trap("INT") { exit }
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
- loop do
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.1.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: 2017-06-20 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
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
- rubyforge_project:
71
- rubygems_version: 2.6.12
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: []