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 CHANGED
@@ -8,6 +8,7 @@ LOGGER = if ENV['BENCH_REPORT']
8
8
  else
9
9
  Logger.new(STDOUT)
10
10
  end
11
+ LOGGER.datetime_format = "" # turn off the datetime in the logs
11
12
 
12
13
  PROGRESS_IO = if ENV['BENCH_PROGRESS_IO']
13
14
  ::IO.for_fd(ENV['BENCH_PROGRESS_IO'].to_i)
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.6041s
9
- Running 10000 Jobs Time: 12.3159s
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
- self.logger.debug "Starting work loop..."
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
- self.logger.debug "Stopping work loop..."
102
- shutdown_worker_pool
104
+ log "Stopping work loop", :debug
103
105
  rescue StandardError => exception
104
- self.logger.error "Exception occurred, stopping daemon!"
105
- self.logger.error "#{exception.class}: #{exception.message}"
106
- self.logger.error exception.backtrace.join("\n")
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
- self.logger.debug "Stopped work loop"
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.signal }
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
- args = [self.signals_redis_key, self.queue_redis_keys.shuffle, 0].flatten
142
- redis_key, serialized_payload = @client.block_dequeue(*args)
143
- if redis_key != @signals_redis_key
144
- @worker_pool.add_work(RedisItem.new(redis_key, serialized_payload))
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
- self.logger.debug "Shutting down worker pool"
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.signal
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
- attr_reader :daemon, :name, :pid_file, :restart_cmd
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
- @name = ignore_if_blank(ENV['QS_PROCESS_NAME']) || "qs-#{@daemon.name}"
22
+ @pid_file = PIDFile.new(@daemon.pid_file)
23
+ @signal_io = IOPipe.new
24
+ @restart_cmd = RestartCmd.new
17
25
 
18
- @daemonize = !!options[:daemonize]
19
- @skip_daemonize = !!ignore_if_blank(ENV['QS_SKIP_DAEMONIZE'])
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
- ::Signal.trap("TERM"){ @daemon.stop }
32
- ::Signal.trap("INT"){ @daemon.halt }
33
- ::Signal.trap("USR2") do
34
- @daemon.stop
35
- @restart = true
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
- 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."
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 && !@skip_daemonize
54
+ @daemonize
51
55
  end
52
56
 
53
- def restart?
54
- @restart
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
- private
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 log(message)
60
- @logger.info "[Qs] #{message}"
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 #{@daemon.name} daemon..."
97
+ def run_restart_cmd(daemon, restart_cmd)
98
+ log "Restarting #{daemon.name} daemon"
65
99
  ENV['QS_SKIP_DAEMONIZE'] = 'yes'
66
- @restart_cmd.run
100
+ restart_cmd.run
67
101
  end
68
102
 
69
- def default_if_blank(value, default, &block)
70
- ignore_if_blank(value, &block) || default
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
@@ -1,3 +1,3 @@
1
1
  module Qs
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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 Logger.new(ROOT_PATH.join('log/app_daemon.log').to_s)
22
+ logger LOGGER
20
23
  verbose_logging true
21
24
 
22
25
  queue AppQueue
@@ -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
@@ -18,9 +18,9 @@ class Qs::Process
18
18
  class InitTests < UnitTests
19
19
  desc "when init"
20
20
  setup do
21
- @current_env_process_name = ENV['QS_PROCESS_NAME']
21
+ @current_env_process_label = ENV['QS_PROCESS_LABEL']
22
22
  @current_env_skip_daemonize = ENV['QS_SKIP_DAEMONIZE']
23
- ENV.delete('QS_PROCESS_NAME')
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['QS_PROCESS_NAME'] = @current_env_process_name
40
+ ENV['QS_PROCESS_LABEL'] = @current_env_process_label
41
41
  end
42
42
  subject{ @process }
43
43
 
44
- should have_readers :daemon, :name, :pid_file, :restart_cmd
45
- should have_imeths :run, :daemonize?, :restart?
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-#{@daemon_spy.name}", subject.name
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['QS_PROCESS_NAME'] = Factory.string
60
+ ENV['QS_PROCESS_LABEL'] = Factory.string
59
61
  process = @process_class.new(@daemon_spy)
60
- assert_equal ENV['QS_PROCESS_NAME'], process.name
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['QS_PROCESS_NAME'] = ''
66
+ ENV['QS_PROCESS_LABEL'] = ''
65
67
  process = @process_class.new(@daemon_spy)
66
- assert_equal "qs-#{@daemon_spy.name}", process.name
67
- end
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
- @term_signal_trap_block = nil
107
- @term_signal_trap_called = false
108
- Assert.stub(::Signal, :trap).with("TERM") do |&block|
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 have daemonized the process" do
124
+ should "not daemonize the process" do
140
125
  assert_false @daemonize_called
141
126
  end
142
127
 
143
- should "have set the process name" do
128
+ should "set the process name" do
144
129
  assert_equal $0, subject.name
145
130
  end
146
131
 
147
- should "have written the PID file" do
132
+ should "write its PID file" do
148
133
  assert_true @pid_file_spy.write_called
149
134
  end
150
135
 
151
- should "have trapped signals" do
152
- assert_true @term_signal_trap_called
153
- assert_false @daemon_spy.stop_called
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 "have started the daemon" do
141
+ should "start the daemon" do
172
142
  assert_true @daemon_spy.start_called
173
143
  end
174
144
 
175
- should "have joined the daemon thread" do
176
- assert_true @daemon_spy.thread.join_called
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
- should "have removed the PID file" do
184
- assert_true @pid_file_spy.remove_called
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 "that should daemonize is run"
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 "have daemonized the process" do
182
+ should "daemonize the process" do
197
183
  assert_true @daemonize_called
198
184
  end
199
185
 
200
186
  end
201
187
 
202
- class RunAndDaemonPausedTests < RunSetupTests
203
- desc "then run and sent a restart signal"
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
- # mimicing pause being called by a signal, after the thread is joined
206
- @daemon_spy.thread.on_join{ @usr2_signal_trap_block.call }
207
- @process.run
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 vars for restarting and run the restart cmd" do
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 doesn't point to ruby working dir"
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 points to the ruby working dir"
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
- @stop_called = false
319
- @halt_called = false
320
-
321
- @start_args = nil
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 = 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: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.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-04-24 00:00:00 Z
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