qs 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +6 -1
- data/LICENSE.txt +1 -1
- data/bench/config.qs +46 -0
- data/bench/queue.rb +8 -0
- data/bench/report.rb +114 -0
- data/bench/report.txt +11 -0
- data/bin/qs +7 -0
- data/lib/qs/cli.rb +124 -0
- data/lib/qs/client.rb +121 -0
- data/lib/qs/config_file.rb +79 -0
- data/lib/qs/daemon.rb +350 -0
- data/lib/qs/daemon_data.rb +46 -0
- data/lib/qs/error_handler.rb +58 -0
- data/lib/qs/job.rb +70 -0
- data/lib/qs/job_handler.rb +90 -0
- data/lib/qs/logger.rb +23 -0
- data/lib/qs/payload_handler.rb +136 -0
- data/lib/qs/pid_file.rb +42 -0
- data/lib/qs/process.rb +136 -0
- data/lib/qs/process_signal.rb +20 -0
- data/lib/qs/qs_runner.rb +49 -0
- data/lib/qs/queue.rb +69 -0
- data/lib/qs/redis_item.rb +33 -0
- data/lib/qs/route.rb +52 -0
- data/lib/qs/runner.rb +26 -0
- data/lib/qs/test_helpers.rb +17 -0
- data/lib/qs/test_runner.rb +43 -0
- data/lib/qs/version.rb +1 -1
- data/lib/qs.rb +92 -2
- data/qs.gemspec +7 -2
- data/test/helper.rb +8 -1
- data/test/support/app_daemon.rb +74 -0
- data/test/support/config.qs +7 -0
- data/test/support/config_files/empty.qs +0 -0
- data/test/support/config_files/invalid.qs +1 -0
- data/test/support/config_files/valid.qs +7 -0
- data/test/support/config_invalid_run.qs +3 -0
- data/test/support/config_no_run.qs +0 -0
- data/test/support/factory.rb +14 -0
- data/test/support/pid_file_spy.rb +19 -0
- data/test/support/runner_spy.rb +17 -0
- data/test/system/daemon_tests.rb +226 -0
- data/test/unit/cli_tests.rb +188 -0
- data/test/unit/client_tests.rb +269 -0
- data/test/unit/config_file_tests.rb +59 -0
- data/test/unit/daemon_data_tests.rb +96 -0
- data/test/unit/daemon_tests.rb +702 -0
- data/test/unit/error_handler_tests.rb +163 -0
- data/test/unit/job_handler_tests.rb +253 -0
- data/test/unit/job_tests.rb +132 -0
- data/test/unit/logger_tests.rb +38 -0
- data/test/unit/payload_handler_tests.rb +276 -0
- data/test/unit/pid_file_tests.rb +70 -0
- data/test/unit/process_signal_tests.rb +61 -0
- data/test/unit/process_tests.rb +371 -0
- data/test/unit/qs_runner_tests.rb +166 -0
- data/test/unit/qs_tests.rb +217 -0
- data/test/unit/queue_tests.rb +132 -0
- data/test/unit/redis_item_tests.rb +49 -0
- data/test/unit/route_tests.rb +81 -0
- data/test/unit/runner_tests.rb +63 -0
- data/test/unit/test_helper_tests.rb +61 -0
- data/test/unit/test_runner_tests.rb +128 -0
- metadata +180 -15
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'dat-worker-pool'
|
3
|
+
require 'qs'
|
4
|
+
require 'qs/error_handler'
|
5
|
+
require 'qs/job'
|
6
|
+
require 'qs/logger'
|
7
|
+
|
8
|
+
module Qs
|
9
|
+
|
10
|
+
class PayloadHandler
|
11
|
+
|
12
|
+
attr_reader :daemon_data, :redis_item, :logger
|
13
|
+
|
14
|
+
def initialize(daemon_data, redis_item)
|
15
|
+
@daemon_data = daemon_data
|
16
|
+
@redis_item = redis_item
|
17
|
+
@logger = Qs::Logger.new(
|
18
|
+
@daemon_data.logger,
|
19
|
+
@daemon_data.verbose_logging
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
log_received
|
25
|
+
benchmark = Benchmark.measure{ run!(@daemon_data, @redis_item) }
|
26
|
+
@redis_item.time_taken = RoundedTime.new(benchmark.real)
|
27
|
+
log_complete(@redis_item)
|
28
|
+
raise_if_debugging!(@redis_item.exception)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run!(daemon_data, redis_item)
|
34
|
+
redis_item.started = true
|
35
|
+
|
36
|
+
payload = Qs.deserialize(redis_item.serialized_payload)
|
37
|
+
job = Qs::Job.parse(payload)
|
38
|
+
log_job(job)
|
39
|
+
redis_item.job = job
|
40
|
+
|
41
|
+
route = daemon_data.route_for(job.name)
|
42
|
+
log_handler_class(route.handler_class)
|
43
|
+
redis_item.handler_class = route.handler_class
|
44
|
+
|
45
|
+
route.run(job, daemon_data)
|
46
|
+
redis_item.finished = true
|
47
|
+
rescue DatWorkerPool::ShutdownError => exception
|
48
|
+
if redis_item.started
|
49
|
+
error = ShutdownError.new(exception.message)
|
50
|
+
error.set_backtrace(exception.backtrace)
|
51
|
+
handle_exception(error, daemon_data, redis_item)
|
52
|
+
end
|
53
|
+
raise exception
|
54
|
+
rescue StandardError => exception
|
55
|
+
handle_exception(exception, daemon_data, redis_item)
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle_exception(exception, daemon_data, redis_item)
|
59
|
+
error_handler = Qs::ErrorHandler.new(exception, {
|
60
|
+
:daemon_data => daemon_data,
|
61
|
+
:queue_redis_key => redis_item.queue_redis_key,
|
62
|
+
:serialized_payload => redis_item.serialized_payload,
|
63
|
+
:job => redis_item.job,
|
64
|
+
:handler_class => redis_item.handler_class
|
65
|
+
}).tap(&:run)
|
66
|
+
redis_item.exception = error_handler.exception
|
67
|
+
log_exception(redis_item.exception)
|
68
|
+
end
|
69
|
+
|
70
|
+
def raise_if_debugging!(exception)
|
71
|
+
raise exception if exception && ENV['QS_DEBUG']
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_received
|
75
|
+
log_verbose "===== Running job ====="
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_job(job)
|
79
|
+
log_verbose " Job: #{job.name.inspect}"
|
80
|
+
log_verbose " Params: #{job.params.inspect}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_handler_class(handler_class)
|
84
|
+
log_verbose " Handler: #{handler_class}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def log_complete(redis_item)
|
88
|
+
log_verbose "===== Completed in #{redis_item.time_taken}ms ====="
|
89
|
+
summary_line_args = {
|
90
|
+
'time' => redis_item.time_taken,
|
91
|
+
'handler' => redis_item.handler_class
|
92
|
+
}
|
93
|
+
if (job = redis_item.job)
|
94
|
+
summary_line_args['job'] = job.name
|
95
|
+
summary_line_args['params'] = job.params
|
96
|
+
end
|
97
|
+
if (exception = redis_item.exception)
|
98
|
+
summary_line_args['error'] = "#{exception.inspect}"
|
99
|
+
end
|
100
|
+
log_summary SummaryLine.new(summary_line_args)
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_exception(exception)
|
104
|
+
backtrace = exception.backtrace.join("\n")
|
105
|
+
message = "#{exception.class}: #{exception.message}\n#{backtrace}"
|
106
|
+
log_verbose(message, :error)
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_verbose(message, level = :info)
|
110
|
+
self.logger.verbose.send(level, "[Qs] #{message}")
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_summary(message, level = :info)
|
114
|
+
self.logger.summary.send(level, "[Qs] #{message}")
|
115
|
+
end
|
116
|
+
|
117
|
+
module RoundedTime
|
118
|
+
ROUND_PRECISION = 2
|
119
|
+
ROUND_MODIFIER = 10 ** ROUND_PRECISION
|
120
|
+
def self.new(time_in_seconds)
|
121
|
+
(time_in_seconds * 1000 * ROUND_MODIFIER).to_i / ROUND_MODIFIER.to_f
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
module SummaryLine
|
126
|
+
def self.new(line_attrs)
|
127
|
+
attr_keys = %w{time handler job params error}
|
128
|
+
attr_keys.map{ |k| "#{k}=#{line_attrs[k].inspect}" }.join(' ')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
ShutdownError = Class.new(DatWorkerPool::ShutdownError)
|
135
|
+
|
136
|
+
end
|
data/lib/qs/pid_file.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class PIDFile
|
6
|
+
attr_reader :path
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@path = (path || '/dev/null').to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def pid
|
13
|
+
pid = File.read(@path).strip
|
14
|
+
pid && !pid.empty? ? pid.to_i : raise('no pid in file')
|
15
|
+
rescue StandardError => exception
|
16
|
+
error = InvalidError.new("A PID couldn't be read from #{@path.inspect}")
|
17
|
+
error.set_backtrace(exception.backtrace)
|
18
|
+
raise error
|
19
|
+
end
|
20
|
+
|
21
|
+
def write
|
22
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
23
|
+
File.open(@path, 'w'){ |f| f.puts ::Process.pid }
|
24
|
+
rescue StandardError => exception
|
25
|
+
error = InvalidError.new("Can't write pid to file #{@path.inspect}")
|
26
|
+
error.set_backtrace(exception.backtrace)
|
27
|
+
raise error
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove
|
31
|
+
FileUtils.rm_f(@path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@path
|
36
|
+
end
|
37
|
+
|
38
|
+
InvalidError = Class.new(RuntimeError)
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/qs/process.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'qs/pid_file'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class Process
|
6
|
+
|
7
|
+
attr_reader :daemon, :name, :pid_file, :restart_cmd
|
8
|
+
|
9
|
+
def initialize(daemon, options = nil)
|
10
|
+
options ||= {}
|
11
|
+
@daemon = daemon
|
12
|
+
@logger = @daemon.logger
|
13
|
+
@pid_file = PIDFile.new(@daemon.pid_file)
|
14
|
+
@restart_cmd = RestartCmd.new
|
15
|
+
|
16
|
+
@name = ignore_if_blank(ENV['QS_PROCESS_NAME']) || "qs-#{@daemon.name}"
|
17
|
+
|
18
|
+
@daemonize = !!options[:daemonize]
|
19
|
+
@skip_daemonize = !!ignore_if_blank(ENV['QS_SKIP_DAEMONIZE'])
|
20
|
+
@restart = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
::Process.daemon(true) if self.daemonize?
|
25
|
+
log "Starting Qs daemon for #{@daemon.name}..."
|
26
|
+
|
27
|
+
$0 = @name
|
28
|
+
@pid_file.write
|
29
|
+
log "PID: #{@pid_file.pid}"
|
30
|
+
|
31
|
+
::Signal.trap("TERM"){ @daemon.stop }
|
32
|
+
::Signal.trap("INT"){ @daemon.halt }
|
33
|
+
::Signal.trap("USR2") do
|
34
|
+
@daemon.stop
|
35
|
+
@restart = true
|
36
|
+
end
|
37
|
+
|
38
|
+
thread = @daemon.start
|
39
|
+
log "#{@daemon.name} daemon started and ready."
|
40
|
+
thread.join
|
41
|
+
run_restart_cmd if self.restart?
|
42
|
+
rescue StandardError => exception
|
43
|
+
log "Error: #{exception.message}"
|
44
|
+
log "#{@daemon.name} daemon never started."
|
45
|
+
ensure
|
46
|
+
@pid_file.remove
|
47
|
+
end
|
48
|
+
|
49
|
+
def daemonize?
|
50
|
+
@daemonize && !@skip_daemonize
|
51
|
+
end
|
52
|
+
|
53
|
+
def restart?
|
54
|
+
@restart
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def log(message)
|
60
|
+
@logger.info "[Qs] #{message}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_restart_cmd
|
64
|
+
log "Restarting #{@daemon.name} daemon..."
|
65
|
+
ENV['QS_SKIP_DAEMONIZE'] = 'yes'
|
66
|
+
@restart_cmd.run
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_if_blank(value, default, &block)
|
70
|
+
ignore_if_blank(value, &block) || default
|
71
|
+
end
|
72
|
+
|
73
|
+
def ignore_if_blank(value, &block)
|
74
|
+
block ||= proc{ |v| v }
|
75
|
+
block.call(value) if value && !value.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class RestartCmd
|
81
|
+
attr_reader :argv, :dir
|
82
|
+
|
83
|
+
def initialize
|
84
|
+
require 'rubygems'
|
85
|
+
@dir = get_pwd
|
86
|
+
@argv = [Gem.ruby, $0, ARGV.dup].flatten
|
87
|
+
end
|
88
|
+
|
89
|
+
def run
|
90
|
+
Dir.chdir self.dir
|
91
|
+
Kernel.exec(*self.argv)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Trick from puma/unicorn. Favor PWD because it contains an unresolved
|
97
|
+
# symlink. This is useful when restarting after deploying; the original
|
98
|
+
# directory may be removed, but the symlink is pointing to a new
|
99
|
+
# directory.
|
100
|
+
def get_pwd
|
101
|
+
return Dir.pwd if ENV['PWD'].nil?
|
102
|
+
env_stat = File.stat(ENV['PWD'])
|
103
|
+
pwd_stat = File.stat(Dir.pwd)
|
104
|
+
if env_stat.ino == pwd_stat.ino && env_stat.dev == pwd_stat.dev
|
105
|
+
ENV['PWD']
|
106
|
+
else
|
107
|
+
Dir.pwd
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# This is from puma for 1.8 compatibility. Ruby 1.9+ defines a
|
113
|
+
# `Process.daemon` for daemonizing processes. This defines the method when it
|
114
|
+
# isn't provided, i.e. Ruby 1.8.
|
115
|
+
unless ::Process.respond_to?(:daemon)
|
116
|
+
::Process.class_eval do
|
117
|
+
|
118
|
+
# Full explanation: http://www.steve.org.uk/Reference/Unix/faq_2.html#SEC16
|
119
|
+
def self.daemon(no_chdir = false, no_close = false)
|
120
|
+
exit if fork
|
121
|
+
::Process.setsid
|
122
|
+
exit if fork
|
123
|
+
Dir.chdir '/' unless no_chdir
|
124
|
+
if !no_close
|
125
|
+
null = File.open('/dev/null', 'w')
|
126
|
+
STDIN.reopen null
|
127
|
+
STDOUT.reopen null
|
128
|
+
STDERR.reopen null
|
129
|
+
end
|
130
|
+
return 0
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'qs/pid_file'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class ProcessSignal
|
6
|
+
|
7
|
+
attr_reader :signal, :pid
|
8
|
+
|
9
|
+
def initialize(daemon, signal)
|
10
|
+
@signal = signal
|
11
|
+
@pid = PIDFile.new(daemon.pid_file).pid
|
12
|
+
end
|
13
|
+
|
14
|
+
def send
|
15
|
+
::Process.kill(@signal, @pid)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/lib/qs/qs_runner.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'system_timer'
|
2
|
+
require 'qs'
|
3
|
+
require 'qs/runner'
|
4
|
+
|
5
|
+
module Qs
|
6
|
+
|
7
|
+
class QsRunner < Runner
|
8
|
+
|
9
|
+
attr_reader :timeout
|
10
|
+
|
11
|
+
def initialize(handler_class, args = nil)
|
12
|
+
super(handler_class, args)
|
13
|
+
@timeout = handler_class.timeout || Qs.config.timeout
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
OptionalTimeout.new(self.timeout) do
|
18
|
+
run_callbacks self.handler_class.before_callbacks
|
19
|
+
self.handler.init
|
20
|
+
self.handler.run
|
21
|
+
run_callbacks self.handler_class.after_callbacks
|
22
|
+
end
|
23
|
+
rescue TimeoutError => exception
|
24
|
+
error = TimeoutError.new "#{handler_class} timed out (#{timeout}s)"
|
25
|
+
error.set_backtrace(exception.backtrace)
|
26
|
+
raise error
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def run_callbacks(callbacks)
|
32
|
+
callbacks.each{ |proc| self.handler.instance_eval(&proc) }
|
33
|
+
end
|
34
|
+
|
35
|
+
module OptionalTimeout
|
36
|
+
def self.new(timeout, &block)
|
37
|
+
if !timeout.nil?
|
38
|
+
SystemTimer.timeout_after(timeout, TimeoutError, &block)
|
39
|
+
else
|
40
|
+
block.call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
TimeoutError = Class.new(RuntimeError)
|
48
|
+
|
49
|
+
end
|
data/lib/qs/queue.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'qs/route'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class Queue
|
6
|
+
|
7
|
+
attr_reader :routes
|
8
|
+
attr_reader :enqueued_jobs
|
9
|
+
|
10
|
+
def initialize(&block)
|
11
|
+
@routes = []
|
12
|
+
@enqueued_jobs = []
|
13
|
+
self.instance_eval(&block) if !block.nil?
|
14
|
+
raise InvalidError, "a queue must have a name" if self.name.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def name(value = nil)
|
18
|
+
@name = value if !value.nil?
|
19
|
+
@name
|
20
|
+
end
|
21
|
+
|
22
|
+
def redis_key
|
23
|
+
@redis_key ||= RedisKey.new(self.name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def job_handler_ns(value = nil)
|
27
|
+
@job_handler_ns = value if !value.nil?
|
28
|
+
@job_handler_ns
|
29
|
+
end
|
30
|
+
|
31
|
+
def job(name, handler_name)
|
32
|
+
if self.job_handler_ns && !(handler_name =~ /^::/)
|
33
|
+
handler_name = "#{self.job_handler_ns}::#{handler_name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
@routes.push(Qs::Route.new(name, handler_name))
|
37
|
+
end
|
38
|
+
|
39
|
+
def enqueue(job_name, params = nil)
|
40
|
+
Qs.enqueue(self, job_name, params)
|
41
|
+
end
|
42
|
+
alias :add :enqueue
|
43
|
+
|
44
|
+
def reset!
|
45
|
+
self.enqueued_jobs.clear
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
reference = '0x0%x' % (self.object_id << 1)
|
50
|
+
"#<#{self.class}:#{reference} " \
|
51
|
+
"@name=#{self.name.inspect} " \
|
52
|
+
"@job_handler_ns=#{self.job_handler_ns.inspect}>"
|
53
|
+
end
|
54
|
+
|
55
|
+
InvalidError = Class.new(RuntimeError)
|
56
|
+
|
57
|
+
module RedisKey
|
58
|
+
def self.parse_name(key)
|
59
|
+
key.split(':').last
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.new(name)
|
63
|
+
"queues:#{name}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Qs
|
2
|
+
|
3
|
+
class RedisItem
|
4
|
+
|
5
|
+
attr_reader :queue_redis_key, :serialized_payload
|
6
|
+
attr_accessor :started, :finished
|
7
|
+
attr_accessor :job, :handler_class
|
8
|
+
attr_accessor :exception, :time_taken
|
9
|
+
|
10
|
+
def initialize(queue_redis_key, serialized_payload)
|
11
|
+
@queue_redis_key = queue_redis_key
|
12
|
+
@serialized_payload = serialized_payload
|
13
|
+
@started = false
|
14
|
+
@finished = false
|
15
|
+
|
16
|
+
@job = nil
|
17
|
+
@handler_class = nil
|
18
|
+
@exception = nil
|
19
|
+
@time_taken = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
if other.kind_of?(self.class)
|
24
|
+
self.queue_redis_key == other.queue_redis_key &&
|
25
|
+
self.serialized_payload == other.serialized_payload
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/qs/route.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'qs/qs_runner'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class Route
|
6
|
+
|
7
|
+
attr_reader :name, :handler_class_name, :handler_class
|
8
|
+
|
9
|
+
def initialize(name, handler_class_name)
|
10
|
+
@name = name.to_s
|
11
|
+
@handler_class_name = handler_class_name
|
12
|
+
@handler_class = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
@handler_class = constantize_handler_class(@handler_class_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(job, daemon_data)
|
20
|
+
QsRunner.new(self.handler_class, {
|
21
|
+
:job => job,
|
22
|
+
:params => job.params,
|
23
|
+
:logger => daemon_data.logger
|
24
|
+
}).run
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def constantize_handler_class(handler_class_name)
|
30
|
+
constantize(handler_class_name).tap do |handler_class|
|
31
|
+
raise(NoHandlerClassError.new(handler_class_name)) if !handler_class
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def constantize(class_name)
|
36
|
+
names = class_name.to_s.split('::').reject{ |name| name.empty? }
|
37
|
+
klass = names.inject(Object){ |constant, name| constant.const_get(name) }
|
38
|
+
klass == Object ? false : klass
|
39
|
+
rescue NameError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class NoHandlerClassError < RuntimeError
|
46
|
+
def initialize(handler_class_name)
|
47
|
+
super "Qs couldn't find the handler '#{handler_class_name}'" \
|
48
|
+
" - it doesn't exist or hasn't been required in yet."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/lib/qs/runner.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'qs/logger'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
attr_reader :handler_class, :handler
|
8
|
+
attr_reader :job, :params, :logger
|
9
|
+
|
10
|
+
def initialize(handler_class, args = nil)
|
11
|
+
@handler_class = handler_class
|
12
|
+
@handler = @handler_class.new(self)
|
13
|
+
|
14
|
+
a = args || {}
|
15
|
+
@job = a[:job]
|
16
|
+
@params = a[:params] || {}
|
17
|
+
@logger = a[:logger] || Qs::NullLogger.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'qs/test_runner'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
module TestHelpers
|
6
|
+
|
7
|
+
def test_runner(handler_class, args = nil)
|
8
|
+
TestRunner.new(handler_class, args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_handler(handler_class, args = nil)
|
12
|
+
test_runner(handler_class, args).handler
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'qs'
|
2
|
+
require 'qs/job'
|
3
|
+
require 'qs/job_handler'
|
4
|
+
require 'qs/runner'
|
5
|
+
|
6
|
+
module Qs
|
7
|
+
|
8
|
+
InvalidJobHandlerError = Class.new(StandardError)
|
9
|
+
|
10
|
+
class TestRunner < Runner
|
11
|
+
|
12
|
+
def initialize(handler_class, args = nil)
|
13
|
+
if !handler_class.include?(Qs::JobHandler)
|
14
|
+
raise InvalidJobHandlerError, "#{handler_class.inspect} is not a"\
|
15
|
+
" Qs::JobHandler"
|
16
|
+
end
|
17
|
+
args = (args || {}).dup
|
18
|
+
super(handler_class, {
|
19
|
+
:job => args.delete(:job),
|
20
|
+
:params => normalize_params(args.delete(:params) || {}),
|
21
|
+
:logger => args.delete(:logger)
|
22
|
+
})
|
23
|
+
args.each{ |key, value| self.handler.send("#{key}=", value) }
|
24
|
+
|
25
|
+
self.handler.init
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
self.handler.run
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Stringify and serialize/deserialize to ensure params are valid and are
|
35
|
+
# in the format they would normally be when a handler is built and run.
|
36
|
+
def normalize_params(params)
|
37
|
+
params = Job::StringifyParams.new(params)
|
38
|
+
Qs.deserialize(Qs.serialize(params))
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/qs/version.rb
CHANGED