resque-scheduler 2.0.1 → 2.1.0

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.

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