resque-scheduler 2.2.0 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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 +31 -0
  8. data/CHANGELOG.md +539 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +26 -2
  11. data/LICENSE +1 -1
  12. data/README.md +423 -91
  13. data/Rakefile +12 -35
  14. data/exe/resque-scheduler +5 -0
  15. data/lib/resque/scheduler/cli.rb +147 -0
  16. data/lib/resque/scheduler/configuration.rb +102 -0
  17. data/lib/resque/scheduler/delaying_extensions.rb +371 -0
  18. data/lib/resque/scheduler/env.rb +85 -0
  19. data/lib/resque/scheduler/extension.rb +13 -0
  20. data/lib/resque/scheduler/failure_handler.rb +11 -0
  21. data/lib/resque/scheduler/lock/base.rb +12 -3
  22. data/lib/resque/scheduler/lock/basic.rb +4 -5
  23. data/lib/resque/scheduler/lock/resilient.rb +52 -43
  24. data/lib/resque/scheduler/lock.rb +2 -1
  25. data/lib/resque/scheduler/locking.rb +104 -0
  26. data/lib/resque/scheduler/logger_builder.rb +83 -0
  27. data/lib/resque/scheduler/plugin.rb +31 -0
  28. data/lib/resque/scheduler/scheduling_extensions.rb +142 -0
  29. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +23 -8
  30. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  31. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +1 -1
  32. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  33. data/lib/resque/scheduler/server/views/search.erb +69 -0
  34. data/lib/resque/scheduler/server/views/search_form.erb +4 -0
  35. data/lib/resque/scheduler/server.rb +268 -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. data/lib/resque/scheduler.rb +333 -149
  41. data/lib/resque-scheduler.rb +4 -0
  42. data/resque-scheduler.gemspec +56 -20
  43. metadata +205 -104
  44. data/.gitignore +0 -8
  45. data/.rubocop.yml +0 -120
  46. data/.travis.yml +0 -10
  47. data/HISTORY.md +0 -180
  48. data/lib/resque/scheduler_locking.rb +0 -90
  49. data/lib/resque_scheduler/logger_builder.rb +0 -51
  50. data/lib/resque_scheduler/plugin.rb +0 -25
  51. data/lib/resque_scheduler/server/views/scheduler.erb +0 -44
  52. data/lib/resque_scheduler/server.rb +0 -92
  53. data/lib/resque_scheduler/tasks.rb +0 -40
  54. data/lib/resque_scheduler/version.rb +0 -3
  55. data/lib/resque_scheduler.rb +0 -355
  56. data/script/migrate_to_timestamps_set.rb +0 -14
  57. data/tasks/resque_scheduler.rake +0 -2
  58. data/test/delayed_queue_test.rb +0 -383
  59. data/test/redis-test.conf +0 -108
  60. data/test/resque-web_test.rb +0 -116
  61. data/test/scheduler_args_test.rb +0 -156
  62. data/test/scheduler_hooks_test.rb +0 -23
  63. data/test/scheduler_locking_test.rb +0 -180
  64. data/test/scheduler_setup_test.rb +0 -59
  65. data/test/scheduler_test.rb +0 -256
  66. data/test/support/redis_instance.rb +0 -129
  67. data/test/test_helper.rb +0 -92
  68. /data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
@@ -1,49 +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
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
6
20
 
7
- class Scheduler
8
-
9
- extend Resque::SchedulerLocking
10
-
11
- class << self
12
-
13
- # If true, logs more stuff...
14
- attr_accessor :verbose
15
-
16
- # If set, produces no output
17
- attr_accessor :mute
21
+ private
18
22
 
19
- # If set, will write messages to the file
20
- attr_accessor :logfile
23
+ extend Resque::Scheduler::Locking
24
+ extend Resque::Scheduler::Configuration
25
+ extend Resque::Scheduler::SignalHandling
21
26
 
22
- # If set, will try to update the schedule in the loop
23
- attr_accessor :dynamic
24
-
25
- # Amount of time in seconds to sleep between polls of the delayed
26
- # queue. Defaults to 5
27
- attr_writer :poll_sleep_amount
27
+ public
28
28
 
29
+ class << self
29
30
  attr_writer :logger
30
31
 
31
32
  # the Rufus::Scheduler jobs that are scheduled
32
- def scheduled_jobs
33
- @@scheduled_jobs
34
- end
33
+ attr_reader :scheduled_jobs
35
34
 
36
- def poll_sleep_amount
37
- @poll_sleep_amount ||= 5 # seconds
38
- end
39
-
40
- def logger
41
- @logger ||= ResqueScheduler::LoggerBuilder.new(:mute => mute, :verbose => verbose, :log_dev => logfile).build
42
- end
35
+ # allow user to set an additional failure handler
36
+ attr_writer :failure_handler
43
37
 
44
38
  # Schedule all jobs and continually look for delayed jobs (never returns)
45
39
  def run
46
- $0 = "resque-scheduler: Starting"
40
+ procline 'Starting'
47
41
 
48
42
  # trap signals
49
43
  register_signal_handlers
@@ -54,52 +48,45 @@ module Resque
54
48
  $stdout.sync = true
55
49
  $stderr.sync = true
56
50
 
57
- # Load the schedule into rufus
58
- # If dynamic is set, load that schedule otherwise use normal load
59
- if dynamic
60
- reload_schedule!
61
- else
62
- load_schedule!
63
- end
51
+ was_master = nil
64
52
 
65
- # Now start the scheduling part of the loop.
66
- loop do
67
- if is_master?
68
- begin
69
- handle_delayed_items
70
- update_schedule if dynamic
71
- rescue Errno::EAGAIN, Errno::ECONNRESET => e
72
- warn e.message
73
- end
74
- end
75
- poll_sleep
76
- end
53
+ begin
54
+ @th = Thread.current
77
55
 
78
- # never gets here.
79
- end
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'
80
63
 
64
+ # Load schedule because changed
65
+ reload_schedule!
66
+ end
81
67
 
82
- # For all signals, set the shutdown flag and wait for current
83
- # poll/enqueing to finish (should be almost istant). In the
84
- # case of sleeping, exit immediately.
85
- def register_signal_handlers
86
- trap("TERM") { shutdown }
87
- trap("INT") { shutdown }
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
88
79
 
89
- begin
90
- trap('QUIT') { shutdown }
91
- trap('USR1') { print_schedule }
92
- trap('USR2') { reload_schedule! }
93
- rescue ArgumentError
94
- warn "Signals QUIT and USR1 and USR2 not supported."
80
+ rescue Interrupt
81
+ log 'Exiting'
95
82
  end
96
83
  end
97
84
 
98
85
  def print_schedule
99
86
  if rufus_scheduler
100
87
  log! "Scheduling Info\tLast Run"
101
- scheduler_jobs = rufus_scheduler.all_jobs
102
- scheduler_jobs.each do |k, v|
88
+ scheduler_jobs = rufus_scheduler.jobs
89
+ scheduler_jobs.each do |_k, v|
103
90
  log! "#{v.t}\t#{v.last}\t"
104
91
  end
105
92
  end
@@ -108,20 +95,20 @@ module Resque
108
95
  # Pulls the schedule from Resque.schedule and loads it into the
109
96
  # rufus scheduler instance
110
97
  def load_schedule!
111
- procline "Loading Schedule"
98
+ procline 'Loading Schedule'
112
99
 
113
100
  # Need to load the schedule from redis for the first time if dynamic
114
101
  Resque.reload_schedule! if dynamic
115
102
 
116
- log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
103
+ log! 'Schedule empty! Set Resque.schedule' if Resque.schedule.empty?
117
104
 
118
- @@scheduled_jobs = {}
105
+ @scheduled_jobs = {}
119
106
 
120
107
  Resque.schedule.each do |name, config|
121
108
  load_schedule_job(name, config)
122
109
  end
123
- Resque.redis.del(:schedules_changed)
124
- procline "Schedules Loaded"
110
+ Resque.redis.del(:schedules_changed) if am_master && dynamic
111
+ procline 'Schedules Loaded'
125
112
  end
126
113
 
127
114
  # modify interval type value to value with options if options available
@@ -130,75 +117,175 @@ module Resque
130
117
  if args.is_a?(::Array)
131
118
  return args.first if args.size > 2 || !args.last.is_a?(::Hash)
132
119
  # symbolize keys of hash for options
133
- args[1] = args[1].inject({}) do |m, i|
120
+ args[2] = args[1].reduce({}) do |m, i|
134
121
  key, value = i
135
- m[(key.to_sym rescue key) || key] = value
122
+ m[(key.respond_to?(:to_sym) ? key.to_sym : key) || key] = value
136
123
  m
137
124
  end
125
+
126
+ args[2][:job] = true
127
+ args[1] = nil
138
128
  end
139
129
  args
140
130
  end
141
131
 
142
- # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
132
+ # Loads a job schedule into the Rufus::Scheduler and stores it
133
+ # in @scheduled_jobs
143
134
  def load_schedule_job(name, config)
144
- # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
145
- # required for the jobs to be scheduled. If rails_env is missing, the
146
- # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
147
- # to.
148
- if config['rails_env'].nil? || rails_env_matches?(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)
149
143
  log! "Scheduling #{name} "
150
144
  interval_defined = false
151
- interval_types = %w{cron every}
145
+ interval_types = %w(cron every)
152
146
  interval_types.each do |interval_type|
153
- if !config[interval_type].nil? && config[interval_type].length > 0
154
- args = optionizate_interval_value(config[interval_type])
155
- @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
156
- if is_master?
157
- log! "queueing #{config['class']} (#{name})"
158
- handle_errors { enqueue_from_config(config) }
159
- end
160
- end
161
- interval_defined = true
162
- 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)
163
153
  end
154
+ @scheduled_jobs[name] = job
155
+ interval_defined = true
156
+ break
164
157
  end
165
158
  unless interval_defined
166
- log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
159
+ log! "no #{interval_types.join(' / ')} found for " \
160
+ "#{config['class']} (#{name}) - skipping"
167
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}"
168
166
  end
169
167
  end
170
168
 
171
- # Returns true if the given schedule config hash matches the current
172
- # ENV['RAILS_ENV']
169
+ # Returns true if the given schedule config hash matches the current env
173
170
  def rails_env_matches?(config)
174
- config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
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)
175
181
  end
176
182
 
177
183
  # Handles queueing delayed items
178
184
  # at_time - Time to start scheduling items (default: now).
179
- def handle_delayed_items(at_time=nil)
180
- if timestamp = Resque.next_delayed_timestamp(at_time)
181
- procline "Processing Delayed Items"
182
- 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?
183
190
  enqueue_delayed_items_for_timestamp(timestamp)
184
191
  timestamp = Resque.next_delayed_timestamp(at_time)
185
192
  end
186
193
  end
187
194
  end
188
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
+
189
207
  # Enqueues all delayed jobs for a timestamp
190
208
  def enqueue_delayed_items_for_timestamp(timestamp)
191
- item = nil
192
- 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
193
216
  handle_shutdown do
194
217
  # Continually check that it is still the master
195
- if is_master? && item = Resque.next_item_for_timestamp(timestamp)
196
- log "queuing #{item['class']} [delayed]"
197
- handle_errors { enqueue_from_config(item) }
218
+ if am_master
219
+ actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
220
+ batch_size)
221
+ end
222
+ end
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)
198
259
  end
260
+
261
+ pipeline.ltrim(timestamp_bucket_key, batch_size, -1)
199
262
  end
200
- # continue processing until there are no more ready items in this timestamp
201
- end while !item.nil?
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)
202
289
  end
203
290
 
204
291
  def handle_shutdown
@@ -207,43 +294,50 @@ module Resque
207
294
  exit if @shutdown
208
295
  end
209
296
 
210
- def handle_errors
211
- begin
212
- yield
213
- rescue Exception => e
214
- log! "#{e.class.name}: #{e.message}"
215
- end
216
- end
217
-
218
297
  # Enqueues a job based on a config hash
219
298
  def enqueue_from_config(job_config)
220
299
  args = job_config['args'] || job_config[:args]
221
300
 
222
301
  klass_name = job_config['class'] || job_config[:class]
223
- klass = Resque.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
224
307
 
225
308
  params = args.is_a?(Hash) ? [args] : Array(args)
226
- queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
227
- # Support custom job classes like those that inherit from Resque::JobWithStatus (resque-status)
228
- if (job_klass = job_config['custom_job_class']) && (job_klass != 'Resque::Job')
229
- # The custom job class API must offer a static "scheduled" method. If the custom
230
- # job class can not be constantized (via a requeue call from the web perhaps), fall
231
- # 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.
232
320
  begin
233
- Resque.constantize(job_klass).scheduled(queue, klass_name, *params)
321
+ Resque::Scheduler::Util.constantize(job_klass).scheduled(
322
+ queue, klass_name, *params
323
+ )
234
324
  rescue NameError
235
- # 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
236
327
  Resque::Job.create(queue, job_klass, *params)
237
328
  end
238
329
  else
239
- # hack to avoid havoc for people shoving stuff into queues
330
+ # Hack to avoid havoc for people shoving stuff into queues
240
331
  # for non-existent classes (for example: running scheduler in
241
- # one app that schedules for another
332
+ # one app that schedules for another.
242
333
  if Class === klass
243
- ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
334
+ Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(
335
+ klass, *params
336
+ )
244
337
 
245
- # If the class is a custom job class, call self#scheduled on it. This allows you to do things like
246
- # 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.
247
341
  if klass.respond_to?(:scheduled)
248
342
  klass.scheduled(queue, klass_name, *params)
249
343
  else
@@ -258,7 +352,7 @@ module Resque
258
352
  end
259
353
 
260
354
  def rufus_scheduler
261
- @rufus_scheduler ||= Rufus::Scheduler.start_new
355
+ @rufus_scheduler ||= Rufus::Scheduler.new
262
356
  end
263
357
 
264
358
  # Stops old rufus scheduler and creates a new one. Returns the new
@@ -266,21 +360,23 @@ module Resque
266
360
  def clear_schedule!
267
361
  rufus_scheduler.stop
268
362
  @rufus_scheduler = nil
269
- @@scheduled_jobs = {}
363
+ @scheduled_jobs = {}
270
364
  rufus_scheduler
271
365
  end
272
366
 
273
367
  def reload_schedule!
274
- procline "Reloading Schedule"
368
+ procline 'Reloading Schedule'
275
369
  clear_schedule!
276
370
  load_schedule!
277
371
  end
278
372
 
279
373
  def update_schedule
280
374
  if Resque.redis.scard(:schedules_changed) > 0
281
- procline "Updating schedule"
282
- Resque.reload_schedule!
283
- 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!
284
380
  if Resque.schedule.keys.include?(schedule_name)
285
381
  unschedule_job(schedule_name)
286
382
  load_schedule_job(schedule_name, Resque.schedule[schedule_name])
@@ -288,7 +384,7 @@ module Resque
288
384
  unschedule_job(schedule_name)
289
385
  end
290
386
  end
291
- procline "Schedules Loaded"
387
+ procline 'Schedules Loaded'
292
388
  end
293
389
  end
294
390
 
@@ -296,44 +392,132 @@ module Resque
296
392
  if scheduled_jobs[name]
297
393
  log "Removing schedule #{name}"
298
394
  scheduled_jobs[name].unschedule
299
- @@scheduled_jobs.delete(name)
395
+ @scheduled_jobs.delete(name)
300
396
  end
301
397
  end
302
398
 
303
399
  # Sleeps and returns true
304
400
  def poll_sleep
401
+ handle_shutdown do
402
+ begin
403
+ poll_sleep_loop
404
+ ensure
405
+ @sleeping = false
406
+ end
407
+ end
408
+ true
409
+ end
410
+
411
+ def poll_sleep_loop
305
412
  @sleeping = true
306
- handle_shutdown { sleep poll_sleep_amount }
307
- @sleeping = false
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
308
439
  true
309
440
  end
310
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
+
311
451
  # Sets the shutdown flag, clean schedules and exits if sleeping
312
452
  def shutdown
453
+ return if @shutdown
313
454
  @shutdown = true
314
-
315
- if @sleeping
316
- Resque.clean_schedules
317
- Thread.new { release_master_lock! }
318
- exit
319
- end
455
+ log!('Shutting down')
456
+ @th.raise Interrupt if @sleeping
320
457
  end
321
458
 
322
459
  def log!(msg)
323
- logger.info msg
460
+ logger.info { msg }
461
+ end
462
+
463
+ def log_error(msg)
464
+ logger.error { msg }
324
465
  end
325
466
 
326
467
  def log(msg)
327
- logger.debug msg
468
+ logger.debug { msg }
328
469
  end
329
470
 
330
471
  def procline(string)
331
472
  log! string
332
- $0 = "resque-scheduler-#{ResqueScheduler::VERSION}: #{string}"
473
+ argv0 = build_procline(string)
474
+ log "Setting procline #{argv0.inspect}"
475
+ $0 = argv0
333
476
  end
334
477
 
335
- end
478
+ def failure_handler
479
+ @failure_handler ||= Resque::Scheduler::FailureHandler
480
+ end
336
481
 
337
- end
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
+
491
+ private
338
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
+
501
+ def app_str
502
+ app_name ? "[#{app_name}]" : ''
503
+ end
504
+
505
+ def env_str
506
+ env ? "[#{env}]" : ''
507
+ end
508
+
509
+ def build_procline(string)
510
+ "#{internal_name}#{app_str}#{env_str}: #{string}"
511
+ end
512
+
513
+ def internal_name
514
+ "resque-scheduler-#{Resque::Scheduler::VERSION}"
515
+ end
516
+
517
+ def am_master
518
+ @am_master = master? unless defined?(@am_master)
519
+ @am_master
520
+ end
521
+ end
522
+ end
339
523
  end
@@ -0,0 +1,4 @@
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'resque/scheduler'
3
+
4
+ Resque.extend Resque::Scheduler::Extension