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
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/bench/config.qs
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'qs'
|
2
|
+
require 'bench/queue'
|
3
|
+
|
4
|
+
ROOT_PATH = Pathname.new(File.expand_path('../..', __FILE__))
|
5
|
+
|
6
|
+
LOGGER = if ENV['BENCH_REPORT']
|
7
|
+
Logger.new(ROOT_PATH.join('log/bench_daemon.log').to_s)
|
8
|
+
else
|
9
|
+
Logger.new(STDOUT)
|
10
|
+
end
|
11
|
+
|
12
|
+
PROGRESS_IO = if ENV['BENCH_PROGRESS_IO']
|
13
|
+
::IO.for_fd(ENV['BENCH_PROGRESS_IO'].to_i)
|
14
|
+
else
|
15
|
+
File.open('/dev/null', 'w')
|
16
|
+
end
|
17
|
+
|
18
|
+
class BenchDaemon
|
19
|
+
include Qs::Daemon
|
20
|
+
|
21
|
+
name 'bench'
|
22
|
+
pid_file ROOT_PATH.join('tmp/bench_daemon.pid').to_s
|
23
|
+
|
24
|
+
logger LOGGER
|
25
|
+
verbose_logging false
|
26
|
+
|
27
|
+
queue BenchQueue
|
28
|
+
|
29
|
+
# if jobs fail notify the bench report so it doesn't hang forever on IO.select
|
30
|
+
error do |exception, daemon_data, job|
|
31
|
+
PROGRESS_IO.write_nonblock('F')
|
32
|
+
end
|
33
|
+
|
34
|
+
class Multiply
|
35
|
+
include Qs::JobHandler
|
36
|
+
|
37
|
+
after{ PROGRESS_IO.write_nonblock('.') }
|
38
|
+
|
39
|
+
def run!
|
40
|
+
'a' * params['size']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
run BenchDaemon.new
|
data/bench/queue.rb
ADDED
data/bench/report.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'scmd'
|
3
|
+
require 'bench/queue'
|
4
|
+
|
5
|
+
class BenchRunner
|
6
|
+
|
7
|
+
TIME_MODIFIER = 10 ** 4 # 4 decimal places
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
output_file_path = if ENV['OUTPUT_FILE']
|
11
|
+
File.expand_path(ENV['OUTPUT_FILE'])
|
12
|
+
else
|
13
|
+
File.expand_path('../report.txt', __FILE__)
|
14
|
+
end
|
15
|
+
@output_file = File.open(output_file_path, 'w')
|
16
|
+
|
17
|
+
@number_of_jobs = ENV['NUM_JOBS'] || 10_000
|
18
|
+
@job_name = 'multiply'
|
19
|
+
@job_params = { 'size' => 100_000 }
|
20
|
+
|
21
|
+
@progress_reader, @progress_writer = IO.pipe
|
22
|
+
|
23
|
+
@results = {}
|
24
|
+
|
25
|
+
Qs.init
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
output "Running benchmark report..."
|
30
|
+
output("\n", false)
|
31
|
+
|
32
|
+
benchmark_adding_jobs
|
33
|
+
benchmark_running_jobs
|
34
|
+
|
35
|
+
size = @results.values.map(&:size).max
|
36
|
+
output "Adding #{@number_of_jobs} Jobs Time: #{@results[:adding_jobs].rjust(size)}s"
|
37
|
+
output "Running #{@number_of_jobs} Jobs Time: #{@results[:running_jobs].rjust(size)}s"
|
38
|
+
|
39
|
+
output "\n"
|
40
|
+
output "Done running benchmark report"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def benchmark_adding_jobs
|
46
|
+
output "Adding jobs"
|
47
|
+
benchmark = Benchmark.measure do
|
48
|
+
(1..@number_of_jobs).each do |n|
|
49
|
+
BenchQueue.add(@job_name, @job_params)
|
50
|
+
output('.', false) if ((n - 1) % 100 == 0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@results[:adding_jobs] = round_and_display(benchmark.real)
|
54
|
+
output("\n", false)
|
55
|
+
end
|
56
|
+
|
57
|
+
def benchmark_running_jobs
|
58
|
+
cmd_str = "bundle exec ./bin/qs bench/config.qs"
|
59
|
+
cmd = Scmd.new(cmd_str, {
|
60
|
+
'BENCH_REPORT' => 'yes',
|
61
|
+
'BENCH_PROGRESS_IO' => @progress_writer.fileno
|
62
|
+
})
|
63
|
+
|
64
|
+
output "Running jobs"
|
65
|
+
begin
|
66
|
+
benchmark = Benchmark.measure do
|
67
|
+
cmd.start
|
68
|
+
if !cmd.running?
|
69
|
+
raise "failed to start qs process: #{cmd_str.inspect}"
|
70
|
+
end
|
71
|
+
|
72
|
+
progress = 0
|
73
|
+
while progress < @number_of_jobs
|
74
|
+
::IO.select([@progress_reader])
|
75
|
+
result = @progress_reader.read_nonblock(1)
|
76
|
+
progress += 1
|
77
|
+
output(result, false) if ((progress - 1) % 100 == 0)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@results[:running_jobs] = round_and_display(benchmark.real)
|
81
|
+
output("\n", false)
|
82
|
+
ensure
|
83
|
+
cmd.kill('TERM')
|
84
|
+
cmd.wait(5)
|
85
|
+
end
|
86
|
+
|
87
|
+
output("\n", false)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def output(message, puts = true)
|
93
|
+
method = puts ? :puts : :print
|
94
|
+
self.send(method, message)
|
95
|
+
@output_file.send(method, message)
|
96
|
+
STDOUT.flush if method == :print
|
97
|
+
end
|
98
|
+
|
99
|
+
def round_and_display(time_in_ms)
|
100
|
+
display_time(round_time(time_in_ms))
|
101
|
+
end
|
102
|
+
|
103
|
+
def round_time(time_in_ms)
|
104
|
+
(time_in_ms * TIME_MODIFIER).to_i / TIME_MODIFIER.to_f
|
105
|
+
end
|
106
|
+
|
107
|
+
def display_time(time)
|
108
|
+
integer, fractional = time.to_s.split('.')
|
109
|
+
[ integer, fractional.ljust(4, '0') ].join('.')
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
BenchRunner.new.run
|
data/bench/report.txt
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Running benchmark report...
|
2
|
+
|
3
|
+
Adding jobs
|
4
|
+
....................................................................................................
|
5
|
+
Running jobs
|
6
|
+
....................................................................................................
|
7
|
+
|
8
|
+
Adding 10000 Jobs Time: 1.6041s
|
9
|
+
Running 10000 Jobs Time: 12.3159s
|
10
|
+
|
11
|
+
Done running benchmark report
|
data/bin/qs
ADDED
data/lib/qs/cli.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'qs'
|
2
|
+
require 'qs/config_file'
|
3
|
+
require 'qs/process'
|
4
|
+
require 'qs/process_signal'
|
5
|
+
require 'qs/version'
|
6
|
+
|
7
|
+
module Qs
|
8
|
+
|
9
|
+
class CLI
|
10
|
+
|
11
|
+
def self.run(args)
|
12
|
+
self.new.run(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(kernel = nil)
|
16
|
+
@kernel = kernel || Kernel
|
17
|
+
@cli = CLIRB.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(*args)
|
21
|
+
begin
|
22
|
+
run!(*args)
|
23
|
+
rescue CLIRB::HelpExit
|
24
|
+
@kernel.puts help
|
25
|
+
rescue CLIRB::VersionExit
|
26
|
+
@kernel.puts Qs::VERSION
|
27
|
+
rescue CLIRB::Error, Qs::ConfigFile::InvalidError => exception
|
28
|
+
@kernel.puts "#{exception.message}\n\n"
|
29
|
+
@kernel.puts help
|
30
|
+
@kernel.exit 1
|
31
|
+
rescue StandardError => exception
|
32
|
+
@kernel.puts "#{exception.class}: #{exception.message}"
|
33
|
+
@kernel.puts exception.backtrace.join("\n")
|
34
|
+
@kernel.exit 1
|
35
|
+
end
|
36
|
+
@kernel.exit 0
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run!(*args)
|
42
|
+
@cli.parse!(args)
|
43
|
+
config_file_path, command = @cli.args
|
44
|
+
config_file_path ||= 'config.qs'
|
45
|
+
command ||= 'run'
|
46
|
+
daemon = Qs::ConfigFile.new(config_file_path).daemon
|
47
|
+
case(command)
|
48
|
+
when 'run'
|
49
|
+
Qs::Process.new(daemon, :daemonize => false).run
|
50
|
+
when 'start'
|
51
|
+
Qs::Process.new(daemon, :daemonize => true).run
|
52
|
+
when 'stop'
|
53
|
+
Qs::ProcessSignal.new(daemon, 'TERM').send
|
54
|
+
when 'restart'
|
55
|
+
Qs::ProcessSignal.new(daemon, 'USR2').send
|
56
|
+
else
|
57
|
+
raise CLIRB::Error, "#{command.inspect} is not a valid command"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def help
|
62
|
+
"Usage: qs [CONFIG_FILE] [COMMAND]\n\n" \
|
63
|
+
"Commands: run, start, stop, restart\n" \
|
64
|
+
"Options: #{@cli}"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
class CLIRB # Version 1.0.0, https://github.com/redding/cli.rb
|
70
|
+
Error = Class.new(RuntimeError);
|
71
|
+
HelpExit = Class.new(RuntimeError); VersionExit = Class.new(RuntimeError)
|
72
|
+
attr_reader :argv, :args, :opts, :data
|
73
|
+
|
74
|
+
def initialize(&block)
|
75
|
+
@options = []; instance_eval(&block) if block
|
76
|
+
require 'optparse'
|
77
|
+
@data, @args, @opts = [], [], {}; @parser = OptionParser.new do |p|
|
78
|
+
p.banner = ''; @options.each do |o|
|
79
|
+
@opts[o.name] = o.value; p.on(*o.parser_args){ |v| @opts[o.name] = v }
|
80
|
+
end
|
81
|
+
p.on_tail('--version', ''){ |v| raise VersionExit, v.to_s }
|
82
|
+
p.on_tail('--help', ''){ |v| raise HelpExit, v.to_s }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def option(*args); @options << Option.new(*args); end
|
87
|
+
def parse!(argv)
|
88
|
+
@args = (argv || []).dup.tap do |args_list|
|
89
|
+
begin; @parser.parse!(args_list)
|
90
|
+
rescue OptionParser::ParseError => err; raise Error, err.message; end
|
91
|
+
end; @data = @args + [@opts]
|
92
|
+
end
|
93
|
+
def to_s; @parser.to_s; end
|
94
|
+
def inspect
|
95
|
+
"#<#{self.class}:#{'0x0%x' % (object_id << 1)} @data=#{@data.inspect}>"
|
96
|
+
end
|
97
|
+
|
98
|
+
class Option
|
99
|
+
attr_reader :name, :opt_name, :desc, :abbrev, :value, :klass, :parser_args
|
100
|
+
|
101
|
+
def initialize(name, *args)
|
102
|
+
settings, @desc = args.last.kind_of?(::Hash) ? args.pop : {}, args.pop || ''
|
103
|
+
@name, @opt_name, @abbrev = parse_name_values(name, settings[:abbrev])
|
104
|
+
@value, @klass = gvalinfo(settings[:value])
|
105
|
+
@parser_args = if [TrueClass, FalseClass, NilClass].include?(@klass)
|
106
|
+
["-#{@abbrev}", "--[no-]#{@opt_name}", @desc]
|
107
|
+
else
|
108
|
+
["-#{@abbrev}", "--#{@opt_name} #{@opt_name.upcase}", @klass, @desc]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def parse_name_values(name, custom_abbrev)
|
115
|
+
[ (processed_name = name.to_s.strip.downcase), processed_name.gsub('_', '-'),
|
116
|
+
custom_abbrev || processed_name.gsub(/[^a-z]/, '').chars.first || 'a'
|
117
|
+
]
|
118
|
+
end
|
119
|
+
def gvalinfo(v); v.kind_of?(Class) ? [nil,gklass(v)] : [v,gklass(v.class)]; end
|
120
|
+
def gklass(k); k == Fixnum ? Integer : k; end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
data/lib/qs/client.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'hella-redis'
|
2
|
+
require 'qs'
|
3
|
+
require 'qs/job'
|
4
|
+
require 'qs/queue'
|
5
|
+
|
6
|
+
module Qs
|
7
|
+
|
8
|
+
module Client
|
9
|
+
|
10
|
+
def self.new(*args)
|
11
|
+
if !ENV['QS_TEST_MODE']
|
12
|
+
QsClient.new(*args)
|
13
|
+
else
|
14
|
+
TestClient.new(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(klass)
|
19
|
+
klass.class_eval do
|
20
|
+
include InstanceMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
|
26
|
+
attr_reader :redis_config, :redis
|
27
|
+
|
28
|
+
def initialize(redis_config)
|
29
|
+
@redis_config = redis_config
|
30
|
+
end
|
31
|
+
|
32
|
+
def enqueue(queue, job_name, params = nil)
|
33
|
+
job = Qs::Job.new(job_name, params || {})
|
34
|
+
enqueue!(queue, job)
|
35
|
+
job
|
36
|
+
end
|
37
|
+
|
38
|
+
def push(queue_name, payload)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
def block_dequeue(*args)
|
43
|
+
self.redis.with{ |c| c.brpop(*args) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def append(queue_redis_key, serialized_payload)
|
47
|
+
self.redis.with{ |c| c.lpush(queue_redis_key, serialized_payload) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepend(queue_redis_key, serialized_payload)
|
51
|
+
self.redis.with{ |c| c.rpush(queue_redis_key, serialized_payload) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear(redis_key)
|
55
|
+
self.redis.with{ |c| c.del(redis_key) }
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class QsClient
|
63
|
+
include Client
|
64
|
+
|
65
|
+
def initialize(*args)
|
66
|
+
super
|
67
|
+
@redis = HellaRedis::Connection.new(self.redis_config)
|
68
|
+
end
|
69
|
+
|
70
|
+
def push(queue_name, payload)
|
71
|
+
queue_redis_key = Queue::RedisKey.new(queue_name)
|
72
|
+
serialized_payload = Qs.serialize(payload)
|
73
|
+
self.append(queue_redis_key, serialized_payload)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def enqueue!(queue, job)
|
79
|
+
serialized_payload = Qs.serialize(job.to_payload)
|
80
|
+
self.append(queue.redis_key, serialized_payload)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class TestClient
|
86
|
+
include Client
|
87
|
+
|
88
|
+
attr_reader :pushed_items
|
89
|
+
|
90
|
+
def initialize(*args)
|
91
|
+
super
|
92
|
+
require 'hella-redis/connection_spy'
|
93
|
+
@redis = HellaRedis::ConnectionSpy.new(self.redis_config)
|
94
|
+
@pushed_items = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def push(queue_name, payload)
|
98
|
+
# attempt to serialize (and then throw away) the payload, this will error
|
99
|
+
# on the developer if it can't be serialized
|
100
|
+
Qs.serialize(payload)
|
101
|
+
@pushed_items << PushedItem.new(queue_name, payload)
|
102
|
+
end
|
103
|
+
|
104
|
+
def reset!
|
105
|
+
@pushed_items.clear
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def enqueue!(queue, job)
|
111
|
+
# attempt to serialize (and then throw away) the job payload, this will
|
112
|
+
# error on the developer if it can't serialize the job
|
113
|
+
Qs.serialize(job.to_payload)
|
114
|
+
queue.enqueued_jobs << job
|
115
|
+
end
|
116
|
+
|
117
|
+
PushedItem = Struct.new(:queue_name, :payload)
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'qs/daemon'
|
2
|
+
|
3
|
+
module Qs
|
4
|
+
|
5
|
+
class ConfigFile
|
6
|
+
|
7
|
+
attr_reader :daemon
|
8
|
+
|
9
|
+
def initialize(file_path)
|
10
|
+
@file_path = build_file_path(file_path)
|
11
|
+
@daemon = nil
|
12
|
+
evaluate_file(@file_path)
|
13
|
+
validate!
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(daemon)
|
17
|
+
@daemon = daemon
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
if !@daemon.kind_of?(Qs::Daemon)
|
24
|
+
raise NoDaemonError.new(@daemon, @file_path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_file_path(path)
|
29
|
+
full_path = File.expand_path(path)
|
30
|
+
raise NoConfigFileError.new(full_path) unless File.exists?(full_path)
|
31
|
+
full_path
|
32
|
+
rescue NoConfigFileError
|
33
|
+
full_path_with_sanford = "#{full_path}.qs"
|
34
|
+
raise unless File.exists?(full_path_with_sanford)
|
35
|
+
full_path_with_sanford
|
36
|
+
end
|
37
|
+
|
38
|
+
# This evaluates the file and creates a proc using it's contents. This is
|
39
|
+
# a trick borrowed from Rack. It is essentially converting a file into a
|
40
|
+
# proc and then instance eval'ing it. This has a couple benefits:
|
41
|
+
# * The obvious benefit is the file is evaluated in the context of this
|
42
|
+
# class. This allows the file to call `run`, setting the daemon that
|
43
|
+
# will be used.
|
44
|
+
# * The other benefit is that the file's contents behave like they were a
|
45
|
+
# proc defined by the user. Instance eval'ing the file directly, makes
|
46
|
+
# any constants (modules/classes) defined in it namespaced by the
|
47
|
+
# instance of the config (not namespaced by `Qs::ConfigFile`,
|
48
|
+
# they are actually namespaced by an instance of this class, its like
|
49
|
+
# accessing it via `ConfigFile.new::MyDaemon`), which is very confusing.
|
50
|
+
# Thus, the proc is created and eval'd using the `TOPLEVEL_BINDING`,
|
51
|
+
# which defines the constants at the top-level, as would be expected.
|
52
|
+
def evaluate_file(file_path)
|
53
|
+
config_file_code = "proc{ #{File.read(file_path)} }"
|
54
|
+
config_file_proc = eval(config_file_code, TOPLEVEL_BINDING, file_path, 0)
|
55
|
+
self.instance_eval(&config_file_proc)
|
56
|
+
end
|
57
|
+
|
58
|
+
InvalidError = Class.new(StandardError)
|
59
|
+
|
60
|
+
class NoConfigFileError < InvalidError
|
61
|
+
def initialize(path)
|
62
|
+
super "A configuration file couldn't be found at: #{path.to_s.inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class NoDaemonError < InvalidError
|
67
|
+
def initialize(daemon, path)
|
68
|
+
prefix = "Configuration file #{path.to_s.inspect}"
|
69
|
+
if daemon
|
70
|
+
super "#{prefix} called `run` without a Qs::Daemon"
|
71
|
+
else
|
72
|
+
super "#{prefix} didn't call `run` with a Qs::Daemon"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|