scheduled 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 688c45f27921579d66859e13503929c9de904061
4
+ data.tar.gz: cc72fa658f792a10ec443ea1dd79f8ef4e069448
5
+ SHA512:
6
+ metadata.gz: f254da84808cdaa0d9c1985c52dac01ba120958241d94fcc0c7438d6c6397616b391aa5ec87e70f79242c7643a74c968a49e7e0135fc13081d17c4d3ccf01601
7
+ data.tar.gz: 61e96fa4874cd744cebd95c21914681511b9567b8b3e8e6d27090655707e83b6e95ab539a266ba2b3ec323d2ee29c7ec7e04064b9daf6a919bc3470a253612f8
@@ -0,0 +1,82 @@
1
+ # Scheduled
2
+
3
+ A simple task scheduler, akin to ClockWork or Rufus Scheduler.
4
+
5
+ ## Project Goals
6
+
7
+ * Absolutely no dependency on ActiveSupport
8
+ * Very small dependency tree
9
+ * Framework agnostic
10
+ * No built-in daemonization
11
+ * No scheduling DSL
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem "scheduled"
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install scheduled
28
+
29
+ ## Usage
30
+
31
+ ### Defining tasks
32
+
33
+ Tasks are defined by calling `Scheduled.every` with an interval and a block to execute.
34
+ The interval can be a basic Integer to represent seconds, or a callable object that receives
35
+ the current Job.
36
+
37
+ If the callable object returns a truthy value, the block is executed.
38
+
39
+ ```ruby
40
+ require "scheduled"
41
+
42
+ # Called every 60 seconds
43
+ Scheduled.every(60) { do_work } # Perform some job
44
+
45
+ # Using a cronline
46
+ Scheduled.every("* * * * *") { do_work }
47
+
48
+ two_hours_from_last_run = ->(job) do
49
+ Time.now - job.last_run >= 60*60*2
50
+ end
51
+ Scheduled.every(two_hours_from_last_run) { puts "Updating" }
52
+
53
+ # Run the scheduler
54
+ Scheduled.wait
55
+ ```
56
+
57
+ ### Running the scheduler
58
+
59
+ Provided your schedule file ends with a call to `Scheduled.wait`, just run it
60
+ as any other Ruby script.
61
+
62
+ ruby schedule.rb
63
+
64
+ ### Quick Returning Tasks
65
+
66
+ Scheduled works best if you schedule the tasks into a long-running queue, such as
67
+ Backburner or Sidekiq.
68
+
69
+ ## Contributing
70
+
71
+ Bug reports and pull requests are welcome on GitHub at https://github.com/adam12/scheduled.
72
+
73
+ I love pull requests! If you fork this project and modify it, please ping me to see
74
+ if your changes can be incorporated back into this project.
75
+
76
+ That said, if your feature idea is nontrivial, you should probably open an issue to
77
+ [discuss it](http://www.igvita.com/2011/12/19/dont-push-your-pull-requests/)
78
+ before attempting a pull request.
79
+
80
+ ## License
81
+
82
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require "concurrent"
3
+ require "scheduled/cron_parser"
4
+
5
+ module Scheduled
6
+ Job = Struct.new(:last_run)
7
+
8
+ module_function
9
+
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
15
+
16
+ task.execute
17
+
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
22
+
23
+ task = Concurrent::ScheduledTask.execute(next_tick_delay) do
24
+ block.call
25
+ run.call
26
+ end
27
+
28
+ task.execute
29
+ }
30
+
31
+ run.call
32
+
33
+ elsif interval.respond_to?(:call)
34
+ job = Job.new
35
+
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
40
+
41
+ job.last_run = Time.now
42
+ when :cancel
43
+ timer_task.shutdown
44
+ end
45
+ end
46
+
47
+ task.execute
48
+ else
49
+ raise ArgumentError, "Unsupported value for interval"
50
+ end
51
+ end
52
+
53
+ def wait
54
+ trap("INT") { exit }
55
+
56
+ loop do
57
+ sleep 1
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,322 @@
1
+ # Original source available at
2
+ # https://github.com/siebertm/parse-cron/blob/6064cc29262ac7c6ffb902c95c0c059eded960d1/lib/cron_parser.rb
3
+ #
4
+ # Copyright (C) 2013 Michael Siebert <siebertm85@googlemail.com>
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without restriction,
8
+ # including without limitation the rights to use, copy, modify,
9
+ # merge, publish, distribute, sublicense, and/or sell copies of
10
+ # the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+
13
+ # The above copyright notice and this permission notice shall
14
+ # be included in all copies or substantial portions of the Software.
15
+
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ # OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require 'set'
26
+ require 'date'
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
42
+
43
+ @time_source = time_source
44
+ end
45
+
46
+ def to_time
47
+ time_source.local(@year, @month, @day, @hour, @min, 0)
48
+ end
49
+
50
+ def inspect
51
+ [year, month, day, hour, min].inspect
52
+ 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
+ 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
100
+ end
101
+ end
102
+
103
+
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)
107
+
108
+ unless time_specs[:month][0].include?(t.month)
109
+ nudge_month(t)
110
+ t.day = 0
111
+ end
112
+
113
+ unless interpolate_weekdays(t.year, t.month)[0].include?(t.day)
114
+ nudge_date(t)
115
+ t.hour = -1
116
+ end
117
+
118
+ unless time_specs[:hour][0].include?(t.hour)
119
+ nudge_hour(t)
120
+ t.min = -1
121
+ end
122
+
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
130
+ end
131
+ end
132
+
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)
136
+
137
+ unless time_specs[:month][0].include?(t.month)
138
+ nudge_month(t, :last)
139
+ t.day = 32
140
+ end
141
+
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
146
+
147
+ unless time_specs[:hour][0].include?(t.hour)
148
+ nudge_hour(t, :last)
149
+ t.min = 60
150
+ end
151
+
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
159
+ end
160
+ end
161
+
162
+
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
178
+ else
179
+ raise ArgumentError, "Bad Vixie-style specification #{subel}"
180
+ end
181
+ end
182
+ end.flatten.sort
183
+
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)
194
+ end
195
+ array
196
+ end
197
+
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
+
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]
208
+
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 == '*'
214
+ end
215
+ # Careful: crontabs may use either 0 or 7 for Sunday:
216
+ valid_wday << 0 if valid_wday.include?(7)
217
+
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
222
+ end
223
+
224
+ [Set.new(result), result]
225
+ end
226
+
227
+ def nudge_year(t, dir = :next)
228
+ t.year = t.year + (dir == :next ? 1 : -1)
229
+ end
230
+
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)
235
+
236
+ nudge_year(t, dir) if next_value.nil?
237
+
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
242
+
243
+ def date_valid?(t, dir = :next)
244
+ interpolate_weekdays(t.year, t.month)[0].include?(t.day)
245
+ end
246
+
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)
251
+
252
+ nudge_month(t, dir) if next_value.nil? && can_nudge_month
253
+ end
254
+
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)
259
+
260
+ nudge_date(t, dir) if next_value.nil?
261
+ end
262
+
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)
267
+
268
+ nudge_hour(t, dir) if next_value.nil?
269
+ end
270
+
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
+ }
282
+ end
283
+ end
284
+
285
+ def substitute_parse_symbols(str)
286
+ SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)|
287
+ s.gsub(symbol, replacement)
288
+ end
289
+ end
290
+
291
+
292
+ def stepped_range(rng, step = 1)
293
+ len = rng.last - rng.first
294
+
295
+ num = len.div(step)
296
+ result = (0..num).map { |i| rng.first + step * i }
297
+
298
+ result.pop if result[-1] == rng.last and rng.exclude_end?
299
+ result
300
+ end
301
+
302
+
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 }
310
+ end
311
+ end
312
+
313
+ def validate_source
314
+ unless @source.respond_to?(:split)
315
+ raise ArgumentError, 'not a valid cronline'
316
+ end
317
+ source_length = @source.split(/\s+/).length
318
+ unless source_length >= 5 && source_length <= 6
319
+ raise ArgumentError, 'not a valid cronline'
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Scheduled
3
+ VERSION = "0.1.0"
4
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scheduled
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Daniels
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
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:
42
+ email: adam@mediadrive.ca
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - lib/scheduled.rb
49
+ - lib/scheduled/cron_parser.rb
50
+ - lib/scheduled/version.rb
51
+ homepage: https://github.com/adam12/scheduled
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.6.12
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: A very lightweight clock process with minimal dependencies and no magic.
75
+ test_files: []