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 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