istox-resque-scheduler 1.0.0.pre

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