scheduled 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []