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/.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
|