qs 0.0.1 → 0.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.
- 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