resque-scheduler 2.3.1 → 2.4.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/.gitignore +3 -0
- data/.rubocop.yml +11 -11
- data/.simplecov +1 -0
- data/.travis.yml +5 -2
- data/AUTHORS.md +3 -0
- data/HISTORY.md +26 -2
- data/LICENSE +1 -1
- data/README.md +120 -31
- data/ROADMAP.md +10 -0
- data/Rakefile +7 -19
- data/bin/resque-scheduler +5 -0
- data/examples/Rakefile +2 -0
- data/examples/config/initializers/resque-web.rb +37 -0
- data/examples/dynamic-scheduling/README.md +28 -0
- data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +54 -0
- data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +9 -0
- data/examples/dynamic-scheduling/app/models/user.rb +16 -0
- data/examples/dynamic-scheduling/config/resque.yml +4 -0
- data/examples/dynamic-scheduling/config/static_schedule.yml +7 -0
- data/examples/dynamic-scheduling/lib/tasks/resque.rake +48 -0
- data/examples/run-resque-web +3 -0
- data/lib/resque-scheduler.rb +2 -0
- data/lib/resque/scheduler.rb +130 -41
- data/lib/resque/scheduler/lock/resilient.rb +1 -1
- data/lib/resque/scheduler_locking.rb +3 -1
- data/lib/resque_scheduler.rb +73 -31
- data/lib/resque_scheduler/cli.rb +160 -0
- data/lib/resque_scheduler/logger_builder.rb +27 -8
- data/lib/resque_scheduler/plugin.rb +10 -7
- data/lib/resque_scheduler/server.rb +52 -11
- data/lib/resque_scheduler/server/views/delayed.erb +2 -0
- data/lib/resque_scheduler/server/views/delayed_schedules.erb +20 -0
- data/lib/resque_scheduler/server/views/scheduler.erb +4 -12
- data/lib/resque_scheduler/tasks.rb +15 -27
- data/lib/resque_scheduler/version.rb +1 -1
- data/resque-scheduler.gemspec +2 -0
- data/test/cli_test.rb +286 -0
- data/test/delayed_queue_test.rb +70 -1
- data/test/resque-web_test.rb +36 -1
- data/test/scheduler_args_test.rb +51 -17
- data/test/scheduler_hooks_test.rb +1 -1
- data/test/scheduler_locking_test.rb +63 -1
- data/test/scheduler_setup_test.rb +54 -18
- data/test/scheduler_task_test.rb +35 -0
- data/test/scheduler_test.rb +130 -42
- data/test/support/redis_instance.rb +8 -3
- data/test/test_helper.rb +47 -20
- metadata +77 -6
- checksums.yaml +0 -15
@@ -79,7 +79,9 @@ module Resque
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def master_lock_key
|
82
|
-
|
82
|
+
lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
|
83
|
+
lock_prefix += ':' if lock_prefix != ''
|
84
|
+
"#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
|
83
85
|
end
|
84
86
|
|
85
87
|
def redis_master_version
|
data/lib/resque_scheduler.rb
CHANGED
@@ -6,6 +6,8 @@ require 'resque/scheduler'
|
|
6
6
|
require 'resque_scheduler/plugin'
|
7
7
|
|
8
8
|
module ResqueScheduler
|
9
|
+
autoload :Cli, 'resque_scheduler/cli'
|
10
|
+
|
9
11
|
#
|
10
12
|
# Accepts a new schedule configuration of the form:
|
11
13
|
#
|
@@ -91,7 +93,7 @@ module ResqueScheduler
|
|
91
93
|
def clean_schedules
|
92
94
|
if redis.exists(:schedules)
|
93
95
|
redis.hkeys(:schedules).each do |key|
|
94
|
-
remove_schedule(key)
|
96
|
+
remove_schedule(key) if !schedule_persisted?(key)
|
95
97
|
end
|
96
98
|
end
|
97
99
|
@schedule = nil
|
@@ -109,9 +111,15 @@ module ResqueScheduler
|
|
109
111
|
# :args => '/tmp/poop'})
|
110
112
|
def set_schedule(name, config)
|
111
113
|
existing_config = get_schedule(name)
|
114
|
+
persist = config.delete(:persist) || config.delete('persist')
|
112
115
|
unless existing_config && existing_config == config
|
113
|
-
redis.
|
114
|
-
|
116
|
+
redis.pipelined do
|
117
|
+
redis.hset(:schedules, name, encode(config))
|
118
|
+
redis.sadd(:schedules_changed, name)
|
119
|
+
if persist
|
120
|
+
redis.sadd(:persisted_schedules, name)
|
121
|
+
end
|
122
|
+
end
|
115
123
|
end
|
116
124
|
config
|
117
125
|
end
|
@@ -121,10 +129,17 @@ module ResqueScheduler
|
|
121
129
|
decode(redis.hget(:schedules, name))
|
122
130
|
end
|
123
131
|
|
132
|
+
def schedule_persisted?(name)
|
133
|
+
redis.sismember(:persisted_schedules, name)
|
134
|
+
end
|
135
|
+
|
124
136
|
# remove a given schedule by name
|
125
137
|
def remove_schedule(name)
|
126
|
-
redis.
|
127
|
-
|
138
|
+
redis.pipelined do
|
139
|
+
redis.hdel(:schedules, name)
|
140
|
+
redis.srem(:persisted_schedules, name)
|
141
|
+
redis.sadd(:schedules_changed, name)
|
142
|
+
end
|
128
143
|
end
|
129
144
|
|
130
145
|
# This method is nearly identical to +enqueue+ only it also
|
@@ -272,6 +287,33 @@ module ResqueScheduler
|
|
272
287
|
remove_delayed(klass, *args).times { Resque::Scheduler.enqueue_from_config(hash) }
|
273
288
|
end
|
274
289
|
|
290
|
+
# Given a block, remove jobs that return true from a block
|
291
|
+
#
|
292
|
+
# This allows for removal of delayed jobs that have arguments matching certain criteria
|
293
|
+
def remove_delayed_selection
|
294
|
+
fail ArgumentError, "Please supply a block" unless block_given?
|
295
|
+
|
296
|
+
destroyed = 0
|
297
|
+
# There is no way to search Redis list entries for a partial match, so we query for all
|
298
|
+
# delayed job tasks and do our matching after decoding the payload data
|
299
|
+
jobs = Resque.redis.keys("delayed:*")
|
300
|
+
jobs.each do |job|
|
301
|
+
index = Resque.redis.llen(job) - 1
|
302
|
+
while index >= 0
|
303
|
+
payload = Resque.redis.lindex(job, index)
|
304
|
+
decoded_payload = decode(payload)
|
305
|
+
if yield(decoded_payload['args'])
|
306
|
+
removed = redis.lrem job, 0, payload
|
307
|
+
destroyed += removed
|
308
|
+
index -= removed
|
309
|
+
else
|
310
|
+
index -= 1
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
destroyed
|
315
|
+
end
|
316
|
+
|
275
317
|
# Given a timestamp and job (klass + args) it removes all instances and
|
276
318
|
# returns the count of jobs removed.
|
277
319
|
#
|
@@ -306,39 +348,39 @@ module ResqueScheduler
|
|
306
348
|
|
307
349
|
private
|
308
350
|
|
309
|
-
|
310
|
-
|
311
|
-
|
351
|
+
def job_to_hash(klass, args)
|
352
|
+
{:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
|
353
|
+
end
|
312
354
|
|
313
|
-
|
314
|
-
|
315
|
-
|
355
|
+
def job_to_hash_with_queue(queue, klass, args)
|
356
|
+
{:class => klass.to_s, :args => args, :queue => queue}
|
357
|
+
end
|
316
358
|
|
317
|
-
|
318
|
-
|
359
|
+
def clean_up_timestamp(key, timestamp)
|
360
|
+
# If the list is empty, remove it.
|
319
361
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
end
|
328
|
-
else
|
329
|
-
redis.unwatch
|
362
|
+
# Use a watch here to ensure nobody adds jobs to this delayed
|
363
|
+
# queue while we're removing it.
|
364
|
+
redis.watch key
|
365
|
+
if 0 == redis.llen(key).to_i
|
366
|
+
redis.multi do
|
367
|
+
redis.del key
|
368
|
+
redis.zrem :delayed_queue_schedule, timestamp.to_i
|
330
369
|
end
|
370
|
+
else
|
371
|
+
redis.unwatch
|
331
372
|
end
|
373
|
+
end
|
332
374
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
end
|
340
|
-
prepared_hash
|
375
|
+
def prepare_schedule(schedule_hash)
|
376
|
+
prepared_hash = {}
|
377
|
+
schedule_hash.each do |name, job_spec|
|
378
|
+
job_spec = job_spec.dup
|
379
|
+
job_spec['class'] = name unless job_spec.key?('class') || job_spec.key?(:class)
|
380
|
+
prepared_hash[name] = job_spec
|
341
381
|
end
|
382
|
+
prepared_hash
|
383
|
+
end
|
342
384
|
end
|
343
385
|
|
344
386
|
Resque.extend ResqueScheduler
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module ResqueScheduler
|
6
|
+
class Cli
|
7
|
+
BANNER = <<-EOF.gsub(/ {6}/, '')
|
8
|
+
Usage: resque-scheduler [options]
|
9
|
+
|
10
|
+
Runs a resque scheduler process directly (rather than via rake).
|
11
|
+
|
12
|
+
EOF
|
13
|
+
OPTIONS = [
|
14
|
+
{
|
15
|
+
args: ['-n', '--app-name [APP_NAME]', 'Application name for procline'],
|
16
|
+
callback: ->(options) { ->(n) { options[:app_name] = n } }
|
17
|
+
},
|
18
|
+
{
|
19
|
+
args: ['-B', '--background', 'Run in the background [BACKGROUND]'],
|
20
|
+
callback: ->(options) { ->(b) { options[:background] = b } }
|
21
|
+
},
|
22
|
+
{
|
23
|
+
args: ['-D', '--dynamic-schedule',
|
24
|
+
'Enable dynamic scheduling [DYNAMIC_SCHEDULE]'],
|
25
|
+
callback: ->(options) { ->(d) { options[:dynamic] = d } }
|
26
|
+
},
|
27
|
+
{
|
28
|
+
args: ['-E', '--environment [RAILS_ENV]', 'Environment name'],
|
29
|
+
callback: ->(options) { ->(e) { options[:env] = e } }
|
30
|
+
},
|
31
|
+
{
|
32
|
+
args: ['-I', '--initializer-path [INITIALIZER_PATH]',
|
33
|
+
'Path to optional initializer ruby file'],
|
34
|
+
callback: ->(options) { ->(i) { options[:initializer_path] = i } }
|
35
|
+
},
|
36
|
+
{
|
37
|
+
args: ['-i', '--interval [RESQUE_SCHEDULER_INTERVAL]',
|
38
|
+
'Interval for checking if a scheduled job must run'],
|
39
|
+
callback: ->(options) { ->(i) { options[:poll_sleep_amount] = i } }
|
40
|
+
},
|
41
|
+
{
|
42
|
+
args: ['-l', '--logfile [LOGFILE]', 'Log file name'],
|
43
|
+
callback: ->(options) { ->(l) { options[:logfile] = l } }
|
44
|
+
},
|
45
|
+
{
|
46
|
+
args: ['-F', '--logformat [LOGFORMAT]', 'Log output format'],
|
47
|
+
callback: ->(options) { ->(f) { options[:logformat] = f } }
|
48
|
+
},
|
49
|
+
{
|
50
|
+
args: ['-P', '--pidfile [PIDFILE]', 'PID file name'],
|
51
|
+
callback: ->(options) { ->(p) { options[:pidfile] = p } }
|
52
|
+
},
|
53
|
+
{
|
54
|
+
args: ['-q', '--quiet', 'Run with minimal output [QUIET] (or [MUTE])'],
|
55
|
+
callback: ->(options) { ->(q) { options[:mute] = q } }
|
56
|
+
},
|
57
|
+
{
|
58
|
+
args: ['-v', '--verbose', 'Run with verbose output [VERBOSE]'],
|
59
|
+
callback: ->(options) { ->(v) { options[:verbose] = v } }
|
60
|
+
}
|
61
|
+
].freeze
|
62
|
+
|
63
|
+
def self.run!(argv = ARGV, env = ENV)
|
64
|
+
new(argv, env).run!
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(argv = ARGV, env = ENV)
|
68
|
+
@argv = argv
|
69
|
+
@env = env
|
70
|
+
end
|
71
|
+
|
72
|
+
def run!
|
73
|
+
pre_run
|
74
|
+
run_forever
|
75
|
+
end
|
76
|
+
|
77
|
+
def pre_run
|
78
|
+
parse_options
|
79
|
+
pre_setup
|
80
|
+
setup_env
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_options
|
84
|
+
OptionParser.new do |opts|
|
85
|
+
opts.banner = BANNER
|
86
|
+
OPTIONS.each do |opt|
|
87
|
+
opts.on(*opt[:args], &(opt[:callback].call(options)))
|
88
|
+
end
|
89
|
+
end.parse!(argv.dup)
|
90
|
+
end
|
91
|
+
|
92
|
+
def pre_setup
|
93
|
+
if options[:initializer_path]
|
94
|
+
load options[:initializer_path].to_s.strip
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def setup_env
|
101
|
+
require 'resque'
|
102
|
+
require 'resque/scheduler'
|
103
|
+
|
104
|
+
# Need to set this here for conditional Process.daemon redirect of
|
105
|
+
# stderr/stdout to /dev/null
|
106
|
+
Resque::Scheduler.mute = !!options[:mute]
|
107
|
+
|
108
|
+
if options[:background]
|
109
|
+
unless Process.respond_to?('daemon')
|
110
|
+
abort 'background option is set, which requires ruby >= 1.9'
|
111
|
+
end
|
112
|
+
|
113
|
+
Process.daemon(true, !Resque::Scheduler.mute)
|
114
|
+
Resque.redis.client.reconnect
|
115
|
+
end
|
116
|
+
|
117
|
+
File.open(options[:pidfile], 'w') do |f|
|
118
|
+
f.puts $PROCESS_ID
|
119
|
+
end if options[:pidfile]
|
120
|
+
|
121
|
+
Resque::Scheduler.configure do |c|
|
122
|
+
# These settings are somewhat redundant given the defaults present
|
123
|
+
# in the attr reader methods. They are left here for clarity and
|
124
|
+
# to serve as an example of how to use `.configure`.
|
125
|
+
|
126
|
+
c.app_name = options[:app_name]
|
127
|
+
c.dynamic = !!options[:dynamic]
|
128
|
+
c.env = options[:env]
|
129
|
+
c.logfile = options[:logfile]
|
130
|
+
c.logformat = options[:logformat]
|
131
|
+
c.poll_sleep_amount = Float(options[:poll_sleep_amount] || '5')
|
132
|
+
c.verbose = !!options[:verbose]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def run_forever
|
137
|
+
Resque::Scheduler.run
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
attr_reader :argv, :env
|
143
|
+
|
144
|
+
def options
|
145
|
+
@options ||= {
|
146
|
+
app_name: env['APP_NAME'],
|
147
|
+
background: env['BACKGROUND'],
|
148
|
+
dynamic: env['DYNAMIC_SCHEDULE'],
|
149
|
+
env: env['RAILS_ENV'],
|
150
|
+
initializer_path: env['INITIALIZER_PATH'],
|
151
|
+
logfile: env['LOGFILE'],
|
152
|
+
logformat: env['LOGFORMAT'],
|
153
|
+
mute: env['MUTE'] || env['QUIET'],
|
154
|
+
pidfile: env['PIDFILE'],
|
155
|
+
poll_sleep_amount: env['RESQUE_SCHEDULER_INTERVAL'],
|
156
|
+
verbose: env['VERBOSE']
|
157
|
+
}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
|
1
3
|
module ResqueScheduler
|
2
4
|
# Just builds a logger, with specified verbosity and destination.
|
3
5
|
# The simplest example:
|
@@ -10,23 +12,25 @@ module ResqueScheduler
|
|
10
12
|
# - :mute if logger needs to be silent for all levels. Default - false
|
11
13
|
# - :verbose if there is a need in debug messages. Default - false
|
12
14
|
# - :log_dev to output logs into a desired file. Default - STDOUT
|
15
|
+
# - :format log format, either 'text' or 'json'. Default - 'text'
|
13
16
|
#
|
14
17
|
# Example:
|
15
18
|
#
|
16
|
-
# LoggerBuilder.new(
|
19
|
+
# LoggerBuilder.new(
|
20
|
+
# :mute => false, :verbose => true, :log_dev => 'log/scheduler.log'
|
21
|
+
# )
|
17
22
|
def initialize(opts={})
|
18
|
-
@muted
|
23
|
+
@muted = !!opts[:mute]
|
19
24
|
@verbose = !!opts[:verbose]
|
20
|
-
@log_dev = opts[:log_dev] ||
|
25
|
+
@log_dev = opts[:log_dev] || $stdout
|
26
|
+
@format = opts[:format] || 'text'
|
21
27
|
end
|
22
28
|
|
23
29
|
# Returns an instance of Logger
|
24
30
|
def build
|
25
31
|
logger = Logger.new(@log_dev)
|
26
32
|
logger.level = level
|
27
|
-
logger.
|
28
|
-
logger.formatter = formatter
|
29
|
-
|
33
|
+
logger.formatter = send(:"#{@format}_formatter")
|
30
34
|
logger
|
31
35
|
end
|
32
36
|
|
@@ -42,9 +46,24 @@ module ResqueScheduler
|
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
|
-
def
|
49
|
+
def text_formatter
|
50
|
+
proc do |severity, datetime, progname, msg|
|
51
|
+
"resque-scheduler: [#{severity}] #{datetime.iso8601}: #{msg}\n"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def json_formatter
|
46
56
|
proc do |severity, datetime, progname, msg|
|
47
|
-
|
57
|
+
require 'json'
|
58
|
+
JSON.dump(
|
59
|
+
{
|
60
|
+
:name => 'resque-scheduler',
|
61
|
+
:progname => progname,
|
62
|
+
:level => severity,
|
63
|
+
:timestamp => datetime.iso8601,
|
64
|
+
:msg => msg
|
65
|
+
}
|
66
|
+
) + "\n"
|
48
67
|
end
|
49
68
|
end
|
50
69
|
end
|
@@ -13,13 +13,16 @@ module ResqueScheduler
|
|
13
13
|
results.all? { |result| result != false }
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
def run_before_delayed_enqueue_hooks(klass, *args)
|
17
|
+
run_hooks(klass, 'before_delayed_enqueue', *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_before_schedule_hooks(klass, *args)
|
21
|
+
run_hooks(klass, 'before_schedule', *args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run_after_schedule_hooks(klass, *args)
|
25
|
+
run_hooks(klass, 'after_schedule', *args)
|
23
26
|
end
|
24
27
|
end
|
25
28
|
end
|
@@ -1,14 +1,12 @@
|
|
1
1
|
require 'resque_scheduler'
|
2
2
|
require 'resque/server'
|
3
|
+
require 'json'
|
4
|
+
|
3
5
|
# Extend Resque::Server to add tabs
|
4
6
|
module ResqueScheduler
|
5
|
-
|
6
7
|
module Server
|
7
|
-
|
8
8
|
def self.included(base)
|
9
|
-
|
10
9
|
base.class_eval do
|
11
|
-
|
12
10
|
helpers do
|
13
11
|
def format_time(t)
|
14
12
|
t.strftime("%Y-%m-%d %H:%M:%S %z")
|
@@ -17,6 +15,41 @@ module ResqueScheduler
|
|
17
15
|
def queue_from_class_name(class_name)
|
18
16
|
Resque.queue_from_class(ResqueScheduler::Util.constantize(class_name))
|
19
17
|
end
|
18
|
+
|
19
|
+
def schedule_interval(config)
|
20
|
+
if config['every']
|
21
|
+
schedule_interval_every(config['every'])
|
22
|
+
elsif config['cron']
|
23
|
+
'cron: ' + config['cron'].to_s
|
24
|
+
else
|
25
|
+
'Not currently scheduled'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def schedule_interval_every(every)
|
30
|
+
s = 'every: '
|
31
|
+
if every.respond_to?(:first)
|
32
|
+
s << every.first
|
33
|
+
else
|
34
|
+
s << every
|
35
|
+
end
|
36
|
+
|
37
|
+
return s unless every.respond_to?(:last) && every.length > 1
|
38
|
+
|
39
|
+
s << ' ('
|
40
|
+
meta = every.last.map do |key, value|
|
41
|
+
"#{key.to_s.gsub(/_/, ' ')} #{value}"
|
42
|
+
end
|
43
|
+
s << meta.join(', ') << ')'
|
44
|
+
end
|
45
|
+
|
46
|
+
def schedule_class(config)
|
47
|
+
if config['class'].nil? && !config['custom_job_class'].nil?
|
48
|
+
config['custom_job_class']
|
49
|
+
else
|
50
|
+
config['class']
|
51
|
+
end
|
52
|
+
end
|
20
53
|
end
|
21
54
|
|
22
55
|
get "/schedule" do
|
@@ -59,6 +92,18 @@ module ResqueScheduler
|
|
59
92
|
erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
|
60
93
|
end
|
61
94
|
|
95
|
+
get "/delayed/jobs/:klass" do
|
96
|
+
begin
|
97
|
+
klass = ResqueScheduler::Util::constantize(params[:klass])
|
98
|
+
@args = JSON.load(URI.decode(params[:args]))
|
99
|
+
@timestamps = Resque.scheduled_at(klass, *@args)
|
100
|
+
rescue => err
|
101
|
+
@timestamps = []
|
102
|
+
end
|
103
|
+
|
104
|
+
erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_schedules.erb'))
|
105
|
+
end
|
106
|
+
|
62
107
|
get "/delayed/:timestamp" do
|
63
108
|
# Is there a better way to specify alternate template locations with sinatra?
|
64
109
|
erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
|
@@ -74,18 +119,14 @@ module ResqueScheduler
|
|
74
119
|
Resque.reset_delayed_queue
|
75
120
|
redirect u('delayed')
|
76
121
|
end
|
77
|
-
|
78
122
|
end
|
79
|
-
|
80
123
|
end
|
81
|
-
|
82
|
-
Resque::Server.tabs << 'Schedule'
|
83
|
-
Resque::Server.tabs << 'Delayed'
|
84
|
-
|
85
124
|
end
|
86
|
-
|
87
125
|
end
|
88
126
|
|
127
|
+
Resque::Server.tabs << 'Schedule'
|
128
|
+
Resque::Server.tabs << 'Delayed'
|
129
|
+
|
89
130
|
Resque::Server.class_eval do
|
90
131
|
include ResqueScheduler::Server
|
91
132
|
end
|