resque-scheduler 2.3.1 → 2.4.0

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.

Files changed (49) hide show
  1. data/.gitignore +3 -0
  2. data/.rubocop.yml +11 -11
  3. data/.simplecov +1 -0
  4. data/.travis.yml +5 -2
  5. data/AUTHORS.md +3 -0
  6. data/HISTORY.md +26 -2
  7. data/LICENSE +1 -1
  8. data/README.md +120 -31
  9. data/ROADMAP.md +10 -0
  10. data/Rakefile +7 -19
  11. data/bin/resque-scheduler +5 -0
  12. data/examples/Rakefile +2 -0
  13. data/examples/config/initializers/resque-web.rb +37 -0
  14. data/examples/dynamic-scheduling/README.md +28 -0
  15. data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +54 -0
  16. data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +9 -0
  17. data/examples/dynamic-scheduling/app/models/user.rb +16 -0
  18. data/examples/dynamic-scheduling/config/resque.yml +4 -0
  19. data/examples/dynamic-scheduling/config/static_schedule.yml +7 -0
  20. data/examples/dynamic-scheduling/lib/tasks/resque.rake +48 -0
  21. data/examples/run-resque-web +3 -0
  22. data/lib/resque-scheduler.rb +2 -0
  23. data/lib/resque/scheduler.rb +130 -41
  24. data/lib/resque/scheduler/lock/resilient.rb +1 -1
  25. data/lib/resque/scheduler_locking.rb +3 -1
  26. data/lib/resque_scheduler.rb +73 -31
  27. data/lib/resque_scheduler/cli.rb +160 -0
  28. data/lib/resque_scheduler/logger_builder.rb +27 -8
  29. data/lib/resque_scheduler/plugin.rb +10 -7
  30. data/lib/resque_scheduler/server.rb +52 -11
  31. data/lib/resque_scheduler/server/views/delayed.erb +2 -0
  32. data/lib/resque_scheduler/server/views/delayed_schedules.erb +20 -0
  33. data/lib/resque_scheduler/server/views/scheduler.erb +4 -12
  34. data/lib/resque_scheduler/tasks.rb +15 -27
  35. data/lib/resque_scheduler/version.rb +1 -1
  36. data/resque-scheduler.gemspec +2 -0
  37. data/test/cli_test.rb +286 -0
  38. data/test/delayed_queue_test.rb +70 -1
  39. data/test/resque-web_test.rb +36 -1
  40. data/test/scheduler_args_test.rb +51 -17
  41. data/test/scheduler_hooks_test.rb +1 -1
  42. data/test/scheduler_locking_test.rb +63 -1
  43. data/test/scheduler_setup_test.rb +54 -18
  44. data/test/scheduler_task_test.rb +35 -0
  45. data/test/scheduler_test.rb +130 -42
  46. data/test/support/redis_instance.rb +8 -3
  47. data/test/test_helper.rb +47 -20
  48. metadata +77 -6
  49. checksums.yaml +0 -15
@@ -3,7 +3,7 @@ require 'resque/scheduler/lock/base'
3
3
  module Resque
4
4
  class Scheduler
5
5
  module Lock
6
- class Resilient < Base
6
+ class Resilient < Base # rubocop:disable TrailingComma
7
7
  def acquire!
8
8
  Resque.redis.evalsha(
9
9
  acquire_sha,
@@ -79,7 +79,9 @@ module Resque
79
79
  end
80
80
 
81
81
  def master_lock_key
82
- "#{ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''}resque_scheduler_master_lock".to_sym
82
+ lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
83
+ lock_prefix += ':' if lock_prefix != ''
84
+ "#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
83
85
  end
84
86
 
85
87
  def redis_master_version
@@ -6,6 +6,8 @@ require 'resque/scheduler'
6
6
  require 'resque_scheduler/plugin'
7
7
 
8
8
  module ResqueScheduler
9
+ autoload :Cli, 'resque_scheduler/cli'
10
+
9
11
  #
10
12
  # Accepts a new schedule configuration of the form:
11
13
  #
@@ -91,7 +93,7 @@ module ResqueScheduler
91
93
  def clean_schedules
92
94
  if redis.exists(:schedules)
93
95
  redis.hkeys(:schedules).each do |key|
94
- remove_schedule(key)
96
+ remove_schedule(key) if !schedule_persisted?(key)
95
97
  end
96
98
  end
97
99
  @schedule = nil
@@ -109,9 +111,15 @@ module ResqueScheduler
109
111
  # :args => '/tmp/poop'})
110
112
  def set_schedule(name, config)
111
113
  existing_config = get_schedule(name)
114
+ persist = config.delete(:persist) || config.delete('persist')
112
115
  unless existing_config && existing_config == config
113
- redis.hset(:schedules, name, encode(config))
114
- redis.sadd(:schedules_changed, name)
116
+ redis.pipelined do
117
+ redis.hset(:schedules, name, encode(config))
118
+ redis.sadd(:schedules_changed, name)
119
+ if persist
120
+ redis.sadd(:persisted_schedules, name)
121
+ end
122
+ end
115
123
  end
116
124
  config
117
125
  end
@@ -121,10 +129,17 @@ module ResqueScheduler
121
129
  decode(redis.hget(:schedules, name))
122
130
  end
123
131
 
132
+ def schedule_persisted?(name)
133
+ redis.sismember(:persisted_schedules, name)
134
+ end
135
+
124
136
  # remove a given schedule by name
125
137
  def remove_schedule(name)
126
- redis.hdel(:schedules, name)
127
- redis.sadd(:schedules_changed, name)
138
+ redis.pipelined do
139
+ redis.hdel(:schedules, name)
140
+ redis.srem(:persisted_schedules, name)
141
+ redis.sadd(:schedules_changed, name)
142
+ end
128
143
  end
129
144
 
130
145
  # This method is nearly identical to +enqueue+ only it also
@@ -272,6 +287,33 @@ module ResqueScheduler
272
287
  remove_delayed(klass, *args).times { Resque::Scheduler.enqueue_from_config(hash) }
273
288
  end
274
289
 
290
+ # Given a block, remove jobs that return true from a block
291
+ #
292
+ # This allows for removal of delayed jobs that have arguments matching certain criteria
293
+ def remove_delayed_selection
294
+ fail ArgumentError, "Please supply a block" unless block_given?
295
+
296
+ destroyed = 0
297
+ # There is no way to search Redis list entries for a partial match, so we query for all
298
+ # delayed job tasks and do our matching after decoding the payload data
299
+ jobs = Resque.redis.keys("delayed:*")
300
+ jobs.each do |job|
301
+ index = Resque.redis.llen(job) - 1
302
+ while index >= 0
303
+ payload = Resque.redis.lindex(job, index)
304
+ decoded_payload = decode(payload)
305
+ if yield(decoded_payload['args'])
306
+ removed = redis.lrem job, 0, payload
307
+ destroyed += removed
308
+ index -= removed
309
+ else
310
+ index -= 1
311
+ end
312
+ end
313
+ end
314
+ destroyed
315
+ end
316
+
275
317
  # Given a timestamp and job (klass + args) it removes all instances and
276
318
  # returns the count of jobs removed.
277
319
  #
@@ -306,39 +348,39 @@ module ResqueScheduler
306
348
 
307
349
  private
308
350
 
309
- def job_to_hash(klass, args)
310
- {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
311
- end
351
+ def job_to_hash(klass, args)
352
+ {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
353
+ end
312
354
 
313
- def job_to_hash_with_queue(queue, klass, args)
314
- {:class => klass.to_s, :args => args, :queue => queue}
315
- end
355
+ def job_to_hash_with_queue(queue, klass, args)
356
+ {:class => klass.to_s, :args => args, :queue => queue}
357
+ end
316
358
 
317
- def clean_up_timestamp(key, timestamp)
318
- # If the list is empty, remove it.
359
+ def clean_up_timestamp(key, timestamp)
360
+ # If the list is empty, remove it.
319
361
 
320
- # Use a watch here to ensure nobody adds jobs to this delayed
321
- # queue while we're removing it.
322
- redis.watch key
323
- if 0 == redis.llen(key).to_i
324
- redis.multi do
325
- redis.del key
326
- redis.zrem :delayed_queue_schedule, timestamp.to_i
327
- end
328
- else
329
- redis.unwatch
362
+ # Use a watch here to ensure nobody adds jobs to this delayed
363
+ # queue while we're removing it.
364
+ redis.watch key
365
+ if 0 == redis.llen(key).to_i
366
+ redis.multi do
367
+ redis.del key
368
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
330
369
  end
370
+ else
371
+ redis.unwatch
331
372
  end
373
+ end
332
374
 
333
- def prepare_schedule(schedule_hash)
334
- prepared_hash = {}
335
- schedule_hash.each do |name, job_spec|
336
- job_spec = job_spec.dup
337
- job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
338
- prepared_hash[name] = job_spec
339
- end
340
- prepared_hash
375
+ def prepare_schedule(schedule_hash)
376
+ prepared_hash = {}
377
+ schedule_hash.each do |name, job_spec|
378
+ job_spec = job_spec.dup
379
+ job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
380
+ prepared_hash[name] = job_spec
341
381
  end
382
+ prepared_hash
383
+ end
342
384
  end
343
385
 
344
386
  Resque.extend ResqueScheduler
@@ -0,0 +1,160 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'optparse'
4
+
5
+ module ResqueScheduler
6
+ class Cli
7
+ BANNER = <<-EOF.gsub(/ {6}/, '')
8
+ Usage: resque-scheduler [options]
9
+
10
+ Runs a resque scheduler process directly (rather than via rake).
11
+
12
+ EOF
13
+ OPTIONS = [
14
+ {
15
+ args: ['-n', '--app-name [APP_NAME]', 'Application name for procline'],
16
+ callback: ->(options) { ->(n) { options[:app_name] = n } }
17
+ },
18
+ {
19
+ args: ['-B', '--background', 'Run in the background [BACKGROUND]'],
20
+ callback: ->(options) { ->(b) { options[:background] = b } }
21
+ },
22
+ {
23
+ args: ['-D', '--dynamic-schedule',
24
+ 'Enable dynamic scheduling [DYNAMIC_SCHEDULE]'],
25
+ callback: ->(options) { ->(d) { options[:dynamic] = d } }
26
+ },
27
+ {
28
+ args: ['-E', '--environment [RAILS_ENV]', 'Environment name'],
29
+ callback: ->(options) { ->(e) { options[:env] = e } }
30
+ },
31
+ {
32
+ args: ['-I', '--initializer-path [INITIALIZER_PATH]',
33
+ 'Path to optional initializer ruby file'],
34
+ callback: ->(options) { ->(i) { options[:initializer_path] = i } }
35
+ },
36
+ {
37
+ args: ['-i', '--interval [RESQUE_SCHEDULER_INTERVAL]',
38
+ 'Interval for checking if a scheduled job must run'],
39
+ callback: ->(options) { ->(i) { options[:poll_sleep_amount] = i } }
40
+ },
41
+ {
42
+ args: ['-l', '--logfile [LOGFILE]', 'Log file name'],
43
+ callback: ->(options) { ->(l) { options[:logfile] = l } }
44
+ },
45
+ {
46
+ args: ['-F', '--logformat [LOGFORMAT]', 'Log output format'],
47
+ callback: ->(options) { ->(f) { options[:logformat] = f } }
48
+ },
49
+ {
50
+ args: ['-P', '--pidfile [PIDFILE]', 'PID file name'],
51
+ callback: ->(options) { ->(p) { options[:pidfile] = p } }
52
+ },
53
+ {
54
+ args: ['-q', '--quiet', 'Run with minimal output [QUIET] (or [MUTE])'],
55
+ callback: ->(options) { ->(q) { options[:mute] = q } }
56
+ },
57
+ {
58
+ args: ['-v', '--verbose', 'Run with verbose output [VERBOSE]'],
59
+ callback: ->(options) { ->(v) { options[:verbose] = v } }
60
+ }
61
+ ].freeze
62
+
63
+ def self.run!(argv = ARGV, env = ENV)
64
+ new(argv, env).run!
65
+ end
66
+
67
+ def initialize(argv = ARGV, env = ENV)
68
+ @argv = argv
69
+ @env = env
70
+ end
71
+
72
+ def run!
73
+ pre_run
74
+ run_forever
75
+ end
76
+
77
+ def pre_run
78
+ parse_options
79
+ pre_setup
80
+ setup_env
81
+ end
82
+
83
+ def parse_options
84
+ OptionParser.new do |opts|
85
+ opts.banner = BANNER
86
+ OPTIONS.each do |opt|
87
+ opts.on(*opt[:args], &(opt[:callback].call(options)))
88
+ end
89
+ end.parse!(argv.dup)
90
+ end
91
+
92
+ def pre_setup
93
+ if options[:initializer_path]
94
+ load options[:initializer_path].to_s.strip
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ def setup_env
101
+ require 'resque'
102
+ require 'resque/scheduler'
103
+
104
+ # Need to set this here for conditional Process.daemon redirect of
105
+ # stderr/stdout to /dev/null
106
+ Resque::Scheduler.mute = !!options[:mute]
107
+
108
+ if options[:background]
109
+ unless Process.respond_to?('daemon')
110
+ abort 'background option is set, which requires ruby >= 1.9'
111
+ end
112
+
113
+ Process.daemon(true, !Resque::Scheduler.mute)
114
+ Resque.redis.client.reconnect
115
+ end
116
+
117
+ File.open(options[:pidfile], 'w') do |f|
118
+ f.puts $PROCESS_ID
119
+ end if options[:pidfile]
120
+
121
+ Resque::Scheduler.configure do |c|
122
+ # These settings are somewhat redundant given the defaults present
123
+ # in the attr reader methods. They are left here for clarity and
124
+ # to serve as an example of how to use `.configure`.
125
+
126
+ c.app_name = options[:app_name]
127
+ c.dynamic = !!options[:dynamic]
128
+ c.env = options[:env]
129
+ c.logfile = options[:logfile]
130
+ c.logformat = options[:logformat]
131
+ c.poll_sleep_amount = Float(options[:poll_sleep_amount] || '5')
132
+ c.verbose = !!options[:verbose]
133
+ end
134
+ end
135
+
136
+ def run_forever
137
+ Resque::Scheduler.run
138
+ end
139
+
140
+ private
141
+
142
+ attr_reader :argv, :env
143
+
144
+ def options
145
+ @options ||= {
146
+ app_name: env['APP_NAME'],
147
+ background: env['BACKGROUND'],
148
+ dynamic: env['DYNAMIC_SCHEDULE'],
149
+ env: env['RAILS_ENV'],
150
+ initializer_path: env['INITIALIZER_PATH'],
151
+ logfile: env['LOGFILE'],
152
+ logformat: env['LOGFORMAT'],
153
+ mute: env['MUTE'] || env['QUIET'],
154
+ pidfile: env['PIDFILE'],
155
+ poll_sleep_amount: env['RESQUE_SCHEDULER_INTERVAL'],
156
+ verbose: env['VERBOSE']
157
+ }
158
+ end
159
+ end
160
+ end
@@ -1,3 +1,5 @@
1
+ # vim:fileencoding=utf-8
2
+
1
3
  module ResqueScheduler
2
4
  # Just builds a logger, with specified verbosity and destination.
3
5
  # The simplest example:
@@ -10,23 +12,25 @@ module ResqueScheduler
10
12
  # - :mute if logger needs to be silent for all levels. Default - false
11
13
  # - :verbose if there is a need in debug messages. Default - false
12
14
  # - :log_dev to output logs into a desired file. Default - STDOUT
15
+ # - :format log format, either 'text' or 'json'. Default - 'text'
13
16
  #
14
17
  # Example:
15
18
  #
16
- # LoggerBuilder.new(:mute => false, :verbose => true, :log_dev => 'log/sheduler.log')
19
+ # LoggerBuilder.new(
20
+ # :mute => false, :verbose => true, :log_dev => 'log/scheduler.log'
21
+ # )
17
22
  def initialize(opts={})
18
- @muted = !!opts[:mute]
23
+ @muted = !!opts[:mute]
19
24
  @verbose = !!opts[:verbose]
20
- @log_dev = opts[:log_dev] || STDOUT
25
+ @log_dev = opts[:log_dev] || $stdout
26
+ @format = opts[:format] || 'text'
21
27
  end
22
28
 
23
29
  # Returns an instance of Logger
24
30
  def build
25
31
  logger = Logger.new(@log_dev)
26
32
  logger.level = level
27
- logger.datetime_format = "%Y-%m-%d %H:%M:%S"
28
- logger.formatter = formatter
29
-
33
+ logger.formatter = send(:"#{@format}_formatter")
30
34
  logger
31
35
  end
32
36
 
@@ -42,9 +46,24 @@ module ResqueScheduler
42
46
  end
43
47
  end
44
48
 
45
- def formatter
49
+ def text_formatter
50
+ proc do |severity, datetime, progname, msg|
51
+ "resque-scheduler: [#{severity}] #{datetime.iso8601}: #{msg}\n"
52
+ end
53
+ end
54
+
55
+ def json_formatter
46
56
  proc do |severity, datetime, progname, msg|
47
- "[#{severity}] #{datetime}: #{msg}\n"
57
+ require 'json'
58
+ JSON.dump(
59
+ {
60
+ :name => 'resque-scheduler',
61
+ :progname => progname,
62
+ :level => severity,
63
+ :timestamp => datetime.iso8601,
64
+ :msg => msg
65
+ }
66
+ ) + "\n"
48
67
  end
49
68
  end
50
69
  end
@@ -13,13 +13,16 @@ module ResqueScheduler
13
13
  results.all? { |result| result != false }
14
14
  end
15
15
 
16
- def method_missing(method_name, *args, &block)
17
- if method_name.to_s =~ /^run_(.*)_hooks$/
18
- job = args.shift
19
- run_hooks job, $1, *args
20
- else
21
- super
22
- end
16
+ def run_before_delayed_enqueue_hooks(klass, *args)
17
+ run_hooks(klass, 'before_delayed_enqueue', *args)
18
+ end
19
+
20
+ def run_before_schedule_hooks(klass, *args)
21
+ run_hooks(klass, 'before_schedule', *args)
22
+ end
23
+
24
+ def run_after_schedule_hooks(klass, *args)
25
+ run_hooks(klass, 'after_schedule', *args)
23
26
  end
24
27
  end
25
28
  end
@@ -1,14 +1,12 @@
1
1
  require 'resque_scheduler'
2
2
  require 'resque/server'
3
+ require 'json'
4
+
3
5
  # Extend Resque::Server to add tabs
4
6
  module ResqueScheduler
5
-
6
7
  module Server
7
-
8
8
  def self.included(base)
9
-
10
9
  base.class_eval do
11
-
12
10
  helpers do
13
11
  def format_time(t)
14
12
  t.strftime("%Y-%m-%d %H:%M:%S %z")
@@ -17,6 +15,41 @@ module ResqueScheduler
17
15
  def queue_from_class_name(class_name)
18
16
  Resque.queue_from_class(ResqueScheduler::Util.constantize(class_name))
19
17
  end
18
+
19
+ def schedule_interval(config)
20
+ if config['every']
21
+ schedule_interval_every(config['every'])
22
+ elsif config['cron']
23
+ 'cron: ' + config['cron'].to_s
24
+ else
25
+ 'Not currently scheduled'
26
+ end
27
+ end
28
+
29
+ def schedule_interval_every(every)
30
+ s = 'every: '
31
+ if every.respond_to?(:first)
32
+ s << every.first
33
+ else
34
+ s << every
35
+ end
36
+
37
+ return s unless every.respond_to?(:last) && every.length > 1
38
+
39
+ s << ' ('
40
+ meta = every.last.map do |key, value|
41
+ "#{key.to_s.gsub(/_/, ' ')} #{value}"
42
+ end
43
+ s << meta.join(', ') << ')'
44
+ end
45
+
46
+ def schedule_class(config)
47
+ if config['class'].nil? && !config['custom_job_class'].nil?
48
+ config['custom_job_class']
49
+ else
50
+ config['class']
51
+ end
52
+ end
20
53
  end
21
54
 
22
55
  get "/schedule" do
@@ -59,6 +92,18 @@ module ResqueScheduler
59
92
  erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
60
93
  end
61
94
 
95
+ get "/delayed/jobs/:klass" do
96
+ begin
97
+ klass = ResqueScheduler::Util::constantize(params[:klass])
98
+ @args = JSON.load(URI.decode(params[:args]))
99
+ @timestamps = Resque.scheduled_at(klass, *@args)
100
+ rescue => err
101
+ @timestamps = []
102
+ end
103
+
104
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_schedules.erb'))
105
+ end
106
+
62
107
  get "/delayed/:timestamp" do
63
108
  # Is there a better way to specify alternate template locations with sinatra?
64
109
  erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
@@ -74,18 +119,14 @@ module ResqueScheduler
74
119
  Resque.reset_delayed_queue
75
120
  redirect u('delayed')
76
121
  end
77
-
78
122
  end
79
-
80
123
  end
81
-
82
- Resque::Server.tabs << 'Schedule'
83
- Resque::Server.tabs << 'Delayed'
84
-
85
124
  end
86
-
87
125
  end
88
126
 
127
+ Resque::Server.tabs << 'Schedule'
128
+ Resque::Server.tabs << 'Delayed'
129
+
89
130
  Resque::Server.class_eval do
90
131
  include ResqueScheduler::Server
91
132
  end