resque-scheduler 2.0.0 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of resque-scheduler might be problematic. Click here for more details.

@@ -1,11 +1,12 @@
1
1
  require 'rufus/scheduler'
2
- require 'thwait'
2
+ require 'resque/scheduler_locking'
3
+ require 'resque_scheduler/logger_builder'
3
4
 
4
5
  module Resque
5
6
 
6
7
  class Scheduler
7
8
 
8
- extend Resque::Helpers
9
+ extend Resque::SchedulerLocking
9
10
 
10
11
  class << self
11
12
 
@@ -15,13 +16,18 @@ module Resque
15
16
  # If set, produces no output
16
17
  attr_accessor :mute
17
18
 
18
- # If set, will try to update the schulde in the loop
19
+ # If set, will write messages to the file
20
+ attr_accessor :logfile
21
+
22
+ # If set, will try to update the schedule in the loop
19
23
  attr_accessor :dynamic
20
24
 
21
25
  # Amount of time in seconds to sleep between polls of the delayed
22
26
  # queue. Defaults to 5
23
27
  attr_writer :poll_sleep_amount
24
28
 
29
+ attr_writer :logger
30
+
25
31
  # the Rufus::Scheduler jobs that are scheduled
26
32
  def scheduled_jobs
27
33
  @@scheduled_jobs
@@ -31,12 +37,23 @@ module Resque
31
37
  @poll_sleep_amount ||= 5 # seconds
32
38
  end
33
39
 
40
+ def logger
41
+ @logger ||= ResqueScheduler::LoggerBuilder.new(:mute => mute, :verbose => verbose, :log_dev => logfile).build
42
+ end
43
+
34
44
  # Schedule all jobs and continually look for delayed jobs (never returns)
35
45
  def run
36
46
  $0 = "resque-scheduler: Starting"
47
+
37
48
  # trap signals
38
49
  register_signal_handlers
39
50
 
51
+ # Quote from the resque/worker.
52
+ # Fix buffering so we can `rake resque:scheduler > scheduler.log` and
53
+ # get output from the child in there.
54
+ $stdout.sync = true
55
+ $stderr.sync = true
56
+
40
57
  # Load the schedule into rufus
41
58
  # If dynamic is set, load that schedule otherwise use normal load
42
59
  if dynamic
@@ -47,11 +64,13 @@ module Resque
47
64
 
48
65
  # Now start the scheduling part of the loop.
49
66
  loop do
50
- begin
51
- handle_delayed_items
52
- update_schedule if dynamic
53
- rescue Errno::EAGAIN, Errno::ECONNRESET => e
54
- warn e.message
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
55
74
  end
56
75
  poll_sleep
57
76
  end
@@ -59,6 +78,7 @@ module Resque
59
78
  # never gets here.
60
79
  end
61
80
 
81
+
62
82
  # For all signals, set the shutdown flag and wait for current
63
83
  # poll/enqueing to finish (should be almost istant). In the
64
84
  # case of sleeping, exit immediately.
@@ -133,8 +153,10 @@ module Resque
133
153
  if !config[interval_type].nil? && config[interval_type].length > 0
134
154
  args = optionizate_interval_value(config[interval_type])
135
155
  @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
136
- log! "queueing #{config['class']} (#{name})"
137
- handle_errors { enqueue_from_config(config) }
156
+ if is_master?
157
+ log! "queueing #{config['class']} (#{name})"
158
+ handle_errors { enqueue_from_config(config) }
159
+ end
138
160
  end
139
161
  interval_defined = true
140
162
  break
@@ -169,7 +191,8 @@ module Resque
169
191
  item = nil
170
192
  begin
171
193
  handle_shutdown do
172
- if item = Resque.next_item_for_timestamp(timestamp)
194
+ # Continually check that it is still the master
195
+ if is_master? && item = Resque.next_item_for_timestamp(timestamp)
173
196
  log "queuing #{item['class']} [delayed]"
174
197
  handle_errors { enqueue_from_config(item) }
175
198
  end
@@ -197,7 +220,7 @@ module Resque
197
220
  args = job_config['args'] || job_config[:args]
198
221
 
199
222
  klass_name = job_config['class'] || job_config[:class]
200
- klass = constantize(klass_name) rescue klass_name
223
+ klass = ResqueScheduler::Util.constantize(klass_name) rescue klass_name
201
224
 
202
225
  params = args.is_a?(Hash) ? [args] : Array(args)
203
226
  queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
@@ -207,7 +230,7 @@ module Resque
207
230
  # job class can not be constantized (via a requeue call from the web perhaps), fall
208
231
  # back to enqueing normally via Resque::Job.create.
209
232
  begin
210
- constantize(job_klass).scheduled(queue, klass_name, *params)
233
+ ResqueScheduler::Util.constantize(job_klass).scheduled(queue, klass_name, *params)
211
234
  rescue NameError
212
235
  # Note that the custom job class (job_config['custom_job_class']) is the one enqueued
213
236
  Resque::Job.create(queue, job_klass, *params)
@@ -218,7 +241,14 @@ module Resque
218
241
  # one app that schedules for another
219
242
  if Class === klass
220
243
  ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
221
- Resque.enqueue_to(queue, klass, *params)
244
+
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.
247
+ if klass.respond_to?(:scheduled)
248
+ klass.scheduled(queue, klass_name, *params)
249
+ else
250
+ Resque.enqueue_to(queue, klass, *params)
251
+ end
222
252
  else
223
253
  # This will not run the before_hooks in rescue, but will at least
224
254
  # queue the job.
@@ -278,19 +308,26 @@ module Resque
278
308
  true
279
309
  end
280
310
 
281
- # Sets the shutdown flag, exits if sleeping
311
+ # Sets the shutdown flag, clean schedules and exits if sleeping
282
312
  def shutdown
283
313
  @shutdown = true
284
- exit if @sleeping
314
+
315
+ if @sleeping
316
+ thread = Thread.new do
317
+ Resque.clean_schedules
318
+ release_master_lock!
319
+ end
320
+ thread.join
321
+ exit
322
+ end
285
323
  end
286
324
 
287
325
  def log!(msg)
288
- puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
326
+ logger.info msg
289
327
  end
290
328
 
291
329
  def log(msg)
292
- # add "verbose" logic later
293
- log!(msg) if verbose
330
+ logger.debug msg
294
331
  end
295
332
 
296
333
  def procline(string)
@@ -0,0 +1,89 @@
1
+ # ### Locking the scheduler process
2
+ #
3
+ # There are two places in resque-scheduler that need to be synchonized
4
+ # in order to be able to run redundant scheduler processes while ensuring jobs don't
5
+ # get queued multiple times when the master process changes.
6
+ #
7
+ # 1) Processing the delayed queues (jobs that are created from enqueue_at/enqueue_in, etc)
8
+ # 2) Processing the scheduled (cron-like) jobs from rufus-scheduler
9
+ #
10
+ # Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
11
+ # redis would suffice. However, protecting the scheduled jobs is trickier
12
+ # because the clocks on machines could be slightly off or actual firing times
13
+ # could vary slightly due to load. If scheduler A's clock is slightly ahead
14
+ # of scheduler B's clock (since they are on different machines), when
15
+ # scheduler A dies, we need to ensure that scheduler B doesn't queue jobs
16
+ # that A already queued before it's death. (This all assumes that it is
17
+ # better to miss a few scheduled jobs than it is to run them multiple times
18
+ # for the same iteration.)
19
+ #
20
+ # To avoid queuing multiple jobs in the case of master fail-over, the master
21
+ # should remain the master as long as it can rather than a simple SETNX which
22
+ # would result in the master roll being passed around frequently.
23
+ #
24
+ # Locking Scheme:
25
+ # Each resque-scheduler process attempts to get the master lock via SETNX.
26
+ # Once obtained, it sets the expiration for 3 minutes (configurable). The
27
+ # master process continually updates the timeout on the lock key to be 3
28
+ # minutes in the future in it's loop(s) (see `run`) and when jobs come out of
29
+ # rufus-scheduler (see `load_schedule_job`). That ensures that a minimum of
30
+ # 3 minutes must pass since the last queuing operation before a new master is
31
+ # chosen. If, for whatever reason, the master fails to update the expiration
32
+ # for 3 minutes, the key expires and the lock is up for grabs. If
33
+ # miraculously the original master comes back to life, it will realize it is
34
+ # no longer the master and stop processing jobs.
35
+ #
36
+ # The clocks on the scheduler machines can then be up to 3 minutes off from
37
+ # each other without the risk of queueing the same scheduled job twice during
38
+ # a master change. The catch is, in the event of a master change, no
39
+ # scheduled jobs will be queued during those 3 minutes. So, there is a trade
40
+ # off: the higher the timeout, the less likely scheduled jobs will be fired
41
+ # twice but greater chances of missing scheduled jobs. The lower the timeout,
42
+ # less likely jobs will be missed, greater the chances of jobs firing twice. If
43
+ # you don't care about jobs firing twice or are certain your machines' clocks
44
+ # are well in sync, a lower timeout is preferable. One thing to keep in mind:
45
+ # this only effects *scheduled* jobs - delayed jobs will never be lost or
46
+ # skipped since eventually a master will come online and it will process
47
+ # everything that is ready (no matter how old it is). Scheduled jobs work
48
+ # like cron - if you stop cron, no jobs fire while it's stopped and it doesn't
49
+ # fire jobs that were missed when it starts up again.
50
+
51
+ require 'resque/scheduler/lock'
52
+
53
+ module Resque
54
+ module SchedulerLocking
55
+ def master_lock
56
+ @master_lock ||= build_master_lock
57
+ end
58
+
59
+ def supports_lua?
60
+ redis_master_version >= 2.5
61
+ end
62
+
63
+ def is_master?
64
+ master_lock.acquire! || master_lock.locked?
65
+ end
66
+
67
+ def release_master_lock!
68
+ master_lock.release!
69
+ end
70
+
71
+ private
72
+
73
+ def build_master_lock
74
+ if supports_lua?
75
+ Resque::Scheduler::Lock::Resilient.new(master_lock_key)
76
+ else
77
+ Resque::Scheduler::Lock::Basic.new(master_lock_key)
78
+ end
79
+ end
80
+
81
+ def master_lock_key
82
+ "#{ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''}resque_scheduler_master_lock".to_sym
83
+ end
84
+
85
+ def redis_master_version
86
+ Resque.redis.info['redis_version'].to_f
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,51 @@
1
+ module ResqueScheduler
2
+ # Just builds a logger, with specified verbosity and destination.
3
+ # The simplest example:
4
+ #
5
+ # ResqueScheduler::LoggerBuilder.new.build
6
+ class LoggerBuilder
7
+ # Initializes new instance of the builder
8
+ #
9
+ # Pass :opts Hash with
10
+ # - :mute if logger needs to be silent for all levels. Default - false
11
+ # - :verbose if there is a need in debug messages. Default - false
12
+ # - :log_dev to output logs into a desired file. Default - STDOUT
13
+ #
14
+ # Example:
15
+ #
16
+ # LoggerBuilder.new(:mute => false, :verbose => true, :log_dev => 'log/sheduler.log')
17
+ def initialize(opts={})
18
+ @muted = !!opts[:mute]
19
+ @verbose = !!opts[:verbose]
20
+ @log_dev = opts[:log_dev] || STDOUT
21
+ end
22
+
23
+ # Returns an instance of Logger
24
+ def build
25
+ logger = Logger.new(@log_dev)
26
+ logger.level = level
27
+ logger.datetime_format = "%Y-%m-%d %H:%M:%S"
28
+ logger.formatter = formatter
29
+
30
+ logger
31
+ end
32
+
33
+ private
34
+
35
+ def level
36
+ if @verbose && !@muted
37
+ Logger::DEBUG
38
+ elsif !@muted
39
+ Logger::INFO
40
+ else
41
+ Logger::FATAL
42
+ end
43
+ end
44
+
45
+ def formatter
46
+ proc do |severity, datetime, progname, msg|
47
+ "[#{severity}] #{datetime}: #{msg}\n"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -8,6 +8,7 @@
8
8
 
9
9
  <p class='intro'>
10
10
  This list below contains the timestamps for scheduled delayed jobs.
11
+ Server local time: <%= Time.now %>
11
12
  </p>
12
13
 
13
14
  <p class='sub'>
@@ -45,4 +46,4 @@
45
46
  <% end %>
46
47
  </table>
47
48
 
48
- <%= partial :next_more, :start => start, :size => size %>
49
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,23 @@
1
+ <h1><%= @job_name %></h1>
2
+
3
+ <p class='intro'>
4
+ This job requires parameters:
5
+ </p>
6
+
7
+ <form style="float:left" action="<%= u "/schedule/requeue_with_params" %>" method="post">
8
+ <table>
9
+ <% @parameters.each do |key, value| %>
10
+ <% value ||= {} %>
11
+ <tr>
12
+ <td>
13
+ <%= key %>
14
+ <% if value['description'] || value[:description] %>
15
+ <span style="border-bottom:1px dotted;" title="<%=value ['description'] || value[:description] %>">(?)</span>
16
+ <% end %>:
17
+ </td>
18
+ <td><input type="text" name="<%= key %>" value="<%= value['default'] || value[:default] %>"></td>
19
+ <% end %>
20
+ </table>
21
+ <input type="hidden" name="job_name" value="<%= @job_name %>">
22
+ <input type="submit" value="Queue now">
23
+ </form>
@@ -3,6 +3,7 @@
3
3
  <p class='intro'>
4
4
  The list below contains all scheduled jobs. Click &quot;Queue now&quot; to queue
5
5
  a job immediately.
6
+ Server local time: <%= Time.now %>
6
7
  </p>
7
8
 
8
9
  <table>
@@ -26,10 +27,10 @@
26
27
  </td>
27
28
  <td><%= h name %></td>
28
29
  <td><%= h config['description'] %></td>
29
- <td style="white-space:nowrap"><%= if !config['every'].nil?
30
- h('every: ' + config['every'])
30
+ <td style="white-space:nowrap"><%= if !config['every'].nil?
31
+ h('every: ' + (config['every'].is_a?(Array) ? config['every'].join(", ") : config['every'].to_s ))
31
32
  elsif !config['cron'].nil?
32
- h('cron: ' + config['cron'])
33
+ h('cron: ' + config['cron'].to_s)
33
34
  else
34
35
  'Not currently scheduled'
35
36
  end %></td>
@@ -1,6 +1,5 @@
1
1
  require 'resque_scheduler'
2
2
  require 'resque/server'
3
-
4
3
  # Extend Resque::Server to add tabs
5
4
  module ResqueScheduler
6
5
 
@@ -16,7 +15,7 @@ module ResqueScheduler
16
15
  end
17
16
 
18
17
  def queue_from_class_name(class_name)
19
- Resque.queue_from_class(Resque.constantize(class_name))
18
+ Resque.queue_from_class(ResqueScheduler::Util.constantize(class_name))
20
19
  end
21
20
  end
22
21
 
@@ -27,7 +26,30 @@ module ResqueScheduler
27
26
  end
28
27
 
29
28
  post "/schedule/requeue" do
30
- config = Resque.schedule[params['job_name']]
29
+ @job_name = params['job_name'] || params[:job_name]
30
+ config = Resque.schedule[@job_name]
31
+ @parameters = config['parameters'] || config[:parameters]
32
+ if @parameters
33
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/requeue-params.erb'))
34
+ else
35
+ Resque::Scheduler.enqueue_from_config(config)
36
+ redirect u("/overview")
37
+ end
38
+ end
39
+
40
+ post "/schedule/requeue_with_params" do
41
+ job_name = params['job_name'] || params[:job_name]
42
+ config = Resque.schedule[job_name]
43
+ # Build args hash from post data (removing the job name)
44
+ submitted_args = params.reject {|key, value| key == 'job_name' || key == :job_name}
45
+
46
+ # Merge constructed args hash with existing args hash for
47
+ # the job, if it exists
48
+ config_args = config['args'] || config[:args] || {}
49
+ config_args = config_args.merge(submitted_args)
50
+
51
+ # Insert the args hash into config and queue the resque job
52
+ config = config.merge('args' => config_args)
31
53
  Resque::Scheduler.enqueue_from_config(config)
32
54
  redirect u("/overview")
33
55
  end
@@ -9,17 +9,23 @@ namespace :resque do
9
9
  require 'resque'
10
10
  require 'resque_scheduler'
11
11
 
12
+ # Need to set this here for conditional Process.daemon redirect of stderr/stdout to /dev/null
13
+ Resque::Scheduler.mute = true if ENV['MUTE']
14
+
12
15
  if ENV['BACKGROUND']
13
16
  unless Process.respond_to?('daemon')
14
17
  abort "env var BACKGROUND is set, which requires ruby >= 1.9"
15
18
  end
16
- Process.daemon(true)
19
+ Process.daemon(true, !Resque::Scheduler.mute)
20
+ Resque.redis.client.reconnect
17
21
  end
18
22
 
19
23
  File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
20
24
 
21
- Resque::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
22
- Resque::Scheduler.verbose = true if ENV['VERBOSE']
25
+ Resque::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
26
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
27
+ Resque::Scheduler.logfile = ENV['LOGFILE'] if ENV['LOGFILE']
28
+ Resque::Scheduler.poll_sleep_amount = Float(ENV['RESQUE_SCHEDULER_INTERVAL']) if ENV['RESQUE_SCHEDULER_INTERVAL']
23
29
  Resque::Scheduler.run
24
30
  end
25
31
 
@@ -0,0 +1,34 @@
1
+ module ResqueScheduler
2
+ class Util
3
+ # In order to upgrade to resque(1.25) which has deprecated following methods ,
4
+ # we just added these usefull helpers back to use in Resque Scheduler.
5
+ # refer to: https://github.com/resque/resque-scheduler/pull/273
6
+
7
+ def self.constantize(camel_cased_word)
8
+ camel_cased_word = camel_cased_word.to_s
9
+
10
+ if camel_cased_word.include?('-')
11
+ camel_cased_word = classify(camel_cased_word)
12
+ end
13
+
14
+ names = camel_cased_word.split('::')
15
+ names.shift if names.empty? || names.first.empty?
16
+
17
+ constant = Object
18
+ names.each do |name|
19
+ args = Module.method(:const_get).arity != 1 ? [false] : []
20
+
21
+ if constant.const_defined?(name, *args)
22
+ constant = constant.const_get(name)
23
+ else
24
+ constant = constant.const_missing(name)
25
+ end
26
+ end
27
+ constant
28
+ end
29
+
30
+ def self.classify(dashed_word)
31
+ dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,5 @@
1
+ # vim:fileencoding=utf-8
2
+
1
3
  module ResqueScheduler
2
- VERSION = '2.0.0'
4
+ VERSION = '2.3.1'
3
5
  end