qs 0.1.0 → 0.2.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/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
|