nogara-resque-scheduler 2.0.1
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.
- data/.gitignore +6 -0
- data/Gemfile +9 -0
- data/HISTORY.md +135 -0
- data/LICENSE +21 -0
- data/README.markdown +354 -0
- data/Rakefile +28 -0
- data/lib/resque/scheduler.rb +362 -0
- data/lib/resque_scheduler.rb +293 -0
- data/lib/resque_scheduler/plugin.rb +25 -0
- data/lib/resque_scheduler/server.rb +69 -0
- data/lib/resque_scheduler/server/views/delayed.erb +48 -0
- data/lib/resque_scheduler/server/views/delayed_timestamp.erb +26 -0
- data/lib/resque_scheduler/server/views/scheduler.erb +43 -0
- data/lib/resque_scheduler/tasks.rb +34 -0
- data/lib/resque_scheduler/version.rb +3 -0
- data/resque-scheduler.gemspec +27 -0
- data/tasks/resque_scheduler.rake +2 -0
- data/test/delayed_queue_test.rb +329 -0
- data/test/redis-test.conf +115 -0
- data/test/resque-web_test.rb +32 -0
- data/test/scheduler_args_test.rb +156 -0
- data/test/scheduler_hooks_test.rb +23 -0
- data/test/scheduler_test.rb +247 -0
- data/test/test_helper.rb +88 -0
- metadata +135 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'bundler'
|
|
2
|
+
|
|
3
|
+
Bundler::GemHelper.install_tasks
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.unshift 'lib'
|
|
6
|
+
|
|
7
|
+
task :default => :test
|
|
8
|
+
|
|
9
|
+
# Tests
|
|
10
|
+
desc "Run tests"
|
|
11
|
+
task :test do
|
|
12
|
+
Dir['test/*_test.rb'].each do |f|
|
|
13
|
+
require File.expand_path(f)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Documentation Tasks
|
|
18
|
+
begin
|
|
19
|
+
require 'rdoc/task'
|
|
20
|
+
|
|
21
|
+
Rake::RDocTask.new do |rd|
|
|
22
|
+
rd.main = "README.markdown"
|
|
23
|
+
rd.rdoc_files.include("README.markdown", "lib/**/*.rb")
|
|
24
|
+
rd.rdoc_dir = 'doc'
|
|
25
|
+
end
|
|
26
|
+
rescue LoadError
|
|
27
|
+
end
|
|
28
|
+
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
require 'rufus/scheduler'
|
|
2
|
+
require 'thwait'
|
|
3
|
+
|
|
4
|
+
module Resque
|
|
5
|
+
|
|
6
|
+
class Scheduler
|
|
7
|
+
|
|
8
|
+
extend Resque::Helpers
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
|
|
12
|
+
LOCK_TIMEOUT = 60 * 5
|
|
13
|
+
|
|
14
|
+
# If true, logs more stuff...
|
|
15
|
+
attr_accessor :verbose
|
|
16
|
+
|
|
17
|
+
# If set, produces no output
|
|
18
|
+
attr_accessor :mute
|
|
19
|
+
|
|
20
|
+
# If set, will try to update the schulde in the loop
|
|
21
|
+
attr_accessor :dynamic
|
|
22
|
+
|
|
23
|
+
# Amount of time in seconds to sleep between polls of the delayed
|
|
24
|
+
# queue. Defaults to 5
|
|
25
|
+
attr_writer :poll_sleep_amount
|
|
26
|
+
|
|
27
|
+
# the Rufus::Scheduler jobs that are scheduled
|
|
28
|
+
def scheduled_jobs
|
|
29
|
+
@@scheduled_jobs
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def poll_sleep_amount
|
|
33
|
+
@poll_sleep_amount ||= 5 # seconds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Schedule all jobs and continually look for delayed jobs (never returns)
|
|
37
|
+
def run
|
|
38
|
+
$0 = "resque-scheduler: Starting"
|
|
39
|
+
# trap signals
|
|
40
|
+
register_signal_handlers
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
got_lock = can_lock_scheduler?
|
|
44
|
+
if got_lock == true
|
|
45
|
+
|
|
46
|
+
# Load the schedule into rufus
|
|
47
|
+
# If dynamic is set, load that schedule otherwise use normal load
|
|
48
|
+
if dynamic
|
|
49
|
+
reload_schedule!
|
|
50
|
+
else
|
|
51
|
+
load_schedule!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
first_time = false
|
|
55
|
+
|
|
56
|
+
# Now start the scheduling part of the loop.
|
|
57
|
+
|
|
58
|
+
30.times do #30 * 5 seconds, it should be less than the timeout defined above
|
|
59
|
+
# loop do
|
|
60
|
+
begin
|
|
61
|
+
handle_delayed_items
|
|
62
|
+
update_schedule if dynamic
|
|
63
|
+
rescue Errno::EAGAIN, Errno::ECONNRESET => e
|
|
64
|
+
warn e.message
|
|
65
|
+
end
|
|
66
|
+
poll_sleep
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unlock_scheduler
|
|
70
|
+
clear_schedule!
|
|
71
|
+
|
|
72
|
+
else
|
|
73
|
+
puts "Scheduler locked!!!"
|
|
74
|
+
sleep 5
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
# never gets here.
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# For all signals, set the shutdown flag and wait for current
|
|
81
|
+
# poll/enqueing to finish (should be almost istant). In the
|
|
82
|
+
# case of sleeping, exit immediately.
|
|
83
|
+
def register_signal_handlers
|
|
84
|
+
trap("TERM") { shutdown }
|
|
85
|
+
trap("INT") { shutdown }
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
trap('QUIT') { shutdown }
|
|
89
|
+
trap('USR1') { print_schedule }
|
|
90
|
+
trap('USR2') { reload_schedule! }
|
|
91
|
+
rescue ArgumentError
|
|
92
|
+
warn "Signals QUIT and USR1 and USR2 not supported."
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_schedule
|
|
97
|
+
if rufus_scheduler
|
|
98
|
+
log! "Scheduling Info\tLast Run"
|
|
99
|
+
scheduler_jobs = rufus_scheduler.all_jobs
|
|
100
|
+
scheduler_jobs.each do |k, v|
|
|
101
|
+
log! "#{v.t}\t#{v.last}\t"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Pulls the schedule from Resque.schedule and loads it into the
|
|
107
|
+
# rufus scheduler instance
|
|
108
|
+
def load_schedule!
|
|
109
|
+
procline "Loading Schedule"
|
|
110
|
+
|
|
111
|
+
# Need to load the schedule from redis for the first time if dynamic
|
|
112
|
+
Resque.reload_schedule! if dynamic
|
|
113
|
+
|
|
114
|
+
log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
|
|
115
|
+
|
|
116
|
+
@@scheduled_jobs = {}
|
|
117
|
+
|
|
118
|
+
Resque.schedule.each do |name, config|
|
|
119
|
+
load_schedule_job(name, config)
|
|
120
|
+
end
|
|
121
|
+
Resque.redis.del(:schedules_changed)
|
|
122
|
+
procline "Schedules Loaded"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# modify interval type value to value with options if options available
|
|
126
|
+
def optionizate_interval_value(value)
|
|
127
|
+
args = value
|
|
128
|
+
if args.is_a?(::Array)
|
|
129
|
+
return args.first if args.size > 2 || !args.last.is_a?(::Hash)
|
|
130
|
+
# symbolize keys of hash for options
|
|
131
|
+
args[1] = args[1].inject({}) do |m, i|
|
|
132
|
+
key, value = i
|
|
133
|
+
m[(key.to_sym rescue key) || key] = value
|
|
134
|
+
m
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
args
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
|
|
141
|
+
def load_schedule_job(name, config)
|
|
142
|
+
# If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
|
|
143
|
+
# required for the jobs to be scheduled. If rails_env is missing, the
|
|
144
|
+
# job should be scheduled regardless of what ENV['RAILS_ENV'] is set
|
|
145
|
+
# to.
|
|
146
|
+
if config['rails_env'].nil? || rails_env_matches?(config)
|
|
147
|
+
log! "Scheduling #{name} "
|
|
148
|
+
interval_defined = false
|
|
149
|
+
interval_types = %w{cron every}
|
|
150
|
+
interval_types.each do |interval_type|
|
|
151
|
+
if !config[interval_type].nil? && config[interval_type].length > 0
|
|
152
|
+
args = optionizate_interval_value(config[interval_type])
|
|
153
|
+
@@scheduled_jobs[name] = rufus_scheduler.send(interval_type, *args) do
|
|
154
|
+
log! "queueing #{config['class']} (#{name})"
|
|
155
|
+
handle_errors { enqueue_from_config(config) }
|
|
156
|
+
end
|
|
157
|
+
interval_defined = true
|
|
158
|
+
break
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
unless interval_defined
|
|
162
|
+
log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns true if the given schedule config hash matches the current
|
|
168
|
+
# ENV['RAILS_ENV']
|
|
169
|
+
def rails_env_matches?(config)
|
|
170
|
+
config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Handles queueing delayed items
|
|
174
|
+
# at_time - Time to start scheduling items (default: now).
|
|
175
|
+
def handle_delayed_items(at_time=nil)
|
|
176
|
+
if timestamp = Resque.next_delayed_timestamp(at_time)
|
|
177
|
+
procline "Processing Delayed Items"
|
|
178
|
+
while !timestamp.nil?
|
|
179
|
+
enqueue_delayed_items_for_timestamp(timestamp)
|
|
180
|
+
timestamp = Resque.next_delayed_timestamp(at_time)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Enqueues all delayed jobs for a timestamp
|
|
186
|
+
def enqueue_delayed_items_for_timestamp(timestamp)
|
|
187
|
+
item = nil
|
|
188
|
+
begin
|
|
189
|
+
handle_shutdown do
|
|
190
|
+
if item = Resque.next_item_for_timestamp(timestamp)
|
|
191
|
+
log "queuing #{item['class']} [delayed]"
|
|
192
|
+
handle_errors { enqueue_from_config(item) }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
# continue processing until there are no more ready items in this timestamp
|
|
196
|
+
end while !item.nil?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def handle_shutdown
|
|
200
|
+
begin
|
|
201
|
+
unlock_scheduler if @shutdown
|
|
202
|
+
rescue
|
|
203
|
+
end
|
|
204
|
+
exit if @shutdown
|
|
205
|
+
yield
|
|
206
|
+
begin
|
|
207
|
+
unlock_scheduler if @shutdown
|
|
208
|
+
rescue
|
|
209
|
+
end
|
|
210
|
+
exit if @shutdown
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def handle_errors
|
|
214
|
+
begin
|
|
215
|
+
yield
|
|
216
|
+
rescue Exception => e
|
|
217
|
+
log! "#{e.class.name}: #{e.message}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Enqueues a job based on a config hash
|
|
222
|
+
def enqueue_from_config(job_config)
|
|
223
|
+
args = job_config['args'] || job_config[:args]
|
|
224
|
+
|
|
225
|
+
klass_name = job_config['class'] || job_config[:class]
|
|
226
|
+
klass = constantize(klass_name) rescue klass_name
|
|
227
|
+
|
|
228
|
+
params = args.is_a?(Hash) ? [args] : Array(args)
|
|
229
|
+
queue = job_config['queue'] || job_config[:queue] || Resque.queue_from_class(klass)
|
|
230
|
+
# Support custom job classes like those that inherit from Resque::JobWithStatus (resque-status)
|
|
231
|
+
if (job_klass = job_config['custom_job_class']) && (job_klass != 'Resque::Job')
|
|
232
|
+
# The custom job class API must offer a static "scheduled" method. If the custom
|
|
233
|
+
# job class can not be constantized (via a requeue call from the web perhaps), fall
|
|
234
|
+
# back to enqueing normally via Resque::Job.create.
|
|
235
|
+
begin
|
|
236
|
+
constantize(job_klass).scheduled(queue, klass_name, *params)
|
|
237
|
+
rescue NameError
|
|
238
|
+
# Note that the custom job class (job_config['custom_job_class']) is the one enqueued
|
|
239
|
+
Resque::Job.create(queue, job_klass, *params)
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
# hack to avoid havoc for people shoving stuff into queues
|
|
243
|
+
# for non-existent classes (for example: running scheduler in
|
|
244
|
+
# one app that schedules for another
|
|
245
|
+
if Class === klass
|
|
246
|
+
ResqueScheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
|
|
247
|
+
Resque.enqueue_to(queue, klass, *params)
|
|
248
|
+
else
|
|
249
|
+
# This will not run the before_hooks in rescue, but will at least
|
|
250
|
+
# queue the job.
|
|
251
|
+
Resque::Job.create(queue, klass, *params)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def rufus_scheduler
|
|
257
|
+
@rufus_scheduler ||= Rufus::Scheduler.start_new
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Stops old rufus scheduler and creates a new one. Returns the new
|
|
261
|
+
# rufus scheduler
|
|
262
|
+
def clear_schedule!
|
|
263
|
+
rufus_scheduler.stop
|
|
264
|
+
@rufus_scheduler = nil
|
|
265
|
+
@@scheduled_jobs = {}
|
|
266
|
+
rufus_scheduler
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def reload_schedule!
|
|
270
|
+
procline "Reloading Schedule"
|
|
271
|
+
clear_schedule!
|
|
272
|
+
load_schedule!
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def update_schedule
|
|
276
|
+
if Resque.redis.scard(:schedules_changed) > 0
|
|
277
|
+
procline "Updating schedule"
|
|
278
|
+
Resque.reload_schedule!
|
|
279
|
+
while schedule_name = Resque.redis.spop(:schedules_changed)
|
|
280
|
+
if Resque.schedule.keys.include?(schedule_name)
|
|
281
|
+
unschedule_job(schedule_name)
|
|
282
|
+
load_schedule_job(schedule_name, Resque.schedule[schedule_name])
|
|
283
|
+
else
|
|
284
|
+
unschedule_job(schedule_name)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
procline "Schedules Loaded"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def unschedule_job(name)
|
|
292
|
+
if scheduled_jobs[name]
|
|
293
|
+
log "Removing schedule #{name}"
|
|
294
|
+
scheduled_jobs[name].unschedule
|
|
295
|
+
@@scheduled_jobs.delete(name)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Sleeps and returns true
|
|
300
|
+
def poll_sleep
|
|
301
|
+
@sleeping = true
|
|
302
|
+
handle_shutdown { sleep poll_sleep_amount }
|
|
303
|
+
@sleeping = false
|
|
304
|
+
true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Sets the shutdown flag, exits if sleeping
|
|
308
|
+
def shutdown
|
|
309
|
+
@shutdown = true
|
|
310
|
+
exit if @sleeping
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def log!(msg)
|
|
314
|
+
puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def log(msg)
|
|
318
|
+
# add "verbose" logic later
|
|
319
|
+
log!(msg) if verbose
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def procline(string)
|
|
323
|
+
log! string
|
|
324
|
+
$0 = "resque-scheduler-#{ResqueScheduler::VERSION}: #{string}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def lock_timeout
|
|
328
|
+
Time.now.utc.to_i + LOCK_TIMEOUT + 1
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def can_lock_scheduler?
|
|
332
|
+
#using logic from http://redis.io/commands/getset
|
|
333
|
+
got_lock = Resque.redis.setnx('scheduler:lock', lock_timeout)
|
|
334
|
+
puts "First get lock #{got_lock}"
|
|
335
|
+
unless got_lock
|
|
336
|
+
timestamp = Resque.redis.get('scheduler:lock').to_i
|
|
337
|
+
puts "Timestamp: #{timestamp}"
|
|
338
|
+
timestamp_now = Time.now.utc.to_i
|
|
339
|
+
puts "Timestamp Now: #{timestamp_now}"
|
|
340
|
+
if timestamp_now > timestamp
|
|
341
|
+
timestamp_old = Resque.redis.getset('scheduler:lock', lock_timeout).to_i
|
|
342
|
+
puts "Timestamp Old: #{timestamp_old}"
|
|
343
|
+
if timestamp_old < timestamp_now
|
|
344
|
+
puts "Got lock here"
|
|
345
|
+
got_lock = true
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
puts "Second get lock #{got_lock}"
|
|
350
|
+
got_lock
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def unlock_scheduler
|
|
354
|
+
puts "Unlocking scheduler lock"
|
|
355
|
+
Resque.redis.del('scheduler:lock')
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'resque'
|
|
3
|
+
require 'resque_scheduler/version'
|
|
4
|
+
require 'resque/scheduler'
|
|
5
|
+
require 'resque_scheduler/plugin'
|
|
6
|
+
|
|
7
|
+
module ResqueScheduler
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Accepts a new schedule configuration of the form:
|
|
11
|
+
#
|
|
12
|
+
# {
|
|
13
|
+
# "MakeTea" => {
|
|
14
|
+
# "every" => "1m" },
|
|
15
|
+
# "some_name" => {
|
|
16
|
+
# "cron" => "5/* * * *",
|
|
17
|
+
# "class" => "DoSomeWork",
|
|
18
|
+
# "args" => "work on this string",
|
|
19
|
+
# "description" => "this thing works it"s butter off" },
|
|
20
|
+
# ...
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# Hash keys can be anything and are used to describe and reference
|
|
24
|
+
# the scheduled job. If the "class" argument is missing, the key
|
|
25
|
+
# is used implicitly as "class" argument - in the "MakeTea" example,
|
|
26
|
+
# "MakeTea" is used both as job name and resque worker class.
|
|
27
|
+
#
|
|
28
|
+
# :cron can be any cron scheduling string
|
|
29
|
+
#
|
|
30
|
+
# :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
|
|
31
|
+
# for valid syntax. If :cron is present it will take precedence over :every.
|
|
32
|
+
#
|
|
33
|
+
# :class must be a resque worker class. If it is missing, the job name (hash key)
|
|
34
|
+
# will be used as :class.
|
|
35
|
+
#
|
|
36
|
+
# :args can be any yaml which will be converted to a ruby literal and
|
|
37
|
+
# passed in a params. (optional)
|
|
38
|
+
#
|
|
39
|
+
# :rails_envs is the list of envs where the job gets loaded. Envs are
|
|
40
|
+
# comma separated (optional)
|
|
41
|
+
#
|
|
42
|
+
# :description is just that, a description of the job (optional). If
|
|
43
|
+
# params is an array, each element in the array is passed as a separate
|
|
44
|
+
# param, otherwise params is passed in as the only parameter to perform.
|
|
45
|
+
def schedule=(schedule_hash)
|
|
46
|
+
schedule_hash = prepare_schedule(schedule_hash)
|
|
47
|
+
|
|
48
|
+
if Resque::Scheduler.dynamic
|
|
49
|
+
schedule_hash.each do |name, job_spec|
|
|
50
|
+
set_schedule(name, job_spec)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
@schedule = schedule_hash
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the schedule hash
|
|
57
|
+
def schedule
|
|
58
|
+
@schedule ||= {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# reloads the schedule from redis
|
|
62
|
+
def reload_schedule!
|
|
63
|
+
@schedule = get_schedules
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# gets the schedule as it exists in redis
|
|
67
|
+
def get_schedules
|
|
68
|
+
if redis.exists(:schedules)
|
|
69
|
+
redis.hgetall(:schedules).tap do |h|
|
|
70
|
+
h.each do |name, config|
|
|
71
|
+
h[name] = decode(config)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Create or update a schedule with the provided name and configuration.
|
|
80
|
+
#
|
|
81
|
+
# Note: values for class and custom_job_class need to be strings,
|
|
82
|
+
# not constants.
|
|
83
|
+
#
|
|
84
|
+
# Resque.set_schedule('some_job', {:class => 'SomeJob',
|
|
85
|
+
# :every => '15mins',
|
|
86
|
+
# :queue => 'high',
|
|
87
|
+
# :args => '/tmp/poop'})
|
|
88
|
+
def set_schedule(name, config)
|
|
89
|
+
existing_config = get_schedule(name)
|
|
90
|
+
unless existing_config && existing_config == config
|
|
91
|
+
redis.hset(:schedules, name, encode(config))
|
|
92
|
+
redis.sadd(:schedules_changed, name)
|
|
93
|
+
end
|
|
94
|
+
config
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# retrive the schedule configuration for the given name
|
|
98
|
+
def get_schedule(name)
|
|
99
|
+
decode(redis.hget(:schedules, name))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# remove a given schedule by name
|
|
103
|
+
def remove_schedule(name)
|
|
104
|
+
redis.hdel(:schedules, name)
|
|
105
|
+
redis.sadd(:schedules_changed, name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# This method is nearly identical to +enqueue+ only it also
|
|
109
|
+
# takes a timestamp which will be used to schedule the job
|
|
110
|
+
# for queueing. Until timestamp is in the past, the job will
|
|
111
|
+
# sit in the schedule list.
|
|
112
|
+
def enqueue_at(timestamp, klass, *args)
|
|
113
|
+
validate_job!(klass)
|
|
114
|
+
enqueue_at_with_queue(queue_from_class(klass), timestamp, klass, *args)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Identical to +enqueue_at+, except you can also specify
|
|
118
|
+
# a queue in which the job will be placed after the
|
|
119
|
+
# timestamp has passed. It respects Resque.inline option, by
|
|
120
|
+
# creating the job right away instead of adding to the queue.
|
|
121
|
+
def enqueue_at_with_queue(queue, timestamp, klass, *args)
|
|
122
|
+
return false unless Plugin.run_before_schedule_hooks(klass, *args)
|
|
123
|
+
|
|
124
|
+
if Resque.inline?
|
|
125
|
+
# Just create the job and let resque perform it right away with inline.
|
|
126
|
+
Resque::Job.create(queue, klass, *args)
|
|
127
|
+
else
|
|
128
|
+
delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Plugin.run_after_schedule_hooks(klass, *args)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Identical to enqueue_at but takes number_of_seconds_from_now
|
|
135
|
+
# instead of a timestamp.
|
|
136
|
+
def enqueue_in(number_of_seconds_from_now, klass, *args)
|
|
137
|
+
enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Identical to +enqueue_in+, except you can also specify
|
|
141
|
+
# a queue in which the job will be placed after the
|
|
142
|
+
# number of seconds has passed.
|
|
143
|
+
def enqueue_in_with_queue(queue, number_of_seconds_from_now, klass, *args)
|
|
144
|
+
enqueue_at_with_queue(queue, Time.now + number_of_seconds_from_now, klass, *args)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Used internally to stuff the item into the schedule sorted list.
|
|
148
|
+
# +timestamp+ can be either in seconds or a datetime object
|
|
149
|
+
# Insertion if O(log(n)).
|
|
150
|
+
# Returns true if it's the first job to be scheduled at that time, else false
|
|
151
|
+
def delayed_push(timestamp, item)
|
|
152
|
+
# First add this item to the list for this timestamp
|
|
153
|
+
redis.rpush("delayed:#{timestamp.to_i}", encode(item))
|
|
154
|
+
|
|
155
|
+
# Now, add this timestamp to the zsets. The score and the value are
|
|
156
|
+
# the same since we'll be querying by timestamp, and we don't have
|
|
157
|
+
# anything else to store.
|
|
158
|
+
redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns an array of timestamps based on start and count
|
|
162
|
+
def delayed_queue_peek(start, count)
|
|
163
|
+
Array(redis.zrange(:delayed_queue_schedule, start, start+count-1)).collect{|x| x.to_i}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns the size of the delayed queue schedule
|
|
167
|
+
def delayed_queue_schedule_size
|
|
168
|
+
redis.zcard :delayed_queue_schedule
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the number of jobs for a given timestamp in the delayed queue schedule
|
|
172
|
+
def delayed_timestamp_size(timestamp)
|
|
173
|
+
redis.llen("delayed:#{timestamp.to_i}").to_i
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Returns an array of delayed items for the given timestamp
|
|
177
|
+
def delayed_timestamp_peek(timestamp, start, count)
|
|
178
|
+
if 1 == count
|
|
179
|
+
r = list_range "delayed:#{timestamp.to_i}", start, count
|
|
180
|
+
r.nil? ? [] : [r]
|
|
181
|
+
else
|
|
182
|
+
list_range "delayed:#{timestamp.to_i}", start, count
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the next delayed queue timestamp
|
|
187
|
+
# (don't call directly)
|
|
188
|
+
def next_delayed_timestamp(at_time=nil)
|
|
189
|
+
items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
|
|
190
|
+
timestamp = items.nil? ? nil : Array(items).first
|
|
191
|
+
timestamp.to_i unless timestamp.nil?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Returns the next item to be processed for a given timestamp, nil if
|
|
195
|
+
# done. (don't call directly)
|
|
196
|
+
# +timestamp+ can either be in seconds or a datetime
|
|
197
|
+
def next_item_for_timestamp(timestamp)
|
|
198
|
+
key = "delayed:#{timestamp.to_i}"
|
|
199
|
+
|
|
200
|
+
item = decode redis.lpop(key)
|
|
201
|
+
|
|
202
|
+
# If the list is empty, remove it.
|
|
203
|
+
clean_up_timestamp(key, timestamp)
|
|
204
|
+
item
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Clears all jobs created with enqueue_at or enqueue_in
|
|
208
|
+
def reset_delayed_queue
|
|
209
|
+
Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
|
|
210
|
+
redis.del "delayed:#{item}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
redis.del :delayed_queue_schedule
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Given an encoded item, remove it from the delayed_queue
|
|
217
|
+
#
|
|
218
|
+
# This method is potentially very expensive since it needs to scan
|
|
219
|
+
# through the delayed queue for every timestamp.
|
|
220
|
+
def remove_delayed(klass, *args)
|
|
221
|
+
destroyed = 0
|
|
222
|
+
search = encode(job_to_hash(klass, args))
|
|
223
|
+
Array(redis.keys("delayed:*")).each do |key|
|
|
224
|
+
destroyed += redis.lrem key, 0, search
|
|
225
|
+
end
|
|
226
|
+
destroyed
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Given a timestamp and job (klass + args) it removes all instances and
|
|
230
|
+
# returns the count of jobs removed.
|
|
231
|
+
#
|
|
232
|
+
# O(N) where N is the number of jobs scheduled to fire at the given
|
|
233
|
+
# timestamp
|
|
234
|
+
def remove_delayed_job_from_timestamp(timestamp, klass, *args)
|
|
235
|
+
key = "delayed:#{timestamp.to_i}"
|
|
236
|
+
count = redis.lrem key, 0, encode(job_to_hash(klass, args))
|
|
237
|
+
clean_up_timestamp(key, timestamp)
|
|
238
|
+
count
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def count_all_scheduled_jobs
|
|
242
|
+
total_jobs = 0
|
|
243
|
+
Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
|
|
244
|
+
total_jobs += redis.llen("delayed:#{timestamp}").to_i
|
|
245
|
+
end
|
|
246
|
+
total_jobs
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def job_to_hash(klass, args)
|
|
252
|
+
{:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def job_to_hash_with_queue(queue, klass, args)
|
|
256
|
+
{:class => klass.to_s, :args => args, :queue => queue}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def clean_up_timestamp(key, timestamp)
|
|
260
|
+
# If the list is empty, remove it.
|
|
261
|
+
redis.watch key
|
|
262
|
+
if 0 == redis.llen(key).to_i
|
|
263
|
+
redis.multi do
|
|
264
|
+
redis.del key
|
|
265
|
+
redis.zrem :delayed_queue_schedule, timestamp.to_i
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
redis.unwatch
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
def validate_job!(klass)
|
|
272
|
+
if klass.to_s.empty?
|
|
273
|
+
raise Resque::NoClassError.new("Jobs must be given a class.")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
unless queue_from_class(klass)
|
|
277
|
+
raise Resque::NoQueueError.new("Jobs must be placed onto a queue.")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def prepare_schedule(schedule_hash)
|
|
282
|
+
prepared_hash = {}
|
|
283
|
+
schedule_hash.each do |name, job_spec|
|
|
284
|
+
job_spec = job_spec.dup
|
|
285
|
+
job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
|
|
286
|
+
prepared_hash[name] = job_spec
|
|
287
|
+
end
|
|
288
|
+
prepared_hash
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
Resque.extend ResqueScheduler
|