resque-scheduler 2.5.1 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/funding.yml +4 -0
  4. data/.github/workflows/codeql-analysis.yml +59 -0
  5. data/.github/workflows/rubocop.yml +27 -0
  6. data/.github/workflows/ruby.yml +81 -0
  7. data/AUTHORS.md +25 -0
  8. data/CHANGELOG.md +539 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +26 -2
  11. data/README.md +291 -70
  12. data/Rakefile +8 -19
  13. data/exe/resque-scheduler +5 -0
  14. data/lib/resque/scheduler/cli.rb +147 -0
  15. data/lib/resque/scheduler/configuration.rb +102 -0
  16. data/lib/resque/scheduler/delaying_extensions.rb +371 -0
  17. data/lib/resque/scheduler/env.rb +85 -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/base.rb +13 -4
  21. data/lib/resque/scheduler/lock/basic.rb +4 -5
  22. data/lib/resque/scheduler/lock/resilient.rb +52 -43
  23. data/lib/resque/scheduler/lock.rb +2 -1
  24. data/lib/resque/scheduler/locking.rb +104 -0
  25. data/lib/resque/scheduler/logger_builder.rb +83 -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 → resque/scheduler}/server/views/delayed.erb +21 -12
  29. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_schedules.erb +1 -1
  30. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +1 -1
  31. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  32. data/lib/{resque_scheduler → resque/scheduler}/server/views/search.erb +4 -7
  33. data/lib/{resque_scheduler → resque/scheduler}/server/views/search_form.erb +1 -5
  34. data/lib/resque/scheduler/server.rb +268 -0
  35. data/lib/resque/scheduler/signal_handling.rb +40 -0
  36. data/lib/{resque_scheduler → resque/scheduler}/tasks.rb +3 -6
  37. data/lib/resque/scheduler/util.rb +39 -0
  38. data/lib/resque/scheduler/version.rb +7 -0
  39. data/lib/resque/scheduler.rb +271 -199
  40. data/lib/resque-scheduler.rb +3 -1
  41. data/resque-scheduler.gemspec +53 -20
  42. metadata +176 -132
  43. data/.gitignore +0 -11
  44. data/.rubocop.yml +0 -129
  45. data/.simplecov +0 -1
  46. data/.travis.yml +0 -21
  47. data/HISTORY.md +0 -226
  48. data/ROADMAP.md +0 -10
  49. data/bin/resque-scheduler +0 -5
  50. data/examples/Rakefile +0 -2
  51. data/examples/config/initializers/resque-web.rb +0 -37
  52. data/examples/dynamic-scheduling/README.md +0 -28
  53. data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +0 -54
  54. data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +0 -9
  55. data/examples/dynamic-scheduling/app/models/user.rb +0 -16
  56. data/examples/dynamic-scheduling/config/resque.yml +0 -4
  57. data/examples/dynamic-scheduling/config/static_schedule.yml +0 -7
  58. data/examples/dynamic-scheduling/lib/tasks/resque.rake +0 -48
  59. data/examples/run-resque-web +0 -3
  60. data/lib/resque/scheduler_locking.rb +0 -91
  61. data/lib/resque_scheduler/cli.rb +0 -160
  62. data/lib/resque_scheduler/logger_builder.rb +0 -70
  63. data/lib/resque_scheduler/plugin.rb +0 -28
  64. data/lib/resque_scheduler/server/views/scheduler.erb +0 -36
  65. data/lib/resque_scheduler/server.rb +0 -182
  66. data/lib/resque_scheduler/util.rb +0 -34
  67. data/lib/resque_scheduler/version.rb +0 -5
  68. data/lib/resque_scheduler.rb +0 -386
  69. data/script/migrate_to_timestamps_set.rb +0 -14
  70. data/tasks/resque_scheduler.rake +0 -2
  71. data/test/cli_test.rb +0 -286
  72. data/test/delayed_queue_test.rb +0 -449
  73. data/test/redis-test.conf +0 -108
  74. data/test/resque-web_test.rb +0 -199
  75. data/test/scheduler_args_test.rb +0 -190
  76. data/test/scheduler_hooks_test.rb +0 -23
  77. data/test/scheduler_locking_test.rb +0 -242
  78. data/test/scheduler_setup_test.rb +0 -95
  79. data/test/scheduler_task_test.rb +0 -35
  80. data/test/scheduler_test.rb +0 -344
  81. data/test/support/redis_instance.rb +0 -134
  82. data/test/test_helper.rb +0 -131
  83. /data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
@@ -1,103 +1,43 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'redis/errors'
1
4
  require 'rufus/scheduler'
2
- require 'resque/scheduler_locking'
3
- require 'resque_scheduler/logger_builder'
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'
4
10
 
5
11
  module Resque
6
- class Scheduler
7
- extend Resque::SchedulerLocking
8
-
9
- class << self
10
- # Allows for block-style configuration
11
- def configure
12
- yield self
13
- end
14
-
15
- attr_writer :signal_queue
16
-
17
- def signal_queue
18
- @signal_queue ||= []
19
- end
20
-
21
- # Used in `#load_schedule_job`
22
- attr_writer :env
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
23
20
 
24
- def env
25
- return @env if @env
26
- @env ||= Rails.env if defined?(Rails)
27
- @env ||= ENV['RAILS_ENV']
28
- @env
29
- end
30
-
31
- # If true, logs more stuff...
32
- attr_writer :verbose
33
-
34
- def verbose
35
- @verbose ||= !!ENV['VERBOSE']
36
- end
21
+ private
37
22
 
38
- # If set, produces no output
39
- attr_writer :mute
23
+ extend Resque::Scheduler::Locking
24
+ extend Resque::Scheduler::Configuration
25
+ extend Resque::Scheduler::SignalHandling
40
26
 
41
- def mute
42
- @mute ||= !!ENV['MUTE']
43
- end
44
-
45
- # If set, will write messages to the file
46
- attr_writer :logfile
47
-
48
- def logfile
49
- @logfile ||= ENV['LOGFILE']
50
- end
51
-
52
- # Sets whether to log in 'text' or 'json'
53
- attr_writer :logformat
54
-
55
- def logformat
56
- @logformat ||= ENV['LOGFORMAT']
57
- end
58
-
59
- # If set, will try to update the schedule in the loop
60
- attr_writer :dynamic
61
-
62
- def dynamic
63
- @dynamic ||= !!ENV['DYNAMIC_SCHEDULE']
64
- end
65
-
66
- # If set, will append the app name to procline
67
- attr_writer :app_name
68
-
69
- def app_name
70
- @app_name ||= ENV['APP_NAME']
71
- end
72
-
73
- # Amount of time in seconds to sleep between polls of the delayed
74
- # queue. Defaults to 5
75
- attr_writer :poll_sleep_amount
76
-
77
- def poll_sleep_amount
78
- @poll_sleep_amount ||=
79
- Float(ENV.fetch('RESQUE_SCHEDULER_INTERVAL', '5'))
80
- end
27
+ public
81
28
 
29
+ class << self
82
30
  attr_writer :logger
83
31
 
84
- def logger
85
- @logger ||= ResqueScheduler::LoggerBuilder.new(
86
- :mute => mute,
87
- :verbose => verbose,
88
- :log_dev => logfile,
89
- :format => logformat
90
- ).build
91
- end
92
-
93
32
  # the Rufus::Scheduler jobs that are scheduled
94
- def scheduled_jobs
95
- @@scheduled_jobs
96
- end
33
+ attr_reader :scheduled_jobs
34
+
35
+ # allow user to set an additional failure handler
36
+ attr_writer :failure_handler
97
37
 
98
38
  # Schedule all jobs and continually look for delayed jobs (never returns)
99
39
  def run
100
- $0 = "resque-scheduler: Starting"
40
+ procline 'Starting'
101
41
 
102
42
  # trap signals
103
43
  register_signal_handlers
@@ -108,28 +48,32 @@ module Resque
108
48
  $stdout.sync = true
109
49
  $stderr.sync = true
110
50
 
111
- # Load the schedule into rufus
112
- # If dynamic is set, load that schedule otherwise use normal load
113
- if dynamic
114
- reload_schedule!
115
- else
116
- load_schedule!
117
- end
51
+ was_master = nil
118
52
 
119
53
  begin
120
54
  @th = Thread.current
121
55
 
122
56
  # Now start the scheduling part of the loop.
123
57
  loop do
124
- if is_master?
125
- begin
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
126
69
  handle_delayed_items
127
70
  update_schedule if dynamic
128
- rescue Errno::EAGAIN, Errno::ECONNRESET => e
129
- log! e.message
130
71
  end
72
+ was_master = am_master
73
+ rescue *INTERMITTENT_ERRORS => e
74
+ log! e.message
75
+ release_master_lock
131
76
  end
132
- handle_signals
133
77
  poll_sleep
134
78
  end
135
79
 
@@ -138,33 +82,11 @@ module Resque
138
82
  end
139
83
  end
140
84
 
141
- # For all signals, set the shutdown flag and wait for current
142
- # poll/enqueing to finish (should be almost instant). In the
143
- # case of sleeping, exit immediately.
144
- def register_signal_handlers
145
- %w(INT TERM USR1 USR2 QUIT).each do |sig|
146
- trap(sig) { signal_queue << sig }
147
- end
148
- end
149
-
150
- def handle_signals
151
- loop do
152
- sig = signal_queue.shift
153
- break unless sig
154
- log! "Got #{sig} signal"
155
- case sig
156
- when 'INT', 'TERM', 'QUIT' then shutdown
157
- when 'USR1' then print_schedule
158
- when 'USR2' then reload_schedule!
159
- end
160
- end
161
- end
162
-
163
85
  def print_schedule
164
86
  if rufus_scheduler
165
87
  log! "Scheduling Info\tLast Run"
166
- scheduler_jobs = rufus_scheduler.all_jobs
167
- scheduler_jobs.each do |k, v|
88
+ scheduler_jobs = rufus_scheduler.jobs
89
+ scheduler_jobs.each do |_k, v|
168
90
  log! "#{v.t}\t#{v.last}\t"
169
91
  end
170
92
  end
@@ -173,20 +95,20 @@ module Resque
173
95
  # Pulls the schedule from Resque.schedule and loads it into the
174
96
  # rufus scheduler instance
175
97
  def load_schedule!
176
- procline "Loading Schedule"
98
+ procline 'Loading Schedule'
177
99
 
178
100
  # Need to load the schedule from redis for the first time if dynamic
179
101
  Resque.reload_schedule! if dynamic
180
102
 
181
- log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
103
+ log! 'Schedule empty! Set Resque.schedule' if Resque.schedule.empty?
182
104
 
183
- @@scheduled_jobs = {}
105
+ @scheduled_jobs = {}
184
106
 
185
107
  Resque.schedule.each do |name, config|
186
108
  load_schedule_job(name, config)
187
109
  end
188
- Resque.redis.del(:schedules_changed)
189
- procline "Schedules Loaded"
110
+ Resque.redis.del(:schedules_changed) if am_master && dynamic
111
+ procline 'Schedules Loaded'
190
112
  end
191
113
 
192
114
  # modify interval type value to value with options if options available
@@ -195,17 +117,20 @@ module Resque
195
117
  if args.is_a?(::Array)
196
118
  return args.first if args.size > 2 || !args.last.is_a?(::Hash)
197
119
  # symbolize keys of hash for options
198
- args[1] = args[1].inject({}) do |m, i|
120
+ args[2] = args[1].reduce({}) do |m, i|
199
121
  key, value = i
200
- m[(key.to_sym rescue key) || key] = value
122
+ m[(key.respond_to?(:to_sym) ? key.to_sym : key) || key] = value
201
123
  m
202
124
  end
125
+
126
+ args[2][:job] = true
127
+ args[1] = nil
203
128
  end
204
129
  args
205
130
  end
206
131
 
207
- # Loads a job schedule into the Rufus::Scheduler and stores it in
208
- # @@scheduled_jobs
132
+ # Loads a job schedule into the Rufus::Scheduler and stores it
133
+ # in @scheduled_jobs
209
134
  def load_schedule_job(name, config)
210
135
  # If `rails_env` or `env` is set in the config, load jobs only if they
211
136
  # are meant to be loaded in `Resque::Scheduler.env`. If `rails_env` or
@@ -217,33 +142,33 @@ module Resque
217
142
  if configured_env.nil? || env_matches?(configured_env)
218
143
  log! "Scheduling #{name} "
219
144
  interval_defined = false
220
- interval_types = %w{cron every}
145
+ interval_types = %w(cron every)
221
146
  interval_types.each do |interval_type|
222
- if !config[interval_type].nil? && config[interval_type].length > 0
223
- args = optionizate_interval_value(config[interval_type])
224
- @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
225
- if is_master?
226
- log! "queueing #{config['class']} (#{name})"
227
- handle_errors { enqueue_from_config(config) }
228
- end
229
- end
230
- interval_defined = true
231
- break
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)
232
153
  end
154
+ @scheduled_jobs[name] = job
155
+ interval_defined = true
156
+ break
233
157
  end
234
158
  unless interval_defined
235
- log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
159
+ log! "no #{interval_types.join(' / ')} found for " \
160
+ "#{config['class']} (#{name}) - skipping"
236
161
  end
237
162
  else
238
- log "Skipping schedule of #{name} because configured " <<
239
- "env #{configured_env.inspect} does not match current " <<
163
+ log "Skipping schedule of #{name} because configured " \
164
+ "env #{configured_env.inspect} does not match current " \
240
165
  "env #{env.inspect}"
241
166
  end
242
167
  end
243
168
 
244
169
  # Returns true if the given schedule config hash matches the current env
245
170
  def rails_env_matches?(config)
246
- warn '`Resque::Scheduler.rails_env_matches?` is deprecated. ' <<
171
+ warn '`Resque::Scheduler.rails_env_matches?` is deprecated. ' \
247
172
  'Please use `Resque::Scheduler.env_matches?` instead.'
248
173
  config['rails_env'] && env &&
249
174
  config['rails_env'].split(/[\s,]+/).include?(env)
@@ -257,29 +182,110 @@ module Resque
257
182
 
258
183
  # Handles queueing delayed items
259
184
  # at_time - Time to start scheduling items (default: now).
260
- def handle_delayed_items(at_time=nil)
261
- if timestamp = Resque.next_delayed_timestamp(at_time)
262
- procline "Processing Delayed Items"
263
- while !timestamp.nil?
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?
264
190
  enqueue_delayed_items_for_timestamp(timestamp)
265
191
  timestamp = Resque.next_delayed_timestamp(at_time)
266
192
  end
267
193
  end
268
194
  end
269
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
+
270
207
  # Enqueues all delayed jobs for a timestamp
271
208
  def enqueue_delayed_items_for_timestamp(timestamp)
272
- item = nil
273
- begin
209
+ count = 0
210
+ batch_size = delayed_requeue_batch_size
211
+ actual_batch_size = nil
212
+
213
+ log "Processing delayed items for timestamp #{timestamp}, in batches of #{batch_size}"
214
+
215
+ loop do
274
216
  handle_shutdown do
275
217
  # Continually check that it is still the master
276
- if is_master? && item = Resque.next_item_for_timestamp(timestamp)
277
- log "queuing #{item['class']} [delayed]"
278
- handle_errors { enqueue_from_config(item) }
218
+ if am_master
219
+ actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
220
+ batch_size)
279
221
  end
280
222
  end
281
- # continue processing until there are no more ready items in this timestamp
282
- end while !item.nil?
223
+
224
+ count += actual_batch_size
225
+ log "queued #{count} jobs" if actual_batch_size != -1
226
+
227
+ # continue processing until there are no more items in this
228
+ # timestamp. If we don't have a full batch, this is the last one.
229
+ # This also breaks us in the event of a redis transaction failure
230
+ # i.e. enqueue_items_in_batch_for_timestamp returned -1
231
+ break if actual_batch_size < batch_size
232
+ end
233
+
234
+ log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count != -1
235
+ end
236
+
237
+ def timestamp_key(timestamp)
238
+ "delayed:#{timestamp.to_i}"
239
+ end
240
+
241
+ def enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
242
+ timestamp_bucket_key = timestamp_key(timestamp)
243
+
244
+ encoded_jobs_to_requeue = Resque.redis.lrange(timestamp_bucket_key, 0, batch_size - 1)
245
+
246
+ # Watch is used to ensure that the timestamp bucket we are operating on
247
+ # is not altered by any other clients between the watch call and when we call exec
248
+ # (to execute the multi block). We should error catch on the redis.exec return value
249
+ # as that will indicate if the entire transaction was aborted or not. Though we should
250
+ # be safe as our ltrim is inside the multi block and therefore also would have been
251
+ # aborted. So nothing would have been queued, but also nothing lost from the bucket.
252
+ watch_result = Resque.redis.watch(timestamp_bucket_key) do
253
+ Resque.redis.multi do |pipeline|
254
+ encoded_jobs_to_requeue.each do |encoded_job|
255
+ pipeline.srem("timestamps:#{encoded_job}", timestamp_bucket_key)
256
+
257
+ decoded_job = Resque.decode(encoded_job)
258
+ enqueue(decoded_job)
259
+ end
260
+
261
+ pipeline.ltrim(timestamp_bucket_key, batch_size, -1)
262
+ end
263
+ end
264
+
265
+ # Did the multi block successfully remove from this timestamp and enqueue the jobs?
266
+ success = !watch_result.nil?
267
+
268
+ # If this was the last batch in this timestamp bucket, clean up
269
+ if success && encoded_jobs_to_requeue.count < batch_size
270
+ Resque.clean_up_timestamp(timestamp_bucket_key, timestamp)
271
+ end
272
+
273
+ unless success
274
+ # Our batched transaction failed in Redis due to the timestamp_bucket_key value
275
+ # being modified while we built our multi block. We return -1 to ensure we break
276
+ # out of the loop iterating on this timestamp so it can be re-processed via the
277
+ # loop in handle_delayed_items.
278
+ return -1
279
+ end
280
+
281
+ # will return 0 if none were left to batch
282
+ encoded_jobs_to_requeue.count
283
+ end
284
+
285
+ def enqueue(config)
286
+ enqueue_from_config(config)
287
+ rescue => e
288
+ Resque::Scheduler.failure_handler.on_enqueue_failure(config, e)
283
289
  end
284
290
 
285
291
  def handle_shutdown
@@ -288,43 +294,50 @@ module Resque
288
294
  exit if @shutdown
289
295
  end
290
296
 
291
- def handle_errors
292
- begin
293
- yield
294
- rescue Exception => e
295
- log_error "#{e.class.name}: #{e.message}"
296
- end
297
- end
298
-
299
297
  # Enqueues a job based on a config hash
300
298
  def enqueue_from_config(job_config)
301
299
  args = job_config['args'] || job_config[:args]
302
300
 
303
301
  klass_name = job_config['class'] || job_config[:class]
304
- klass = ResqueScheduler::Util.constantize(klass_name) rescue klass_name
302
+ begin
303
+ klass = Resque::Scheduler::Util.constantize(klass_name)
304
+ rescue NameError
305
+ klass = klass_name
306
+ end
305
307
 
306
308
  params = args.is_a?(Hash) ? [args] : Array(args)
307
- queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
308
- # Support custom job classes like those that inherit from Resque::JobWithStatus (resque-status)
309
- if (job_klass = job_config['custom_job_class']) && (job_klass != 'Resque::Job')
310
- # The custom job class API must offer a static "scheduled" method. If the custom
311
- # job class can not be constantized (via a requeue call from the web perhaps), fall
312
- # back to enqueing normally via Resque::Job.create.
309
+ queue = job_config['queue'] ||
310
+ job_config[:queue] ||
311
+ Resque.queue_from_class(klass)
312
+ # Support custom job classes like those that inherit from
313
+ # Resque::JobWithStatus (resque-status)
314
+ job_klass = job_config['custom_job_class']
315
+ if job_klass && job_klass != 'Resque::Job'
316
+ # The custom job class API must offer a static "scheduled" method. If
317
+ # the custom job class can not be constantized (via a requeue call
318
+ # from the web perhaps), fall back to enqueuing normally via
319
+ # Resque::Job.create.
313
320
  begin
314
- ResqueScheduler::Util.constantize(job_klass).scheduled(queue, klass_name, *params)
321
+ Resque::Scheduler::Util.constantize(job_klass).scheduled(
322
+ queue, klass_name, *params
323
+ )
315
324
  rescue NameError
316
- # Note that the custom job class (job_config['custom_job_class']) is the one enqueued
325
+ # Note that the custom job class (job_config['custom_job_class'])
326
+ # is the one enqueued
317
327
  Resque::Job.create(queue, job_klass, *params)
318
328
  end
319
329
  else
320
- # hack to avoid havoc for people shoving stuff into queues
330
+ # Hack to avoid havoc for people shoving stuff into queues
321
331
  # for non-existent classes (for example: running scheduler in
322
- # one app that schedules for another
332
+ # one app that schedules for another.
323
333
  if Class === klass
324
- ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
334
+ Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(
335
+ klass, *params
336
+ )
325
337
 
326
- # If the class is a custom job class, call self#scheduled on it. This allows you to do things like
327
- # Resque.enqueue_at(timestamp, CustomJobClass). Otherwise, pass off to Resque.
338
+ # If the class is a custom job class, call self#scheduled on it.
339
+ # This allows you to do things like Resque.enqueue_at(timestamp,
340
+ # CustomJobClass). Otherwise, pass off to Resque.
328
341
  if klass.respond_to?(:scheduled)
329
342
  klass.scheduled(queue, klass_name, *params)
330
343
  else
@@ -339,7 +352,7 @@ module Resque
339
352
  end
340
353
 
341
354
  def rufus_scheduler
342
- @rufus_scheduler ||= Rufus::Scheduler.start_new
355
+ @rufus_scheduler ||= Rufus::Scheduler.new
343
356
  end
344
357
 
345
358
  # Stops old rufus scheduler and creates a new one. Returns the new
@@ -347,21 +360,23 @@ module Resque
347
360
  def clear_schedule!
348
361
  rufus_scheduler.stop
349
362
  @rufus_scheduler = nil
350
- @@scheduled_jobs = {}
363
+ @scheduled_jobs = {}
351
364
  rufus_scheduler
352
365
  end
353
366
 
354
367
  def reload_schedule!
355
- procline "Reloading Schedule"
368
+ procline 'Reloading Schedule'
356
369
  clear_schedule!
357
370
  load_schedule!
358
371
  end
359
372
 
360
373
  def update_schedule
361
374
  if Resque.redis.scard(:schedules_changed) > 0
362
- procline "Updating schedule"
363
- Resque.reload_schedule!
364
- while schedule_name = Resque.redis.spop(:schedules_changed)
375
+ procline 'Updating schedule'
376
+ loop do
377
+ schedule_name = Resque.redis.spop(:schedules_changed)
378
+ break unless schedule_name
379
+ Resque.reload_schedule!
365
380
  if Resque.schedule.keys.include?(schedule_name)
366
381
  unschedule_job(schedule_name)
367
382
  load_schedule_job(schedule_name, Resque.schedule[schedule_name])
@@ -369,7 +384,7 @@ module Resque
369
384
  unschedule_job(schedule_name)
370
385
  end
371
386
  end
372
- procline "Schedules Loaded"
387
+ procline 'Schedules Loaded'
373
388
  end
374
389
  end
375
390
 
@@ -377,7 +392,7 @@ module Resque
377
392
  if scheduled_jobs[name]
378
393
  log "Removing schedule #{name}"
379
394
  scheduled_jobs[name].unschedule
380
- @@scheduled_jobs.delete(name)
395
+ @scheduled_jobs.delete(name)
381
396
  end
382
397
  end
383
398
 
@@ -385,16 +400,7 @@ module Resque
385
400
  def poll_sleep
386
401
  handle_shutdown do
387
402
  begin
388
- begin
389
- @sleeping = true
390
- sleep poll_sleep_amount
391
- @sleeping = false
392
- rescue Interrupt
393
- if @shutdown
394
- Resque.clean_schedules
395
- release_master_lock!
396
- end
397
- end
403
+ poll_sleep_loop
398
404
  ensure
399
405
  @sleeping = false
400
406
  end
@@ -402,6 +408,46 @@ module Resque
402
408
  true
403
409
  end
404
410
 
411
+ def poll_sleep_loop
412
+ @sleeping = true
413
+ if poll_sleep_amount > 0
414
+ start = Time.now
415
+ loop do
416
+ elapsed_sleep = (Time.now - start)
417
+ remaining_sleep = poll_sleep_amount - elapsed_sleep
418
+ @do_break = false
419
+ if remaining_sleep <= 0
420
+ @do_break = true
421
+ else
422
+ @do_break = handle_signals_with_operation do
423
+ sleep(remaining_sleep)
424
+ end
425
+ end
426
+ break if @do_break
427
+ end
428
+ else
429
+ handle_signals_with_operation
430
+ end
431
+ end
432
+
433
+ def handle_signals_with_operation
434
+ yield if block_given?
435
+ handle_signals
436
+ false
437
+ rescue Interrupt
438
+ before_shutdown if @shutdown
439
+ true
440
+ end
441
+
442
+ def stop_rufus_scheduler
443
+ rufus_scheduler.shutdown(:wait)
444
+ end
445
+
446
+ def before_shutdown
447
+ stop_rufus_scheduler
448
+ release_master_lock
449
+ end
450
+
405
451
  # Sets the shutdown flag, clean schedules and exits if sleeping
406
452
  def shutdown
407
453
  return if @shutdown
@@ -429,8 +475,29 @@ module Resque
429
475
  $0 = argv0
430
476
  end
431
477
 
478
+ def failure_handler
479
+ @failure_handler ||= Resque::Scheduler::FailureHandler
480
+ end
481
+
482
+ def logger
483
+ @logger ||= Resque::Scheduler::LoggerBuilder.new(
484
+ quiet: quiet,
485
+ verbose: verbose,
486
+ log_dev: logfile,
487
+ format: logformat
488
+ ).build
489
+ end
490
+
432
491
  private
433
492
 
493
+ def enqueue_recurring(name, config)
494
+ if am_master
495
+ log! "queueing #{config['class']} (#{name})"
496
+ enqueue(config)
497
+ Resque.last_enqueued_at(name, Time.now.to_s)
498
+ end
499
+ end
500
+
434
501
  def app_str
435
502
  app_name ? "[#{app_name}]" : ''
436
503
  end
@@ -444,7 +511,12 @@ module Resque
444
511
  end
445
512
 
446
513
  def internal_name
447
- "resque-scheduler-#{ResqueScheduler::VERSION}"
514
+ "resque-scheduler-#{Resque::Scheduler::VERSION}"
515
+ end
516
+
517
+ def am_master
518
+ @am_master = master? unless defined?(@am_master)
519
+ @am_master
448
520
  end
449
521
  end
450
522
  end
@@ -1,2 +1,4 @@
1
1
  # vim:fileencoding=utf-8
2
- require File.expand_path('../resque_scheduler', __FILE__)
2
+ require_relative 'resque/scheduler'
3
+
4
+ Resque.extend Resque::Scheduler::Extension