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/.rubocop.yml +120 -0
- data/.travis.yml +10 -0
- data/AUTHORS.md +59 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +3 -7
- data/HISTORY.md +41 -6
- data/LICENSE +3 -1
- data/{README.markdown → README.md} +171 -103
- data/Rakefile +22 -9
- data/lib/resque/scheduler.rb +22 -9
- data/lib/resque_scheduler.rb +49 -13
- data/lib/resque_scheduler/logger_builder.rb +51 -0
- data/lib/resque_scheduler/server/views/delayed.erb +2 -1
- data/lib/resque_scheduler/server/views/scheduler.erb +3 -2
- data/lib/resque_scheduler/tasks.rb +9 -3
- data/lib/resque_scheduler/version.rb +1 -1
- data/resque-scheduler.gemspec +24 -19
- data/script/migrate_to_timestamps_set.rb +14 -0
- data/test/delayed_queue_test.rb +50 -15
- data/test/resque-web_test.rb +3 -3
- data/test/scheduler_args_test.rb +1 -1
- data/test/scheduler_setup_test.rb +59 -0
- data/test/scheduler_test.rb +11 -0
- data/test/test_helper.rb +9 -2
- metadata +102 -9
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
|
-
|
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
|
-
|
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 =
|
23
|
-
rd.rdoc_files.include(
|
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
|
-
|
data/lib/resque/scheduler.rb
CHANGED
@@ -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
|
-
|
320
|
+
logger.info msg
|
307
321
|
end
|
308
322
|
|
309
323
|
def log(msg)
|
310
|
-
|
311
|
-
log!(msg) if verbose
|
324
|
+
logger.debug msg
|
312
325
|
end
|
313
326
|
|
314
327
|
def procline(string)
|
data/lib/resque_scheduler.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
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
|
-
|
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
|
-
|
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 "Queue now" 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
|
22
|
-
Resque::Scheduler.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
|
|
data/resque-scheduler.gemspec
CHANGED
@@ -1,27 +1,32 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
|
-
|
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 |
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|