qs 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bench/config.qs +1 -0
- data/bench/report.txt +2 -2
- data/lib/qs/daemon.rb +46 -50
- data/lib/qs/io_pipe.rb +41 -0
- data/lib/qs/process.rb +65 -31
- data/lib/qs/version.rb +1 -1
- data/test/support/app_daemon.rb +4 -1
- data/test/unit/daemon_tests.rb +62 -48
- data/test/unit/io_pipe_tests.rb +76 -0
- data/test/unit/process_tests.rb +149 -101
- metadata +7 -4
data/bench/config.qs
CHANGED
data/bench/report.txt
CHANGED
@@ -5,7 +5,7 @@ Adding jobs
|
|
5
5
|
Running jobs
|
6
6
|
....................................................................................................
|
7
7
|
|
8
|
-
Adding 10000 Jobs Time: 1.
|
9
|
-
Running 10000 Jobs Time: 12.
|
8
|
+
Adding 10000 Jobs Time: 1.6155s
|
9
|
+
Running 10000 Jobs Time: 12.4848s
|
10
10
|
|
11
11
|
Done running benchmark report
|
data/lib/qs/daemon.rb
CHANGED
@@ -6,6 +6,7 @@ require 'thread'
|
|
6
6
|
require 'qs'
|
7
7
|
require 'qs/client'
|
8
8
|
require 'qs/daemon_data'
|
9
|
+
require 'qs/io_pipe'
|
9
10
|
require 'qs/logger'
|
10
11
|
require 'qs/payload_handler'
|
11
12
|
require 'qs/redis_item'
|
@@ -16,6 +17,8 @@ module Qs
|
|
16
17
|
|
17
18
|
InvalidError = Class.new(ArgumentError)
|
18
19
|
|
20
|
+
SIGNAL = '.'.freeze
|
21
|
+
|
19
22
|
def self.included(klass)
|
20
23
|
klass.class_eval do
|
21
24
|
extend ClassMethods
|
@@ -94,20 +97,21 @@ module Qs
|
|
94
97
|
end
|
95
98
|
|
96
99
|
def work_loop
|
97
|
-
|
100
|
+
log "Starting work loop", :debug
|
98
101
|
setup_redis_and_ios
|
99
102
|
@worker_pool = build_worker_pool
|
100
103
|
process_inputs while @signal.start?
|
101
|
-
|
102
|
-
shutdown_worker_pool
|
104
|
+
log "Stopping work loop", :debug
|
103
105
|
rescue StandardError => exception
|
104
|
-
|
105
|
-
|
106
|
-
|
106
|
+
@signal.set :stop
|
107
|
+
log "Error occurred while running the daemon, exiting", :error
|
108
|
+
log "#{exception.class}: #{exception.message}", :error
|
109
|
+
log exception.backtrace.join("\n"), :error
|
107
110
|
ensure
|
111
|
+
shutdown_worker_pool
|
108
112
|
@worker_available_io.teardown
|
109
113
|
@work_loop_thread = nil
|
110
|
-
|
114
|
+
log "Stopped work loop", :debug
|
111
115
|
end
|
112
116
|
|
113
117
|
def setup_redis_and_ios
|
@@ -122,9 +126,9 @@ module Qs
|
|
122
126
|
self.daemon_data.max_workers
|
123
127
|
){ |redis_item| process(redis_item) }
|
124
128
|
wp.on_worker_error do |worker, exception, redis_item|
|
125
|
-
handle_worker_exception(redis_item)
|
129
|
+
handle_worker_exception(exception, redis_item)
|
126
130
|
end
|
127
|
-
wp.on_worker_sleep{ @worker_available_io.
|
131
|
+
wp.on_worker_sleep{ @worker_available_io.write(SIGNAL) }
|
128
132
|
wp.start
|
129
133
|
wp
|
130
134
|
end
|
@@ -134,27 +138,42 @@ module Qs
|
|
134
138
|
# shuffling we ensure they are randomly ordered so every queue should
|
135
139
|
# get a chance.
|
136
140
|
# * Use 0 for the brpop timeout which means block indefinitely.
|
141
|
+
# * Rescue runtime errors so the daemon thread doesn't fail if redis is
|
142
|
+
# temporarily down. Sleep for a second to keep the thread from thrashing
|
143
|
+
# by repeatedly erroring if redis is down.
|
137
144
|
def process_inputs
|
138
145
|
wait_for_available_worker
|
139
146
|
return unless @worker_pool.worker_available? && @signal.start?
|
140
147
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
148
|
+
begin
|
149
|
+
args = [self.signals_redis_key, self.queue_redis_keys.shuffle, 0].flatten
|
150
|
+
redis_key, serialized_payload = @client.block_dequeue(*args)
|
151
|
+
if redis_key != @signals_redis_key
|
152
|
+
@worker_pool.add_work(RedisItem.new(redis_key, serialized_payload))
|
153
|
+
end
|
154
|
+
rescue RuntimeError => exception
|
155
|
+
log "Error dequeueing #{exception.message.inspect}", :error
|
156
|
+
log exception.backtrace.join("\n"), :error
|
157
|
+
sleep 1
|
145
158
|
end
|
146
159
|
end
|
147
160
|
|
148
161
|
def wait_for_available_worker
|
149
162
|
if !@worker_pool.worker_available? && @signal.start?
|
150
|
-
@worker_available_io.wait
|
163
|
+
@worker_available_io.wait.read
|
151
164
|
end
|
152
165
|
end
|
153
166
|
|
154
167
|
def shutdown_worker_pool
|
155
|
-
|
168
|
+
return unless @worker_pool
|
156
169
|
timeout = @signal.stop? ? self.daemon_data.shutdown_timeout : 0
|
170
|
+
if timeout
|
171
|
+
log "Shutting down, waiting up to #{timeout} seconds for work to finish"
|
172
|
+
else
|
173
|
+
log "Shutting down, waiting for work to finish"
|
174
|
+
end
|
157
175
|
@worker_pool.shutdown(timeout)
|
176
|
+
log "Requeueing #{@worker_pool.work_items.size} job(s)"
|
158
177
|
@worker_pool.work_items.each do |ri|
|
159
178
|
@client.prepend(ri.queue_redis_key, ri.serialized_payload)
|
160
179
|
end
|
@@ -165,8 +184,8 @@ module Qs
|
|
165
184
|
end
|
166
185
|
|
167
186
|
def wakeup_work_loop_thread
|
168
|
-
@client.append(self.signals_redis_key,
|
169
|
-
@worker_available_io.
|
187
|
+
@client.append(self.signals_redis_key, SIGNAL)
|
188
|
+
@worker_available_io.write(SIGNAL)
|
170
189
|
end
|
171
190
|
|
172
191
|
# * This only catches errors that happen outside of running the payload
|
@@ -177,11 +196,20 @@ module Qs
|
|
177
196
|
# * If we never started processing the redis item, its safe to requeue it.
|
178
197
|
# Otherwise it happened while processing so the payload handler caught
|
179
198
|
# it or it happened after the payload handler which we don't care about.
|
180
|
-
def handle_worker_exception(redis_item)
|
199
|
+
def handle_worker_exception(exception, redis_item)
|
181
200
|
return if redis_item.nil?
|
182
201
|
if !redis_item.started
|
202
|
+
log "Worker error, requeueing job because it hasn't started", :error
|
183
203
|
@client.prepend(redis_item.queue_redis_key, redis_item.serialized_payload)
|
204
|
+
else
|
205
|
+
log "Worker error after job was processed, ignoring", :error
|
184
206
|
end
|
207
|
+
log "#{exception.class}: #{exception.message}", :error
|
208
|
+
log exception.backtrace.join("\n"), :error
|
209
|
+
end
|
210
|
+
|
211
|
+
def log(message, level = :info)
|
212
|
+
self.logger.send(level, "[Qs] #{message}")
|
185
213
|
end
|
186
214
|
|
187
215
|
end
|
@@ -290,38 +318,6 @@ module Qs
|
|
290
318
|
end
|
291
319
|
end
|
292
320
|
|
293
|
-
class IOPipe
|
294
|
-
NULL = File.open('/dev/null', 'w')
|
295
|
-
SIGNAL = '.'.freeze
|
296
|
-
|
297
|
-
attr_reader :reader, :writer
|
298
|
-
|
299
|
-
def initialize
|
300
|
-
@reader = NULL
|
301
|
-
@writer = NULL
|
302
|
-
end
|
303
|
-
|
304
|
-
def wait
|
305
|
-
::IO.select([@reader])
|
306
|
-
@reader.read_nonblock(SIGNAL.bytesize)
|
307
|
-
end
|
308
|
-
|
309
|
-
def signal
|
310
|
-
@writer.write_nonblock(SIGNAL)
|
311
|
-
end
|
312
|
-
|
313
|
-
def setup
|
314
|
-
@reader, @writer = ::IO.pipe
|
315
|
-
end
|
316
|
-
|
317
|
-
def teardown
|
318
|
-
@reader.close unless @reader === NULL
|
319
|
-
@writer.close unless @writer === NULL
|
320
|
-
@reader = NULL
|
321
|
-
@writer = NULL
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
321
|
class Signal
|
326
322
|
def initialize(value)
|
327
323
|
@value = value
|
data/lib/qs/io_pipe.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Qs
|
2
|
+
|
3
|
+
class IOPipe
|
4
|
+
|
5
|
+
NULL = File.open('/dev/null', 'w')
|
6
|
+
NUMBER_OF_BYTES = 1
|
7
|
+
|
8
|
+
attr_reader :reader, :writer
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@reader = NULL
|
12
|
+
@writer = NULL
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup
|
16
|
+
@reader, @writer = ::IO.pipe
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown
|
20
|
+
@reader.close unless @reader === NULL
|
21
|
+
@writer.close unless @writer === NULL
|
22
|
+
@reader = NULL
|
23
|
+
@writer = NULL
|
24
|
+
end
|
25
|
+
|
26
|
+
def read
|
27
|
+
@reader.read_nonblock(NUMBER_OF_BYTES)
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(value)
|
31
|
+
@writer.write_nonblock(value[0, NUMBER_OF_BYTES])
|
32
|
+
end
|
33
|
+
|
34
|
+
def wait
|
35
|
+
::IO.select([@reader])
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/lib/qs/process.rb
CHANGED
@@ -1,73 +1,107 @@
|
|
1
|
+
require 'qs/io_pipe'
|
1
2
|
require 'qs/pid_file'
|
2
3
|
|
3
4
|
module Qs
|
4
5
|
|
5
6
|
class Process
|
6
7
|
|
7
|
-
|
8
|
+
HALT = 'H'.freeze
|
9
|
+
STOP = 'S'.freeze
|
10
|
+
RESTART = 'R'.freeze
|
11
|
+
|
12
|
+
attr_reader :daemon, :name
|
13
|
+
attr_reader :pid_file, :signal_io, :restart_cmd
|
8
14
|
|
9
15
|
def initialize(daemon, options = nil)
|
10
16
|
options ||= {}
|
11
17
|
@daemon = daemon
|
18
|
+
process_label = ignore_if_blank(ENV['QS_PROCESS_LABEL']) || @daemon.name
|
19
|
+
@name = "qs: #{process_label}"
|
12
20
|
@logger = @daemon.logger
|
13
|
-
@pid_file = PIDFile.new(@daemon.pid_file)
|
14
|
-
@restart_cmd = RestartCmd.new
|
15
21
|
|
16
|
-
@
|
22
|
+
@pid_file = PIDFile.new(@daemon.pid_file)
|
23
|
+
@signal_io = IOPipe.new
|
24
|
+
@restart_cmd = RestartCmd.new
|
17
25
|
|
18
|
-
|
19
|
-
@
|
20
|
-
@restart = false
|
26
|
+
skip_daemonize = ignore_if_blank(ENV['QS_SKIP_DAEMONIZE'])
|
27
|
+
@daemonize = !!options[:daemonize] && !skip_daemonize
|
21
28
|
end
|
22
29
|
|
23
30
|
def run
|
24
31
|
::Process.daemon(true) if self.daemonize?
|
25
|
-
log "Starting Qs daemon for #{@daemon.name}
|
32
|
+
log "Starting Qs daemon for #{@daemon.name}"
|
26
33
|
|
27
34
|
$0 = @name
|
28
35
|
@pid_file.write
|
29
36
|
log "PID: #{@pid_file.pid}"
|
30
37
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
38
|
+
@signal_io.setup
|
39
|
+
trap_signals(@signal_io)
|
40
|
+
|
41
|
+
start_daemon(@daemon)
|
42
|
+
|
43
|
+
signal = catch(:signal) do
|
44
|
+
wait_for_signals(@signal_io, @daemon)
|
36
45
|
end
|
46
|
+
@signal_io.teardown
|
37
47
|
|
38
|
-
|
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."
|
48
|
+
run_restart_cmd(@daemon, @restart_cmd) if signal == RESTART
|
45
49
|
ensure
|
46
50
|
@pid_file.remove
|
47
51
|
end
|
48
52
|
|
49
53
|
def daemonize?
|
50
|
-
@daemonize
|
54
|
+
@daemonize
|
51
55
|
end
|
52
56
|
|
53
|
-
|
54
|
-
|
57
|
+
private
|
58
|
+
|
59
|
+
def start_daemon(daemon)
|
60
|
+
@daemon.start
|
61
|
+
log "#{@daemon.name} daemon started and ready."
|
62
|
+
rescue StandardError => exception
|
63
|
+
log "#{@daemon.name} daemon never started."
|
64
|
+
raise exception
|
55
65
|
end
|
56
66
|
|
57
|
-
|
67
|
+
def trap_signals(signal_io)
|
68
|
+
trap_signal('INT'){ signal_io.write(HALT) }
|
69
|
+
trap_signal('TERM'){ signal_io.write(STOP) }
|
70
|
+
trap_signal('USR2'){ signal_io.write(RESTART) }
|
71
|
+
end
|
58
72
|
|
59
|
-
def
|
60
|
-
|
73
|
+
def trap_signal(signal, &block)
|
74
|
+
::Signal.trap(signal, &block)
|
75
|
+
rescue ArgumentError
|
76
|
+
log "'#{signal}' signal not supported"
|
77
|
+
end
|
78
|
+
|
79
|
+
def wait_for_signals(signal_io, daemon)
|
80
|
+
while signal_io.wait do
|
81
|
+
os_signal = signal_io.read
|
82
|
+
handle_signal(os_signal, daemon)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_signal(signal, daemon)
|
87
|
+
log "Got '#{signal}' signal"
|
88
|
+
case signal
|
89
|
+
when HALT
|
90
|
+
daemon.halt(true)
|
91
|
+
when STOP, RESTART
|
92
|
+
daemon.stop(true)
|
93
|
+
end
|
94
|
+
throw :signal, signal
|
61
95
|
end
|
62
96
|
|
63
|
-
def run_restart_cmd
|
64
|
-
log "Restarting #{
|
97
|
+
def run_restart_cmd(daemon, restart_cmd)
|
98
|
+
log "Restarting #{daemon.name} daemon"
|
65
99
|
ENV['QS_SKIP_DAEMONIZE'] = 'yes'
|
66
|
-
|
100
|
+
restart_cmd.run
|
67
101
|
end
|
68
102
|
|
69
|
-
def
|
70
|
-
|
103
|
+
def log(message)
|
104
|
+
@logger.info "[Qs] #{message}"
|
71
105
|
end
|
72
106
|
|
73
107
|
def ignore_if_blank(value, &block)
|
data/lib/qs/version.rb
CHANGED
data/test/support/app_daemon.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'qs'
|
2
2
|
|
3
|
+
LOGGER = Logger.new(ROOT_PATH.join('log/app_daemon.log').to_s)
|
4
|
+
LOGGER.datetime_format = "" # turn off the datetime in the logs
|
5
|
+
|
3
6
|
AppQueue = Qs::Queue.new do
|
4
7
|
name 'app_main'
|
5
8
|
|
@@ -16,7 +19,7 @@ class AppDaemon
|
|
16
19
|
|
17
20
|
name 'app'
|
18
21
|
|
19
|
-
logger
|
22
|
+
logger LOGGER
|
20
23
|
verbose_logging true
|
21
24
|
|
22
25
|
queue AppQueue
|
data/test/unit/daemon_tests.rb
CHANGED
@@ -286,6 +286,34 @@ module Qs::Daemon
|
|
286
286
|
|
287
287
|
end
|
288
288
|
|
289
|
+
class RunningWithErrorWhileDequeuingTests < InitSetupTests
|
290
|
+
desc "running with an error while dequeueing"
|
291
|
+
setup do
|
292
|
+
@daemon = @daemon_class.new
|
293
|
+
@thread = @daemon.start
|
294
|
+
|
295
|
+
@block_dequeue_calls = 0
|
296
|
+
Assert.stub(@client_spy, :block_dequeue) do
|
297
|
+
@block_dequeue_calls += 1
|
298
|
+
raise RuntimeError
|
299
|
+
end
|
300
|
+
# cause the daemon to loop, its sleeping on the original block_dequeue
|
301
|
+
# call that happened before the stub
|
302
|
+
@client_spy.append(@queue.redis_key, Factory.string)
|
303
|
+
@thread.join(0.1)
|
304
|
+
end
|
305
|
+
subject{ @daemon }
|
306
|
+
|
307
|
+
should "not cause the thread to exit" do
|
308
|
+
assert_true @thread.alive?
|
309
|
+
assert_equal 1, @block_dequeue_calls
|
310
|
+
@thread.join(1)
|
311
|
+
assert_true @thread.alive?
|
312
|
+
assert_equal 2, @block_dequeue_calls
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
289
317
|
class RunningWithMultipleQueuesTests < InitSetupTests
|
290
318
|
desc "running with multiple queues"
|
291
319
|
setup do
|
@@ -460,6 +488,40 @@ module Qs::Daemon
|
|
460
488
|
|
461
489
|
end
|
462
490
|
|
491
|
+
class WorkLoopErrorTests < StartTests
|
492
|
+
desc "with a work loop error"
|
493
|
+
setup do
|
494
|
+
# cause a non-dequeue error
|
495
|
+
Assert.stub(@worker_pool_spy, :worker_available?){ raise RuntimeError }
|
496
|
+
|
497
|
+
# cause the daemon to loop, its sleeping on the original block_dequeue
|
498
|
+
# call that happened before the stub
|
499
|
+
@redis_item = Qs::RedisItem.new(@queue.redis_key, Factory.string)
|
500
|
+
@client_spy.append(@redis_item.queue_redis_key, @redis_item.serialized_payload)
|
501
|
+
end
|
502
|
+
|
503
|
+
should "shutdown the worker pool" do
|
504
|
+
assert_true @worker_pool_spy.shutdown_called
|
505
|
+
assert_equal @daemon_class.shutdown_timeout, @worker_pool_spy.shutdown_timeout
|
506
|
+
end
|
507
|
+
|
508
|
+
should "requeue any work left on the pool" do
|
509
|
+
call = @client_spy.calls.last
|
510
|
+
assert_equal :prepend, call.command
|
511
|
+
assert_equal @redis_item.queue_redis_key, call.args.first
|
512
|
+
assert_equal @redis_item.serialized_payload, call.args.last
|
513
|
+
end
|
514
|
+
|
515
|
+
should "stop the work loop thread" do
|
516
|
+
assert_false @thread.alive?
|
517
|
+
end
|
518
|
+
|
519
|
+
should "not be running" do
|
520
|
+
assert_false subject.running?
|
521
|
+
end
|
522
|
+
|
523
|
+
end
|
524
|
+
|
463
525
|
class ConfigurationTests < UnitTests
|
464
526
|
include NsOptions::AssertMacros
|
465
527
|
|
@@ -565,54 +627,6 @@ module Qs::Daemon
|
|
565
627
|
|
566
628
|
end
|
567
629
|
|
568
|
-
class IOPipeTests < UnitTests
|
569
|
-
desc "IOPipe"
|
570
|
-
setup do
|
571
|
-
@io = IOPipe.new
|
572
|
-
end
|
573
|
-
subject{ @io }
|
574
|
-
|
575
|
-
should have_readers :reader, :writer
|
576
|
-
should have_imeths :wait, :signal
|
577
|
-
should have_imeths :setup, :teardown
|
578
|
-
|
579
|
-
should "default its reader and writer" do
|
580
|
-
assert_same IOPipe::NULL, subject.reader
|
581
|
-
assert_same IOPipe::NULL, subject.writer
|
582
|
-
end
|
583
|
-
|
584
|
-
should "be able to wait until signalled" do
|
585
|
-
subject.setup
|
586
|
-
|
587
|
-
thread = Thread.new{ subject.wait }
|
588
|
-
thread.join(0.1)
|
589
|
-
assert_equal 'sleep', thread.status
|
590
|
-
|
591
|
-
subject.signal
|
592
|
-
thread.join
|
593
|
-
assert_false thread.status
|
594
|
-
end
|
595
|
-
|
596
|
-
should "set its reader and writer to an IO pipe when setup" do
|
597
|
-
subject.setup
|
598
|
-
assert_instance_of ::IO, subject.reader
|
599
|
-
assert_instance_of ::IO, subject.writer
|
600
|
-
end
|
601
|
-
|
602
|
-
should "close its reader/writer and set them to defaults when torn down" do
|
603
|
-
subject.setup
|
604
|
-
reader = subject.reader
|
605
|
-
writer = subject.writer
|
606
|
-
|
607
|
-
subject.teardown
|
608
|
-
assert_true reader.closed?
|
609
|
-
assert_true writer.closed?
|
610
|
-
assert_same IOPipe::NULL, subject.reader
|
611
|
-
assert_same IOPipe::NULL, subject.writer
|
612
|
-
end
|
613
|
-
|
614
|
-
end
|
615
|
-
|
616
630
|
class SignalTests < UnitTests
|
617
631
|
desc "Signal"
|
618
632
|
setup do
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'assert'
|
2
|
+
require 'qs/io_pipe'
|
3
|
+
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
class Qs::IOPipe
|
7
|
+
|
8
|
+
class UnitTests < Assert::Context
|
9
|
+
desc "Qs::IOPipe"
|
10
|
+
setup do
|
11
|
+
@io_pipe = Qs::IOPipe.new
|
12
|
+
end
|
13
|
+
subject{ @io_pipe }
|
14
|
+
|
15
|
+
should have_readers :reader, :writer
|
16
|
+
should have_imeths :setup, :teardown
|
17
|
+
should have_imeths :read, :write, :wait
|
18
|
+
|
19
|
+
should "default its reader and writer" do
|
20
|
+
assert_same NULL, subject.reader
|
21
|
+
assert_same NULL, subject.writer
|
22
|
+
end
|
23
|
+
|
24
|
+
should "change its reader/writer to an IO pipe when setup" do
|
25
|
+
subject.setup
|
26
|
+
assert_not_same NULL, subject.reader
|
27
|
+
assert_not_same NULL, subject.writer
|
28
|
+
assert_instance_of IO, subject.reader
|
29
|
+
assert_instance_of IO, subject.writer
|
30
|
+
end
|
31
|
+
|
32
|
+
should "close its reader/writer and set them to defaults when torn down" do
|
33
|
+
subject.setup
|
34
|
+
reader = subject.reader
|
35
|
+
writer = subject.writer
|
36
|
+
|
37
|
+
subject.teardown
|
38
|
+
assert_true reader.closed?
|
39
|
+
assert_true writer.closed?
|
40
|
+
assert_same NULL, subject.reader
|
41
|
+
assert_same NULL, subject.writer
|
42
|
+
end
|
43
|
+
|
44
|
+
should "be able to read/write values" do
|
45
|
+
subject.setup
|
46
|
+
|
47
|
+
value = Factory.string(NUMBER_OF_BYTES)
|
48
|
+
subject.write(value)
|
49
|
+
assert_equal value, subject.read
|
50
|
+
end
|
51
|
+
|
52
|
+
should "only read/write a fixed number of bytes" do
|
53
|
+
subject.setup
|
54
|
+
|
55
|
+
value = Factory.string
|
56
|
+
subject.write(value)
|
57
|
+
assert_equal value[0, NUMBER_OF_BYTES], subject.read
|
58
|
+
end
|
59
|
+
|
60
|
+
should "be able to wait until there is something to read" do
|
61
|
+
subject.setup
|
62
|
+
|
63
|
+
result = nil
|
64
|
+
thread = Thread.new{ result = subject.wait }
|
65
|
+
thread.join(0.1)
|
66
|
+
assert_equal 'sleep', thread.status
|
67
|
+
|
68
|
+
subject.write(Factory.string)
|
69
|
+
thread.join
|
70
|
+
assert_false thread.status
|
71
|
+
assert_equal subject, result
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/test/unit/process_tests.rb
CHANGED
@@ -18,9 +18,9 @@ class Qs::Process
|
|
18
18
|
class InitTests < UnitTests
|
19
19
|
desc "when init"
|
20
20
|
setup do
|
21
|
-
@
|
21
|
+
@current_env_process_label = ENV['QS_PROCESS_LABEL']
|
22
22
|
@current_env_skip_daemonize = ENV['QS_SKIP_DAEMONIZE']
|
23
|
-
ENV.delete('
|
23
|
+
ENV.delete('QS_PROCESS_LABEL')
|
24
24
|
ENV.delete('QS_SKIP_DAEMONIZE')
|
25
25
|
|
26
26
|
@daemon_spy = DaemonSpy.new
|
@@ -37,34 +37,36 @@ class Qs::Process
|
|
37
37
|
end
|
38
38
|
teardown do
|
39
39
|
ENV['QS_SKIP_DAEMONIZE'] = @current_env_skip_daemonize
|
40
|
-
ENV['
|
40
|
+
ENV['QS_PROCESS_LABEL'] = @current_env_process_label
|
41
41
|
end
|
42
42
|
subject{ @process }
|
43
43
|
|
44
|
-
should have_readers :daemon, :name
|
45
|
-
should
|
44
|
+
should have_readers :daemon, :name
|
45
|
+
should have_readers :pid_file, :signal_io, :restart_cmd
|
46
|
+
should have_imeths :run, :daemonize?
|
46
47
|
|
47
48
|
should "know its daemon" do
|
48
49
|
assert_equal @daemon_spy, subject.daemon
|
49
50
|
end
|
50
51
|
|
51
|
-
should "know its name, pid file and restart cmd" do
|
52
|
-
assert_equal "qs
|
52
|
+
should "know its name, pid file, signal io and restart cmd" do
|
53
|
+
assert_equal "qs: #{@daemon_spy.name}", subject.name
|
53
54
|
assert_equal @pid_file_spy, subject.pid_file
|
55
|
+
assert_instance_of Qs::IOPipe, subject.signal_io
|
54
56
|
assert_equal @restart_cmd_spy, subject.restart_cmd
|
55
57
|
end
|
56
58
|
|
57
59
|
should "set its name using env vars" do
|
58
|
-
ENV['
|
60
|
+
ENV['QS_PROCESS_LABEL'] = Factory.string
|
59
61
|
process = @process_class.new(@daemon_spy)
|
60
|
-
assert_equal ENV['
|
62
|
+
assert_equal "qs: #{ENV['QS_PROCESS_LABEL']}", process.name
|
61
63
|
end
|
62
64
|
|
63
65
|
should "ignore blank env values for its name" do
|
64
|
-
ENV['
|
66
|
+
ENV['QS_PROCESS_LABEL'] = ''
|
65
67
|
process = @process_class.new(@daemon_spy)
|
66
|
-
assert_equal "qs
|
67
|
-
|
68
|
+
assert_equal "qs: #{@daemon_spy.name}", process.name
|
69
|
+
end
|
68
70
|
|
69
71
|
should "not daemonize by default" do
|
70
72
|
process = @process_class.new(@daemon_spy)
|
@@ -90,10 +92,6 @@ class Qs::Process
|
|
90
92
|
assert_true process.daemonize?
|
91
93
|
end
|
92
94
|
|
93
|
-
should "not restart by default" do
|
94
|
-
assert_false subject.restart?
|
95
|
-
end
|
96
|
-
|
97
95
|
end
|
98
96
|
|
99
97
|
class RunSetupTests < InitTests
|
@@ -103,28 +101,14 @@ class Qs::Process
|
|
103
101
|
|
104
102
|
@current_process_name = $0
|
105
103
|
|
106
|
-
@
|
107
|
-
|
108
|
-
|
109
|
-
@term_signal_trap_block = block
|
110
|
-
@term_signal_trap_called = true
|
111
|
-
end
|
112
|
-
|
113
|
-
@int_signal_trap_block = nil
|
114
|
-
@int_signal_trap_called = false
|
115
|
-
Assert.stub(::Signal, :trap).with("INT") do |&block|
|
116
|
-
@int_signal_trap_block = block
|
117
|
-
@int_signal_trap_called = true
|
118
|
-
end
|
119
|
-
|
120
|
-
@usr2_signal_trap_block = nil
|
121
|
-
@usr2_signal_trap_called = false
|
122
|
-
Assert.stub(::Signal, :trap).with("USR2") do |&block|
|
123
|
-
@usr2_signal_trap_block = block
|
124
|
-
@usr2_signal_trap_called = true
|
104
|
+
@signal_traps = []
|
105
|
+
Assert.stub(::Signal, :trap) do |signal, &block|
|
106
|
+
@signal_traps << SignalTrap.new(signal, block)
|
125
107
|
end
|
126
108
|
end
|
127
109
|
teardown do
|
110
|
+
@process.signal_io.write(HALT)
|
111
|
+
@thread.join if @thread
|
128
112
|
$0 = @current_process_name
|
129
113
|
end
|
130
114
|
|
@@ -133,87 +117,168 @@ class Qs::Process
|
|
133
117
|
class RunTests < RunSetupTests
|
134
118
|
desc "and run"
|
135
119
|
setup do
|
136
|
-
@process.run
|
120
|
+
@thread = Thread.new{ @process.run }
|
121
|
+
@thread.join(0.1)
|
137
122
|
end
|
138
123
|
|
139
|
-
should "not
|
124
|
+
should "not daemonize the process" do
|
140
125
|
assert_false @daemonize_called
|
141
126
|
end
|
142
127
|
|
143
|
-
should "
|
128
|
+
should "set the process name" do
|
144
129
|
assert_equal $0, subject.name
|
145
130
|
end
|
146
131
|
|
147
|
-
should "
|
132
|
+
should "write its PID file" do
|
148
133
|
assert_true @pid_file_spy.write_called
|
149
134
|
end
|
150
135
|
|
151
|
-
should "
|
152
|
-
|
153
|
-
|
154
|
-
@term_signal_trap_block.call
|
155
|
-
assert_true @daemon_spy.stop_called
|
156
|
-
|
157
|
-
assert_true @int_signal_trap_called
|
158
|
-
assert_false @daemon_spy.halt_called
|
159
|
-
@int_signal_trap_block.call
|
160
|
-
assert_true @daemon_spy.halt_called
|
161
|
-
|
162
|
-
@daemon_spy.stop_called = false
|
163
|
-
|
164
|
-
assert_true @usr2_signal_trap_called
|
165
|
-
assert_false subject.restart?
|
166
|
-
@usr2_signal_trap_block.call
|
167
|
-
assert_true @daemon_spy.stop_called
|
168
|
-
assert_true subject.restart?
|
136
|
+
should "trap signals" do
|
137
|
+
assert_equal 3, @signal_traps.size
|
138
|
+
assert_equal ['INT', 'TERM', 'USR2'], @signal_traps.map(&:signal)
|
169
139
|
end
|
170
140
|
|
171
|
-
should "
|
141
|
+
should "start the daemon" do
|
172
142
|
assert_true @daemon_spy.start_called
|
173
143
|
end
|
174
144
|
|
175
|
-
should "
|
176
|
-
|
145
|
+
should "sleep its thread waiting for signals" do
|
146
|
+
assert_equal 'sleep', @thread.status
|
177
147
|
end
|
178
148
|
|
179
149
|
should "not run the restart cmd" do
|
180
150
|
assert_false @restart_cmd_spy.run_called
|
181
151
|
end
|
182
152
|
|
183
|
-
|
184
|
-
|
153
|
+
end
|
154
|
+
|
155
|
+
class SignalTrapsTests < RunSetupTests
|
156
|
+
desc "signal traps"
|
157
|
+
setup do
|
158
|
+
# setup the io pipe so we can see whats written to it
|
159
|
+
@process.signal_io.setup
|
160
|
+
end
|
161
|
+
teardown do
|
162
|
+
@process.signal_io.teardown
|
163
|
+
end
|
164
|
+
|
165
|
+
should "write the signals to processes signal IO" do
|
166
|
+
@signal_traps.each do |signal_trap|
|
167
|
+
signal_trap.block.call
|
168
|
+
assert_equal signal_trap.signal, subject.signal_io.read
|
169
|
+
end
|
185
170
|
end
|
186
171
|
|
187
172
|
end
|
188
173
|
|
189
174
|
class RunWithDaemonizeTests < RunSetupTests
|
190
|
-
desc "
|
175
|
+
desc "and run when it should daemonize"
|
191
176
|
setup do
|
192
177
|
Assert.stub(@process, :daemonize?){ true }
|
193
|
-
@process.run
|
178
|
+
@thread = Thread.new{ @process.run }
|
179
|
+
@thread.join(0.1)
|
194
180
|
end
|
195
181
|
|
196
|
-
should "
|
182
|
+
should "daemonize the process" do
|
197
183
|
assert_true @daemonize_called
|
198
184
|
end
|
199
185
|
|
200
186
|
end
|
201
187
|
|
202
|
-
class
|
203
|
-
desc "
|
188
|
+
class RunAndHaltTests < RunSetupTests
|
189
|
+
desc "and run with a halt signal"
|
190
|
+
setup do
|
191
|
+
@thread = Thread.new{ @process.run }
|
192
|
+
@process.signal_io.write(HALT)
|
193
|
+
@thread.join(0.1)
|
194
|
+
end
|
195
|
+
|
196
|
+
should "halt its daemon" do
|
197
|
+
assert_true @daemon_spy.halt_called
|
198
|
+
assert_equal [true], @daemon_spy.halt_args
|
199
|
+
end
|
200
|
+
|
201
|
+
should "not set the env var to skip daemonize" do
|
202
|
+
assert_equal @current_env_skip_daemonize, ENV['QS_SKIP_DAEMONIZE']
|
203
|
+
end
|
204
|
+
|
205
|
+
should "not run the restart cmd" do
|
206
|
+
assert_false @restart_cmd_spy.run_called
|
207
|
+
end
|
208
|
+
|
209
|
+
should "remove the PID file" do
|
210
|
+
assert_true @pid_file_spy.remove_called
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
class RunAndStopTests < RunSetupTests
|
216
|
+
desc "and run with a stop signal"
|
217
|
+
setup do
|
218
|
+
@thread = Thread.new{ @process.run }
|
219
|
+
@process.signal_io.write(STOP)
|
220
|
+
@thread.join(0.1)
|
221
|
+
end
|
222
|
+
|
223
|
+
should "stop its daemon" do
|
224
|
+
assert_true @daemon_spy.stop_called
|
225
|
+
assert_equal [true], @daemon_spy.stop_args
|
226
|
+
end
|
227
|
+
|
228
|
+
should "not set the env var to skip daemonize" do
|
229
|
+
assert_equal @current_env_skip_daemonize, ENV['QS_SKIP_DAEMONIZE']
|
230
|
+
end
|
231
|
+
|
232
|
+
should "not run the restart cmd" do
|
233
|
+
assert_false @restart_cmd_spy.run_called
|
234
|
+
end
|
235
|
+
|
236
|
+
should "remove the PID file" do
|
237
|
+
assert_true @pid_file_spy.remove_called
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
class RunAndRestartTests < RunSetupTests
|
243
|
+
desc "and run with a restart signal"
|
204
244
|
setup do
|
205
|
-
|
206
|
-
@
|
207
|
-
@
|
245
|
+
@thread = Thread.new{ @process.run }
|
246
|
+
@process.signal_io.write(RESTART)
|
247
|
+
@thread.join(0.1)
|
248
|
+
end
|
249
|
+
|
250
|
+
should "stop its daemon" do
|
251
|
+
assert_true @daemon_spy.stop_called
|
252
|
+
assert_equal [true], @daemon_spy.stop_args
|
208
253
|
end
|
209
254
|
|
210
|
-
should "set env
|
255
|
+
should "set the env var to skip daemonize" do
|
211
256
|
assert_equal 'yes', ENV['QS_SKIP_DAEMONIZE']
|
257
|
+
end
|
258
|
+
|
259
|
+
should "run the restart cmd" do
|
212
260
|
assert_true @restart_cmd_spy.run_called
|
213
261
|
end
|
214
262
|
|
215
263
|
end
|
216
264
|
|
265
|
+
class RunWithInvalidSignalTests < RunSetupTests
|
266
|
+
desc "and run with unsupported signals"
|
267
|
+
setup do
|
268
|
+
# ruby throws an argument error if the OS doesn't support a signal
|
269
|
+
Assert.stub(::Signal, :trap){ raise ArgumentError }
|
270
|
+
|
271
|
+
@thread = Thread.new{ @process.run }
|
272
|
+
@thread.join(0.1)
|
273
|
+
end
|
274
|
+
|
275
|
+
should "start normally" do
|
276
|
+
assert_true @daemon_spy.start_called
|
277
|
+
assert_equal 'sleep', @thread.status
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
217
282
|
class RestartCmdTests < UnitTests
|
218
283
|
desc "RestartCmd"
|
219
284
|
setup do
|
@@ -262,7 +327,7 @@ class Qs::Process
|
|
262
327
|
end
|
263
328
|
|
264
329
|
class RestartCmdWithPWDEnvNoMatchTests < RestartCmdTests
|
265
|
-
desc "when init with a PWD env variable that
|
330
|
+
desc "when init with a PWD env variable that's not the ruby working dir"
|
266
331
|
setup do
|
267
332
|
@restart_cmd = @cmd_class.new
|
268
333
|
end
|
@@ -274,7 +339,7 @@ class Qs::Process
|
|
274
339
|
end
|
275
340
|
|
276
341
|
class RestartCmdWithPWDEnvInitTests < RestartCmdTests
|
277
|
-
desc "when init with a PWD env variable that
|
342
|
+
desc "when init with a PWD env variable that's the ruby working dir"
|
278
343
|
setup do
|
279
344
|
# make ENV['PWD'] point to the same file as Dir.pwd
|
280
345
|
Assert.stub(File, :stat).with(ENV['PWD']){ @ruby_pwd_stat }
|
@@ -300,6 +365,8 @@ class Qs::Process
|
|
300
365
|
|
301
366
|
end
|
302
367
|
|
368
|
+
SignalTrap = Struct.new(:signal, :block)
|
369
|
+
|
303
370
|
class DaemonSpy
|
304
371
|
include Qs::Daemon
|
305
372
|
|
@@ -309,53 +376,34 @@ class Qs::Process
|
|
309
376
|
queue Qs::Queue.new{ name Factory.string }
|
310
377
|
|
311
378
|
attr_accessor :start_called, :stop_called, :halt_called
|
312
|
-
attr_reader :start_args
|
313
|
-
attr_reader :thread
|
379
|
+
attr_reader :start_args, :stop_args, :halt_args
|
314
380
|
|
315
381
|
def initialize(*args)
|
316
382
|
super
|
383
|
+
@start_args = nil
|
317
384
|
@start_called = false
|
318
|
-
@
|
319
|
-
@
|
320
|
-
|
321
|
-
@
|
322
|
-
|
323
|
-
@thread = ThreadSpy.new
|
385
|
+
@stop_args = nil
|
386
|
+
@stop_called = false
|
387
|
+
@halt_args = nil
|
388
|
+
@halt_called = false
|
324
389
|
end
|
325
390
|
|
326
391
|
def start(*args)
|
327
|
-
@start_args
|
392
|
+
@start_args = args
|
328
393
|
@start_called = true
|
329
|
-
@thread
|
330
394
|
end
|
331
395
|
|
332
396
|
def stop(*args)
|
397
|
+
@stop_args = args
|
333
398
|
@stop_called = true
|
334
399
|
end
|
335
400
|
|
336
401
|
def halt(*args)
|
402
|
+
@halt_args = args
|
337
403
|
@halt_called = true
|
338
404
|
end
|
339
405
|
end
|
340
406
|
|
341
|
-
class ThreadSpy
|
342
|
-
attr_reader :join_called, :on_join_proc
|
343
|
-
|
344
|
-
def initialize
|
345
|
-
@join_called = false
|
346
|
-
@on_join_proc = proc{ }
|
347
|
-
end
|
348
|
-
|
349
|
-
def on_join(&block)
|
350
|
-
@on_join_proc = block
|
351
|
-
end
|
352
|
-
|
353
|
-
def join
|
354
|
-
@join_called = true
|
355
|
-
@on_join_proc.call
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
407
|
class RestartCmdSpy
|
360
408
|
attr_reader :run_called
|
361
409
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Kelly Redding
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2015-
|
19
|
+
date: 2015-05-01 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
@@ -136,6 +136,7 @@ files:
|
|
136
136
|
- lib/qs/daemon.rb
|
137
137
|
- lib/qs/daemon_data.rb
|
138
138
|
- lib/qs/error_handler.rb
|
139
|
+
- lib/qs/io_pipe.rb
|
139
140
|
- lib/qs/job.rb
|
140
141
|
- lib/qs/job_handler.rb
|
141
142
|
- lib/qs/logger.rb
|
@@ -171,6 +172,7 @@ files:
|
|
171
172
|
- test/unit/daemon_data_tests.rb
|
172
173
|
- test/unit/daemon_tests.rb
|
173
174
|
- test/unit/error_handler_tests.rb
|
175
|
+
- test/unit/io_pipe_tests.rb
|
174
176
|
- test/unit/job_handler_tests.rb
|
175
177
|
- test/unit/job_tests.rb
|
176
178
|
- test/unit/logger_tests.rb
|
@@ -239,6 +241,7 @@ test_files:
|
|
239
241
|
- test/unit/daemon_data_tests.rb
|
240
242
|
- test/unit/daemon_tests.rb
|
241
243
|
- test/unit/error_handler_tests.rb
|
244
|
+
- test/unit/io_pipe_tests.rb
|
242
245
|
- test/unit/job_handler_tests.rb
|
243
246
|
- test/unit/job_tests.rb
|
244
247
|
- test/unit/logger_tests.rb
|