istox-resque-scheduler 1.0.0.pre

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS.md +87 -0
  3. data/CHANGELOG.md +478 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/CONTRIBUTING.md +6 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +23 -0
  8. data/README.md +698 -0
  9. data/Rakefile +21 -0
  10. data/exe/resque-scheduler +5 -0
  11. data/istox-resque-scheduler.gemspec +61 -0
  12. data/lib/resque-scheduler.rb +4 -0
  13. data/lib/resque/scheduler.rb +460 -0
  14. data/lib/resque/scheduler/cli.rb +147 -0
  15. data/lib/resque/scheduler/configuration.rb +73 -0
  16. data/lib/resque/scheduler/delaying_extensions.rb +356 -0
  17. data/lib/resque/scheduler/env.rb +89 -0
  18. data/lib/resque/scheduler/extension.rb +13 -0
  19. data/lib/resque/scheduler/failure_handler.rb +11 -0
  20. data/lib/resque/scheduler/lock.rb +4 -0
  21. data/lib/resque/scheduler/lock/base.rb +61 -0
  22. data/lib/resque/scheduler/lock/basic.rb +27 -0
  23. data/lib/resque/scheduler/lock/resilient.rb +78 -0
  24. data/lib/resque/scheduler/locking.rb +104 -0
  25. data/lib/resque/scheduler/logger_builder.rb +72 -0
  26. data/lib/resque/scheduler/plugin.rb +31 -0
  27. data/lib/resque/scheduler/scheduling_extensions.rb +142 -0
  28. data/lib/resque/scheduler/server.rb +268 -0
  29. data/lib/resque/scheduler/server/views/delayed.erb +63 -0
  30. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  31. data/lib/resque/scheduler/server/views/delayed_timestamp.erb +26 -0
  32. data/lib/resque/scheduler/server/views/requeue-params.erb +23 -0
  33. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  34. data/lib/resque/scheduler/server/views/search.erb +72 -0
  35. data/lib/resque/scheduler/server/views/search_form.erb +8 -0
  36. data/lib/resque/scheduler/signal_handling.rb +40 -0
  37. data/lib/resque/scheduler/tasks.rb +25 -0
  38. data/lib/resque/scheduler/util.rb +39 -0
  39. data/lib/resque/scheduler/version.rb +7 -0
  40. metadata +343 -0
@@ -0,0 +1,21 @@
1
+ # vim:fileencoding=utf-8
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+ require 'rubocop/rake_task'
5
+ require 'yard'
6
+
7
+ task default: [:rubocop, :test] unless RUBY_PLATFORM =~ /java/
8
+ task default: [:test] if RUBY_PLATFORM =~ /java/
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ Rake::TestTask.new do |t|
13
+ t.libs << 'test'
14
+ t.pattern = ENV['PATTERN'] || 'test/*_test.rb'
15
+ t.options = ''.tap do |o|
16
+ o << "--seed #{ENV['SEED']} " if ENV['SEED']
17
+ o << '--verbose ' if ENV['VERBOSE']
18
+ end
19
+ end
20
+
21
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:fileencoding=utf-8
3
+
4
+ require 'resque-scheduler'
5
+ Resque::Scheduler::Cli.run!
@@ -0,0 +1,61 @@
1
+ # vim:fileencoding=utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque/scheduler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'istox-resque-scheduler'
8
+ spec.version = Resque::Scheduler::VERSION
9
+ spec.authors = <<-EOF.split(/\n/).map(&:strip)
10
+ Ben VandenBos
11
+ Simon Eskildsen
12
+ Ryan Biesemeyer
13
+ Dan Buch
14
+ EOF
15
+ spec.email = %w(
16
+ bvandenbos@gmail.com
17
+ sirup@sirupsen.com
18
+ ryan@yaauie.com
19
+ dan@meatballhat.com
20
+ )
21
+ spec.summary = 'Light weight job scheduling on top of Resque'
22
+ spec.description = <<-DESCRIPTION
23
+ Light weight job scheduling on top of Resque.
24
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
25
+ Also supports queueing jobs on a fixed, cron-like schedule.
26
+ DESCRIPTION
27
+ spec.homepage = 'http://github.com/resque/resque-scheduler'
28
+ spec.license = 'MIT'
29
+
30
+ spec.files = `git ls-files -z`.split("\0").reject do |f|
31
+ f.match(%r{^(test|spec|features|examples|bin|tasks)/}) ||
32
+ f.match(/^(Vagrantfile|Gemfile\.lock|appveyor\.yml)/) ||
33
+ f.match(/^\.(rubocop|simplecov|travis|vagrant|gitignore)/)
34
+ end
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = %w(lib)
38
+
39
+ spec.add_development_dependency 'bundler'
40
+ spec.add_development_dependency 'json'
41
+ spec.add_development_dependency 'kramdown'
42
+ spec.add_development_dependency 'minitest'
43
+ spec.add_development_dependency 'mocha'
44
+ spec.add_development_dependency 'pry'
45
+ spec.add_development_dependency 'rack-test'
46
+ spec.add_development_dependency 'rake'
47
+ spec.add_development_dependency 'simplecov'
48
+ spec.add_development_dependency 'test-unit'
49
+ spec.add_development_dependency 'yard'
50
+ spec.add_development_dependency 'tzinfo-data'
51
+ spec.add_development_dependency 'timecop'
52
+
53
+ # We pin rubocop because new cops have a tendency to result in false-y
54
+ # positives for new contributors, which is not a nice experience.
55
+ spec.add_development_dependency 'rubocop', '~> 0.40.0'
56
+
57
+ spec.add_runtime_dependency 'mono_logger', '~> 1.0'
58
+ spec.add_runtime_dependency 'redis', '>= 3.3'
59
+ spec.add_runtime_dependency 'resque', '>= 1.27'
60
+ spec.add_runtime_dependency 'rufus-scheduler', '~> 3.2'
61
+ end
@@ -0,0 +1,4 @@
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'resque/scheduler'
3
+
4
+ Resque.extend Resque::Scheduler::Extension
@@ -0,0 +1,460 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'redis/errors'
4
+ require 'rufus/scheduler'
5
+ require_relative 'scheduler/configuration'
6
+ require_relative 'scheduler/locking'
7
+ require_relative 'scheduler/logger_builder'
8
+ require_relative 'scheduler/signal_handling'
9
+ require_relative 'scheduler/failure_handler'
10
+
11
+ module Resque
12
+ module Scheduler
13
+ autoload :Cli, 'resque/scheduler/cli'
14
+ autoload :Extension, 'resque/scheduler/extension'
15
+ autoload :Util, 'resque/scheduler/util'
16
+ autoload :VERSION, 'resque/scheduler/version'
17
+ INTERMITTENT_ERRORS = [
18
+ Errno::EAGAIN, Errno::ECONNRESET, Redis::CannotConnectError, Redis::TimeoutError
19
+ ].freeze
20
+
21
+ private
22
+
23
+ extend Resque::Scheduler::Locking
24
+ extend Resque::Scheduler::Configuration
25
+ extend Resque::Scheduler::SignalHandling
26
+
27
+ public
28
+
29
+ class << self
30
+ attr_writer :logger
31
+
32
+ # the Rufus::Scheduler jobs that are scheduled
33
+ attr_reader :scheduled_jobs
34
+
35
+ # allow user to set an additional failure handler
36
+ attr_writer :failure_handler
37
+
38
+ # Schedule all jobs and continually look for delayed jobs (never returns)
39
+ def run
40
+ procline 'Starting'
41
+
42
+ # trap signals
43
+ register_signal_handlers
44
+
45
+ # Quote from the resque/worker.
46
+ # Fix buffering so we can `rake resque:scheduler > scheduler.log` and
47
+ # get output from the child in there.
48
+ $stdout.sync = true
49
+ $stderr.sync = true
50
+
51
+ was_master = nil
52
+
53
+ begin
54
+ @th = Thread.current
55
+
56
+ # Now start the scheduling part of the loop.
57
+ loop do
58
+ begin
59
+ # Check on changes to master/child
60
+ @am_master = master?
61
+ if am_master != was_master
62
+ procline am_master ? 'Master scheduler' : 'Child scheduler'
63
+
64
+ # Load schedule because changed
65
+ reload_schedule!
66
+ end
67
+
68
+ if am_master
69
+ handle_delayed_items
70
+ update_schedule if dynamic
71
+ end
72
+ was_master = am_master
73
+ rescue *INTERMITTENT_ERRORS => e
74
+ log! e.message
75
+ release_master_lock
76
+ end
77
+ poll_sleep
78
+ end
79
+
80
+ rescue Interrupt
81
+ log 'Exiting'
82
+ end
83
+ end
84
+
85
+ def print_schedule
86
+ if rufus_scheduler
87
+ log! "Scheduling Info\tLast Run"
88
+ scheduler_jobs = rufus_scheduler.jobs
89
+ scheduler_jobs.each do |_k, v|
90
+ log! "#{v.t}\t#{v.last}\t"
91
+ end
92
+ end
93
+ end
94
+
95
+ # Pulls the schedule from Resque.schedule and loads it into the
96
+ # rufus scheduler instance
97
+ def load_schedule!
98
+ procline 'Loading Schedule'
99
+
100
+ # Need to load the schedule from redis for the first time if dynamic
101
+ Resque.reload_schedule! if dynamic
102
+
103
+ log! 'Schedule empty! Set Resque.schedule' if Resque.schedule.empty?
104
+
105
+ @scheduled_jobs = {}
106
+
107
+ Resque.schedule.each do |name, config|
108
+ load_schedule_job(name, config)
109
+ end
110
+ Resque.redis.del(:schedules_changed) if am_master && dynamic
111
+ procline 'Schedules Loaded'
112
+ end
113
+
114
+ # modify interval type value to value with options if options available
115
+ def optionizate_interval_value(value)
116
+ args = value
117
+ if args.is_a?(::Array)
118
+ return args.first if args.size > 2 || !args.last.is_a?(::Hash)
119
+ # symbolize keys of hash for options
120
+ args[2] = args[1].reduce({}) do |m, i|
121
+ key, value = i
122
+ m[(key.respond_to?(:to_sym) ? key.to_sym : key) || key] = value
123
+ m
124
+ end
125
+
126
+ args[2][:job] = true
127
+ args[1] = nil
128
+ end
129
+ args
130
+ end
131
+
132
+ # Loads a job schedule into the Rufus::Scheduler and stores it
133
+ # in @scheduled_jobs
134
+ def load_schedule_job(name, config)
135
+ # If `rails_env` or `env` is set in the config, load jobs only if they
136
+ # are meant to be loaded in `Resque::Scheduler.env`. If `rails_env` or
137
+ # `env` is missing, the job should be scheduled regardless of the value
138
+ # of `Resque::Scheduler.env`.
139
+
140
+ configured_env = config['rails_env'] || config['env']
141
+
142
+ if configured_env.nil? || env_matches?(configured_env)
143
+ log! "Scheduling #{name} "
144
+ interval_defined = false
145
+ interval_types = %w(cron every)
146
+ interval_types.each do |interval_type|
147
+ next unless !config[interval_type].nil? && !config[interval_type].empty?
148
+ args = optionizate_interval_value(config[interval_type])
149
+ args = [args, nil, job: true] if args.is_a?(::String)
150
+
151
+ job = rufus_scheduler.send(interval_type, *args) do
152
+ enqueue_recurring(name, config)
153
+ end
154
+ @scheduled_jobs[name] = job
155
+ interval_defined = true
156
+ break
157
+ end
158
+ unless interval_defined
159
+ log! "no #{interval_types.join(' / ')} found for " \
160
+ "#{config['class']} (#{name}) - skipping"
161
+ end
162
+ else
163
+ log "Skipping schedule of #{name} because configured " \
164
+ "env #{configured_env.inspect} does not match current " \
165
+ "env #{env.inspect}"
166
+ end
167
+ end
168
+
169
+ # Returns true if the given schedule config hash matches the current env
170
+ def rails_env_matches?(config)
171
+ warn '`Resque::Scheduler.rails_env_matches?` is deprecated. ' \
172
+ 'Please use `Resque::Scheduler.env_matches?` instead.'
173
+ config['rails_env'] && env &&
174
+ config['rails_env'].split(/[\s,]+/).include?(env)
175
+ end
176
+
177
+ # Returns true if the current env is non-nil and the configured env
178
+ # (which is a comma-split string) includes the current env.
179
+ def env_matches?(configured_env)
180
+ env && configured_env.split(/[\s,]+/).include?(env)
181
+ end
182
+
183
+ # Handles queueing delayed items
184
+ # at_time - Time to start scheduling items (default: now).
185
+ def handle_delayed_items(at_time = nil)
186
+ timestamp = Resque.next_delayed_timestamp(at_time)
187
+ if timestamp
188
+ procline 'Processing Delayed Items'
189
+ until timestamp.nil?
190
+ enqueue_delayed_items_for_timestamp(timestamp)
191
+ timestamp = Resque.next_delayed_timestamp(at_time)
192
+ end
193
+ end
194
+ end
195
+
196
+ def enqueue_next_item(timestamp)
197
+ item = Resque.next_item_for_timestamp(timestamp)
198
+
199
+ if item
200
+ log "queuing #{item['class']} [delayed]"
201
+ enqueue(item)
202
+ end
203
+
204
+ item
205
+ end
206
+
207
+ # Enqueues all delayed jobs for a timestamp
208
+ def enqueue_delayed_items_for_timestamp(timestamp)
209
+ item = nil
210
+ loop do
211
+ handle_shutdown do
212
+ # Continually check that it is still the master
213
+ item = enqueue_next_item(timestamp) if am_master
214
+ end
215
+ # continue processing until there are no more ready items in this
216
+ # timestamp
217
+ break if item.nil?
218
+ end
219
+ end
220
+
221
+ def enqueue(config)
222
+ enqueue_from_config(config)
223
+ rescue => e
224
+ Resque::Scheduler.failure_handler.on_enqueue_failure(config, e)
225
+ end
226
+
227
+ def handle_shutdown
228
+ exit if @shutdown
229
+ yield
230
+ exit if @shutdown
231
+ end
232
+
233
+ # Enqueues a job based on a config hash
234
+ def enqueue_from_config(job_config)
235
+ args = job_config['args'] || job_config[:args]
236
+
237
+ klass_name = job_config['class'] || job_config[:class]
238
+ begin
239
+ klass = Resque::Scheduler::Util.constantize(klass_name)
240
+ rescue NameError
241
+ klass = klass_name
242
+ end
243
+
244
+ params = args.is_a?(Hash) ? [args] : Array(args)
245
+ queue = job_config['queue'] ||
246
+ job_config[:queue] ||
247
+ Resque.queue_from_class(klass)
248
+ # Support custom job classes like those that inherit from
249
+ # Resque::JobWithStatus (resque-status)
250
+ job_klass = job_config['custom_job_class']
251
+ if job_klass && job_klass != 'Resque::Job'
252
+ # The custom job class API must offer a static "scheduled" method. If
253
+ # the custom job class can not be constantized (via a requeue call
254
+ # from the web perhaps), fall back to enqueing normally via
255
+ # Resque::Job.create.
256
+ begin
257
+ Resque::Scheduler::Util.constantize(job_klass).scheduled(
258
+ queue, klass_name, *params
259
+ )
260
+ rescue NameError
261
+ # Note that the custom job class (job_config['custom_job_class'])
262
+ # is the one enqueued
263
+ Resque::Job.create(queue, job_klass, *params)
264
+ end
265
+ else
266
+ # Hack to avoid havoc for people shoving stuff into queues
267
+ # for non-existent classes (for example: running scheduler in
268
+ # one app that schedules for another.
269
+ if Class === klass
270
+ Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(
271
+ klass, *params
272
+ )
273
+
274
+ # If the class is a custom job class, call self#scheduled on it.
275
+ # This allows you to do things like Resque.enqueue_at(timestamp,
276
+ # CustomJobClass). Otherwise, pass off to Resque.
277
+ if klass.respond_to?(:scheduled)
278
+ klass.scheduled(queue, klass_name, *params)
279
+ else
280
+ Resque.enqueue_to(queue, klass, *params)
281
+ end
282
+ else
283
+ # This will not run the before_hooks in rescue, but will at least
284
+ # queue the job.
285
+ Resque::Job.create(queue, klass, *params)
286
+ end
287
+ end
288
+ end
289
+
290
+ def rufus_scheduler
291
+ @rufus_scheduler ||= Rufus::Scheduler.new
292
+ end
293
+
294
+ # Stops old rufus scheduler and creates a new one. Returns the new
295
+ # rufus scheduler
296
+ def clear_schedule!
297
+ rufus_scheduler.stop
298
+ @rufus_scheduler = nil
299
+ @scheduled_jobs = {}
300
+ rufus_scheduler
301
+ end
302
+
303
+ def reload_schedule!
304
+ procline 'Reloading Schedule'
305
+ clear_schedule!
306
+ load_schedule!
307
+ end
308
+
309
+ def update_schedule
310
+ if Resque.redis.scard(:schedules_changed) > 0
311
+ procline 'Updating schedule'
312
+ loop do
313
+ schedule_name = Resque.redis.spop(:schedules_changed)
314
+ break unless schedule_name
315
+ Resque.reload_schedule!
316
+ if Resque.schedule.keys.include?(schedule_name)
317
+ unschedule_job(schedule_name)
318
+ load_schedule_job(schedule_name, Resque.schedule[schedule_name])
319
+ else
320
+ unschedule_job(schedule_name)
321
+ end
322
+ end
323
+ procline 'Schedules Loaded'
324
+ end
325
+ end
326
+
327
+ def unschedule_job(name)
328
+ if scheduled_jobs[name]
329
+ log "Removing schedule #{name}"
330
+ scheduled_jobs[name].unschedule
331
+ @scheduled_jobs.delete(name)
332
+ end
333
+ end
334
+
335
+ # Sleeps and returns true
336
+ def poll_sleep
337
+ handle_shutdown do
338
+ begin
339
+ poll_sleep_loop
340
+ ensure
341
+ @sleeping = false
342
+ end
343
+ end
344
+ true
345
+ end
346
+
347
+ def poll_sleep_loop
348
+ @sleeping = true
349
+ if poll_sleep_amount > 0
350
+ start = Time.now
351
+ loop do
352
+ elapsed_sleep = (Time.now - start)
353
+ remaining_sleep = poll_sleep_amount - elapsed_sleep
354
+ @do_break = false
355
+ if remaining_sleep <= 0
356
+ @do_break = true
357
+ else
358
+ @do_break = handle_signals_with_operation do
359
+ sleep(remaining_sleep)
360
+ end
361
+ end
362
+ break if @do_break
363
+ end
364
+ else
365
+ handle_signals_with_operation
366
+ end
367
+ end
368
+
369
+ def handle_signals_with_operation
370
+ yield if block_given?
371
+ handle_signals
372
+ false
373
+ rescue Interrupt
374
+ before_shutdown if @shutdown
375
+ true
376
+ end
377
+
378
+ def stop_rufus_scheduler
379
+ rufus_scheduler.shutdown(:wait)
380
+ rufus_scheduler.join
381
+ end
382
+
383
+ def before_shutdown
384
+ stop_rufus_scheduler
385
+ release_master_lock
386
+ end
387
+
388
+ # Sets the shutdown flag, clean schedules and exits if sleeping
389
+ def shutdown
390
+ return if @shutdown
391
+ @shutdown = true
392
+ log!('Shutting down')
393
+ @th.raise Interrupt if @sleeping
394
+ end
395
+
396
+ def log!(msg)
397
+ logger.info { msg }
398
+ end
399
+
400
+ def log_error(msg)
401
+ logger.error { msg }
402
+ end
403
+
404
+ def log(msg)
405
+ logger.debug { msg }
406
+ end
407
+
408
+ def procline(string)
409
+ log! string
410
+ argv0 = build_procline(string)
411
+ log "Setting procline #{argv0.inspect}"
412
+ $0 = argv0
413
+ end
414
+
415
+ def failure_handler
416
+ @failure_handler ||= Resque::Scheduler::FailureHandler
417
+ end
418
+
419
+ def logger
420
+ @logger ||= Resque::Scheduler::LoggerBuilder.new(
421
+ quiet: quiet,
422
+ verbose: verbose,
423
+ log_dev: logfile,
424
+ format: logformat
425
+ ).build
426
+ end
427
+
428
+ private
429
+
430
+ def enqueue_recurring(name, config)
431
+ if am_master
432
+ log! "queueing #{config['class']} (#{name})"
433
+ enqueue(config)
434
+ Resque.last_enqueued_at(name, Time.now.to_s)
435
+ end
436
+ end
437
+
438
+ def app_str
439
+ app_name ? "[#{app_name}]" : ''
440
+ end
441
+
442
+ def env_str
443
+ env ? "[#{env}]" : ''
444
+ end
445
+
446
+ def build_procline(string)
447
+ "#{internal_name}#{app_str}#{env_str}: #{string}"
448
+ end
449
+
450
+ def internal_name
451
+ "resque-scheduler-#{Resque::Scheduler::VERSION}"
452
+ end
453
+
454
+ def am_master
455
+ @am_master = master? unless defined?(@am_master)
456
+ @am_master
457
+ end
458
+ end
459
+ end
460
+ end