resque-scheduler 2.0.0 → 2.3.1

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.

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