resque-scheduler 2.0.1 → 2.1.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.

data/Rakefile CHANGED
@@ -1,28 +1,41 @@
1
- require 'bundler'
2
-
3
- Bundler::GemHelper.install_tasks
1
+ require 'bundler/gem_tasks'
4
2
 
5
3
  $LOAD_PATH.unshift 'lib'
6
4
 
7
5
  task :default => :test
8
6
 
9
- # Tests
10
- desc "Run tests"
7
+ desc 'Run tests'
11
8
  task :test do
9
+ if RUBY_VERSION =~ /^1\.8/
10
+ unless ENV['SEED']
11
+ srand
12
+ ENV['SEED'] = (srand % 0xFFFF).to_s
13
+ end
14
+
15
+ $stdout.puts "Running with SEED=#{ENV['SEED']}"
16
+ srand Integer(ENV['SEED'])
17
+ elsif ENV['SEED']
18
+ ARGV += %W(--seed #{ENV['SEED']})
19
+ end
12
20
  Dir['test/*_test.rb'].each do |f|
13
21
  require File.expand_path(f)
14
22
  end
15
23
  end
16
24
 
17
- # Documentation Tasks
25
+ desc 'Run rubocop'
26
+ task :rubocop do
27
+ unless RUBY_VERSION < '1.9'
28
+ sh('rubocop --config .rubocop.yml --format simple') { |r, _| r || abort }
29
+ end
30
+ end
31
+
18
32
  begin
19
33
  require 'rdoc/task'
20
34
 
21
35
  Rake::RDocTask.new do |rd|
22
- rd.main = "README.markdown"
23
- rd.rdoc_files.include("README.markdown", "lib/**/*.rb")
36
+ rd.main = 'README.md'
37
+ rd.rdoc_files.include('README.md', 'lib/**/*.rb')
24
38
  rd.rdoc_dir = 'doc'
25
39
  end
26
40
  rescue LoadError
27
41
  end
28
-
@@ -1,12 +1,11 @@
1
1
  require 'rufus/scheduler'
2
- require 'thwait'
3
2
  require 'resque/scheduler_locking'
3
+ require 'resque_scheduler/logger_builder'
4
4
 
5
5
  module Resque
6
6
 
7
7
  class Scheduler
8
8
 
9
- extend Resque::Helpers
10
9
  extend Resque::SchedulerLocking
11
10
 
12
11
  class << self
@@ -17,6 +16,9 @@ module Resque
17
16
  # If set, produces no output
18
17
  attr_accessor :mute
19
18
 
19
+ # If set, will write messages to the file
20
+ attr_accessor :logfile
21
+
20
22
  # If set, will try to update the schedule in the loop
21
23
  attr_accessor :dynamic
22
24
 
@@ -24,6 +26,8 @@ module Resque
24
26
  # queue. Defaults to 5
25
27
  attr_writer :poll_sleep_amount
26
28
 
29
+ attr_writer :logger
30
+
27
31
  # the Rufus::Scheduler jobs that are scheduled
28
32
  def scheduled_jobs
29
33
  @@scheduled_jobs
@@ -33,12 +37,22 @@ module Resque
33
37
  @poll_sleep_amount ||= 5 # seconds
34
38
  end
35
39
 
40
+ def logger
41
+ @logger ||= ResqueScheduler::LoggerBuilder.new(:mute => mute, :verbose => verbose, :log_dev => logfile).build
42
+ end
43
+
36
44
  # Schedule all jobs and continually look for delayed jobs (never returns)
37
45
  def run
38
46
  $0 = "resque-scheduler: Starting"
39
47
  # trap signals
40
48
  register_signal_handlers
41
49
 
50
+ # Quote from the resque/worker.
51
+ # Fix buffering so we can `rake resque:scheduler > scheduler.log` and
52
+ # get output from the child in there.
53
+ $stdout.sync = true
54
+ $stderr.sync = true
55
+
42
56
  # Load the schedule into rufus
43
57
  # If dynamic is set, load that schedule otherwise use normal load
44
58
  if dynamic
@@ -62,7 +76,7 @@ module Resque
62
76
 
63
77
  # never gets here.
64
78
  end
65
-
79
+
66
80
 
67
81
  # For all signals, set the shutdown flag and wait for current
68
82
  # poll/enqueing to finish (should be almost istant). In the
@@ -205,7 +219,7 @@ module Resque
205
219
  args = job_config['args'] || job_config[:args]
206
220
 
207
221
  klass_name = job_config['class'] || job_config[:class]
208
- klass = constantize(klass_name) rescue klass_name
222
+ klass = Resque.constantize(klass_name) rescue klass_name
209
223
 
210
224
  params = args.is_a?(Hash) ? [args] : Array(args)
211
225
  queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
@@ -215,7 +229,7 @@ module Resque
215
229
  # job class can not be constantized (via a requeue call from the web perhaps), fall
216
230
  # back to enqueing normally via Resque::Job.create.
217
231
  begin
218
- constantize(job_klass).scheduled(queue, klass_name, *params)
232
+ Resque.constantize(job_klass).scheduled(queue, klass_name, *params)
219
233
  rescue NameError
220
234
  # Note that the custom job class (job_config['custom_job_class']) is the one enqueued
221
235
  Resque::Job.create(queue, job_klass, *params)
@@ -297,18 +311,17 @@ module Resque
297
311
  def shutdown
298
312
  @shutdown = true
299
313
  if @sleeping
300
- release_master_lock!
314
+ Thread.new { release_master_lock! }
301
315
  exit
302
316
  end
303
317
  end
304
318
 
305
319
  def log!(msg)
306
- puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
320
+ logger.info msg
307
321
  end
308
322
 
309
323
  def log(msg)
310
- # add "verbose" logic later
311
- log!(msg) if verbose
324
+ logger.debug msg
312
325
  end
313
326
 
314
327
  def procline(string)
@@ -25,6 +25,9 @@ module ResqueScheduler
25
25
  # is used implicitly as "class" argument - in the "MakeTea" example,
26
26
  # "MakeTea" is used both as job name and resque worker class.
27
27
  #
28
+ # Any jobs that were in the old schedule, but are not
29
+ # present in the new schedule, will be removed.
30
+ #
28
31
  # :cron can be any cron scheduling string
29
32
  #
30
33
  # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
@@ -46,9 +49,13 @@ module ResqueScheduler
46
49
  schedule_hash = prepare_schedule(schedule_hash)
47
50
 
48
51
  if Resque::Scheduler.dynamic
52
+ reload_schedule!
49
53
  schedule_hash.each do |name, job_spec|
50
54
  set_schedule(name, job_spec)
51
55
  end
56
+ (schedule.keys - schedule_hash.keys.map(&:to_s)).each do |name|
57
+ remove_schedule(name)
58
+ end
52
59
  end
53
60
  @schedule = schedule_hash
54
61
  end
@@ -121,7 +128,7 @@ module ResqueScheduler
121
128
  def enqueue_at_with_queue(queue, timestamp, klass, *args)
122
129
  return false unless Plugin.run_before_schedule_hooks(klass, *args)
123
130
 
124
- if Resque.inline?
131
+ if Resque.inline? || timestamp.to_i < Time.now.to_i
125
132
  # Just create the job and let resque perform it right away with inline.
126
133
  # If the class is a custom job class, call self#scheduled on it. This allows you to do things like
127
134
  # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1). Otherwise, pass off to Resque.
@@ -158,6 +165,9 @@ module ResqueScheduler
158
165
  # First add this item to the list for this timestamp
159
166
  redis.rpush("delayed:#{timestamp.to_i}", encode(item))
160
167
 
168
+ # Store the timestamps at with this item occurs
169
+ redis.sadd("timestamps:#{encode(item)}", "delayed:#{timestamp.to_i}")
170
+
161
171
  # Now, add this timestamp to the zsets. The score and the value are
162
172
  # the same since we'll be querying by timestamp, and we don't have
163
173
  # anything else to store.
@@ -166,7 +176,7 @@ module ResqueScheduler
166
176
 
167
177
  # Returns an array of timestamps based on start and count
168
178
  def delayed_queue_peek(start, count)
169
- Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect{|x| x.to_i}
179
+ Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect { |x| x.to_i }
170
180
  end
171
181
 
172
182
  # Returns the size of the delayed queue schedule
@@ -203,7 +213,9 @@ module ResqueScheduler
203
213
  def next_item_for_timestamp(timestamp)
204
214
  key = "delayed:#{timestamp.to_i}"
205
215
 
206
- item = decode redis.lpop(key)
216
+ encoded_item = redis.lpop(key)
217
+ redis.srem("timestamps:#{encoded_item}", key)
218
+ item = decode(encoded_item)
207
219
 
208
220
  # If the list is empty, remove it.
209
221
  clean_up_timestamp(key, timestamp)
@@ -213,24 +225,36 @@ module ResqueScheduler
213
225
  # Clears all jobs created with enqueue_at or enqueue_in
214
226
  def reset_delayed_queue
215
227
  Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
216
- redis.del "delayed:#{item}"
228
+ key = "delayed:#{item}"
229
+ items = redis.lrange(key, 0, -1)
230
+ redis.pipelined do
231
+ items.each { |ts_item| redis.del("timestamps:#{ts_item}") }
232
+ end
233
+ redis.del key
217
234
  end
218
235
 
219
236
  redis.del :delayed_queue_schedule
220
237
  end
221
238
 
222
239
  # Given an encoded item, remove it from the delayed_queue
223
- #
224
- # This method is potentially very expensive since it needs to scan
225
- # through the delayed queue for every timestamp, but at least it
226
- # doesn't kill Redis by calling redis.keys.
227
240
  def remove_delayed(klass, *args)
228
- destroyed = 0
229
241
  search = encode(job_to_hash(klass, args))
230
- Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
231
- destroyed += redis.lrem "delayed:#{timestamp}", 0, search
242
+ timestamps = redis.smembers("timestamps:#{search}")
243
+
244
+ replies = redis.pipelined do
245
+ timestamps.each do |key|
246
+ redis.lrem(key, 0, search)
247
+ redis.srem("timestamps:#{search}", key)
248
+ end
232
249
  end
233
- destroyed
250
+
251
+ (replies.nil? || replies.empty?) ? 0 : replies.each_slice(2).collect { |slice| slice.first }.inject(:+)
252
+ end
253
+
254
+ # Given an encoded item, enqueue it now
255
+ def enqueue_delayed(klass, *args)
256
+ hash = job_to_hash(klass, args)
257
+ remove_delayed(klass, *args).times { Resque::Scheduler.enqueue_from_config(hash) }
234
258
  end
235
259
 
236
260
  # Given a timestamp and job (klass + args) it removes all instances and
@@ -240,8 +264,12 @@ module ResqueScheduler
240
264
  # timestamp
241
265
  def remove_delayed_job_from_timestamp(timestamp, klass, *args)
242
266
  key = "delayed:#{timestamp.to_i}"
243
- count = redis.lrem key, 0, encode(job_to_hash(klass, args))
267
+ encoded_job = encode(job_to_hash(klass, args))
268
+
269
+ redis.srem("timestamps:#{encoded_job}", key)
270
+ count = redis.lrem(key, 0, encoded_job)
244
271
  clean_up_timestamp(key, timestamp)
272
+
245
273
  count
246
274
  end
247
275
 
@@ -253,6 +281,14 @@ module ResqueScheduler
253
281
  total_jobs
254
282
  end
255
283
 
284
+ # Returns delayed jobs schedule timestamp for +klass+, +args+.
285
+ def scheduled_at(klass, *args)
286
+ search = encode(job_to_hash(klass, args))
287
+ redis.smembers("timestamps:#{search}").collect do |key|
288
+ key.tr('delayed:', '').to_i
289
+ end
290
+ end
291
+
256
292
  private
257
293
 
258
294
  def job_to_hash(klass, args)
@@ -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 %>
@@ -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>
@@ -27,9 +28,9 @@
27
28
  <td><%= h name %></td>
28
29
  <td><%= h config['description'] %></td>
29
30
  <td style="white-space:nowrap"><%= if !config['every'].nil?
30
- h('every: ' + config['every'])
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>
@@ -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 = Integer(ENV['INTERVAL']) if ENV['INTERVAL']
23
29
  Resque::Scheduler.run
24
30
  end
25
31
 
@@ -1,3 +1,3 @@
1
1
  module ResqueScheduler
2
- VERSION = '2.0.1'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -1,27 +1,32 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.unshift File.expand_path("../lib", __FILE__)
3
- require "resque_scheduler/version"
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque_scheduler/version'
4
5
 
5
- Gem::Specification.new do |s|
6
- s.name = "resque-scheduler"
7
- s.version = ResqueScheduler::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = ['Ben VandenBos']
10
- s.email = ['bvandenbos@gmail.com']
11
- s.homepage = "http://github.com/bvandenbos/resque-scheduler"
12
- s.summary = "Light weight job scheduling on top of Resque"
13
- s.description = %q{Light weight job scheduling on top of Resque.
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'resque-scheduler'
8
+ spec.version = ResqueScheduler::VERSION
9
+ spec.authors = ['Ben VandenBos']
10
+ spec.email = ['bvandenbos@gmail.com']
11
+ spec.homepage = 'http://github.com/resque/resque-scheduler'
12
+ spec.summary = 'Light weight job scheduling on top of Resque'
13
+ spec.description = %q{Light weight job scheduling on top of Resque.
14
14
  Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
15
15
  Also supports queueing jobs on a fixed, cron-like schedule.}
16
16
 
17
- s.required_rubygems_version = ">= 1.3.6"
18
- s.add_development_dependency "bundler", ">= 1.0.0"
17
+ spec.files = `git ls-files`.split("\n")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^test/})
20
+ spec.require_path = ['lib']
19
21
 
20
- s.files = `git ls-files`.split("\n")
21
- s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
22
- s.require_path = 'lib'
22
+ spec.add_development_dependency 'bundler', '~> 1.3'
23
+ spec.add_development_dependency 'mocha'
24
+ spec.add_development_dependency 'rack-test'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'json' if RUBY_VERSION < '1.9'
27
+ spec.add_development_dependency 'rubocop' unless RUBY_VERSION < '1.9'
23
28
 
24
- s.add_runtime_dependency(%q<redis>, [">= 2.0.1"])
25
- s.add_runtime_dependency(%q<resque>, [">= 1.20.0"])
26
- s.add_runtime_dependency(%q<rufus-scheduler>, [">= 0"])
29
+ spec.add_runtime_dependency 'redis', '>= 2.0.1'
30
+ spec.add_runtime_dependency 'resque', ['>= 1.20.0', '< 1.25']
31
+ spec.add_runtime_dependency 'rufus-scheduler', '>= 0'
27
32
  end