servolux 0.9.7 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,12 @@
1
+ == 0.10.0 / 2012-02-18
2
+
3
+ * Minor Enhancements
4
+ * Add in the ability to vary the Prefork worker pool size [issue #8]
5
+ * Pass original child exception backtrace up the exception chain [issue #7]
6
+ * Improved child process wellness checks in Piper and Child classes
7
+ * Bug Fixes
8
+ * Typo and documentation fixes [issue #6]
9
+
1
10
  == 0.9.7 / 2012-01-19
2
11
 
3
12
  * Minor Enhancements
data/README.rdoc CHANGED
@@ -8,7 +8,7 @@ by Tim Pease {<img src="https://secure.travis-ci.org/TwP/servolux.png">}[http://
8
8
 
9
9
  Serv-O-Lux is a collection of Ruby classes that are useful for daemon and
10
10
  process management, and for writing your own Ruby services. The code is well
11
- documented and tested. It works with Ruby and JRuby supporing both 1.8 and 1.9
11
+ documented and tested. It works with Ruby and JRuby supporting both 1.8 and 1.9
12
12
  interpreters.
13
13
 
14
14
  === FEATURES
@@ -29,7 +29,7 @@ process to be passed to the parent and raised there.
29
29
 
30
30
  Servolux::Daemon -- a robust class for starting and stopping daemon processes.
31
31
 
32
- Servolux::Child -- adds some much needed funtionality to child processes
32
+ Servolux::Child -- adds some much needed functionality to child processes
33
33
  created via Ruby's IO#popen method. Specifically, a timeout thread is used to
34
34
  signal the child process to die if it does not exit in a given amount of time.
35
35
 
data/Rakefile CHANGED
@@ -14,7 +14,7 @@ Bones {
14
14
  email 'tim.pease@gmail.com'
15
15
  url 'http://gemcutter.org/gems/servolux'
16
16
  readme_file 'README.rdoc'
17
- spec.opts << '--color'
17
+ spec.opts << '--color' << '--format documentation'
18
18
 
19
19
  use_gmail
20
20
 
@@ -2,12 +2,12 @@
2
2
  #
3
3
  # In this example, we prefork 7 processes each of which connect to our
4
4
  # Beanstalkd queue and then wait for jobs to process. We are using a module so
5
- # that we can connect to the beanstalk queue before exectuing and then
5
+ # that we can connect to the beanstalk queue before executing and then
6
6
  # disconnect from the beanstalk queue after exiting. These methods are called
7
7
  # exactly once per child process.
8
8
  #
9
9
  # A variation on this is to load source code in the before_executing method
10
- # and initialize an object that will process jobs. This is advantagous because
10
+ # and initialize an object that will process jobs. This is advantageous because
11
11
  # now you can send SIGHUP to a child process and it will restart, loading your
12
12
  # Ruby libraries before executing. Now you can do a rolling deploy of new
13
13
  # code.
@@ -0,0 +1,151 @@
1
+ require 'rubygems'
2
+ require 'servolux'
3
+ require 'logger'
4
+
5
+ ###############################################################################
6
+ # This is an example script that creates uses both Prefork and Server
7
+ #
8
+ # The Server will manage the Prefork pool and respond to signals to add new
9
+ # workers to the pool
10
+ ###############################################################################
11
+
12
+ # The child script that will be executed, this is just a shell script that
13
+ # will be execed by each worker pool member.
14
+ module ExecChild
15
+ def self.the_script
16
+ <<__sh__
17
+ #!/bin/sh
18
+ #
19
+ trap "echo I am process $$" SIGUSR1
20
+ trap "exit 0" SIGHUP
21
+ trap "exit 1" SIGTERM
22
+
23
+ echo "[$$] I am a child program"
24
+ while true
25
+ do
26
+ sleep 1
27
+ done
28
+ __sh__
29
+ end
30
+
31
+ def self.script_name
32
+ "/tmp/exec-child-worker.sh"
33
+ end
34
+
35
+ def self.write_script
36
+ File.open( script_name, "w+", 0750 ) do |f|
37
+ f.write( the_script )
38
+ end
39
+ end
40
+
41
+ def self.remove_script
42
+ File.unlink( script_name )
43
+ end
44
+
45
+ def execute
46
+ exec ExecChild.script_name
47
+ end
48
+ end
49
+
50
+ class PreforkingServerExample < ::Servolux::Server
51
+
52
+ # Create a preforking server that has the given minimum and maximum boundaries
53
+ #
54
+ def initialize( min_workers = 2, max_workers = 10 )
55
+ @logger = ::Logger.new( $stderr )
56
+ super( self.class.name, :interval => 2, :logger => @logger )
57
+ @pool = Servolux::Prefork.new( :module => ExecChild, :timeout => nil,
58
+ :min_workers => min_workers, :max_workers => max_workers )
59
+ end
60
+
61
+ def log( msg )
62
+ logger.info msg
63
+ end
64
+
65
+ def log_pool_status
66
+ log "Pool status : #{@pool.worker_counts.inspect} living pids #{live_worker_pids.join(' ')}"
67
+ end
68
+
69
+ def live_worker_pids
70
+ pids = []
71
+ @pool.each_worker { |w| pids << w.pid if w.alive? }
72
+ return pids
73
+ end
74
+
75
+ def shutdown_workers
76
+ log "Shutting down all workers"
77
+ @pool.stop
78
+ loop do
79
+ log_pool_status
80
+ break if @pool.live_worker_count <= 0
81
+ sleep 0.25
82
+ end
83
+ end
84
+
85
+ def log_worker_status( worker )
86
+ if not worker.alive? then
87
+ worker.wait
88
+ if worker.exited? then
89
+ log "Worker #{worker.pid} exited with status #{worker.exitstatus}"
90
+ elsif worker.signaled? then
91
+ log "Worker #{worker.pid} signaled by #{worker.termsig}"
92
+ elsif worker.stopped? then
93
+ log "Worker #{worker.pid} stopped by #{worker.stopsig}"
94
+ else
95
+ log "I have no clue #{worker.inspect}"
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+ #############################################################################
102
+ # Implementations of parts of the Servolux::Server API
103
+ #############################################################################
104
+
105
+ # this is run once before the Server's run loop
106
+ def before_starting
107
+ ExecChild.write_script
108
+ log "Starting up the Pool"
109
+ @pool.start( 1 )
110
+ log "Send a USR1 to add a worker (kill -usr1 #{Process.pid})"
111
+ log "Send a USR2 to kill all the workers (kill -usr2 #{Process.pid})"
112
+ log "Send a INT (Ctrl-C) or TERM to shutdown the server (kill -term #{Process.pid})"
113
+ end
114
+
115
+ # Add a worker to the pool when USR1 is received
116
+ def usr1
117
+ log "Adding a worker"
118
+ @pool.add_workers
119
+ end
120
+
121
+ # kill all the current workers with a usr2, the run loop will respawn up to
122
+ # the min_worker count
123
+ #
124
+ def usr2
125
+ shutdown_workers
126
+ end
127
+
128
+ # By default, Servolux::Server will capture the TERM signal and call its
129
+ # +shutdown+ method. After that +shutdown+ method is called it will call
130
+ # +after_shutdown+ we're going to hook into that so that all the workers get
131
+ # cleanly shutdown before the parent process exits
132
+ def after_stopping
133
+ shutdown_workers
134
+ ExecChild.remove_script
135
+ end
136
+
137
+ # This is the method that is executed during the run loop
138
+ #
139
+ def run
140
+ log_pool_status
141
+ @pool.each_worker do |worker|
142
+ log_worker_status( worker )
143
+ end
144
+ @pool.ensure_worker_pool_size
145
+ end
146
+ end
147
+
148
+ if $0 == __FILE__ then
149
+ pse = PreforkingServerExample.new
150
+ pse.startup
151
+ end
@@ -25,7 +25,7 @@ server.startup
25
25
  # pool to be gracefully stopped and to be monitored by the server thread. This
26
26
  # monitoring involves reaping child processes that have died and reporting on
27
27
  # errors raised by children. It is also possible to respawn dead child
28
- # workers, but this should be thuroughly thought through (ha, unintentional
28
+ # workers, but this should be thoroughly thought through (ha, unintentional
29
29
  # alliteration) before doing so [if the CPU is thrashing, then respawning dead
30
30
  # child workers will only contribute to the thrash].
31
31
  module BeanstalkWorkerPool
@@ -38,7 +38,7 @@ module BeanstalkWorkerPool
38
38
 
39
39
  # This run loop will be called at a fixed interval by the server thread. If
40
40
  # the pool has any child processes that have died or restarted, then the
41
- # expired PIDs are reaed from the proc table. If any workers in the pool
41
+ # expired PIDs are read from the proc table. If any workers in the pool
42
42
  # have reported an error, then display those errors on STDOUT; these are
43
43
  # errors raised from the child process that caused the child to terminate.
44
44
  def run
data/lib/servolux.rb CHANGED
@@ -18,7 +18,7 @@ module Servolux
18
18
  end
19
19
 
20
20
  # Returns the library path for the module. If any arguments are given,
21
- # they will be joined to the end of the libray path using
21
+ # they will be joined to the end of the library path using
22
22
  # <tt>File.join</tt>.
23
23
  #
24
24
  # @return [String] absolute servolux 'lib' path
@@ -130,6 +130,18 @@ class Servolux::Child
130
130
  # global variable $? is set to a Process::Status object containing
131
131
  # information on the child process.
132
132
  #
133
+ # You can get more information about how the child status exited by calling
134
+ # the following methods on the piper instance:
135
+ #
136
+ # * coredump?
137
+ # * exited?
138
+ # * signaled?
139
+ # * stopped?
140
+ # * success?
141
+ # * exitstatus
142
+ # * stopsig
143
+ # * termsig
144
+ #
133
145
  # @param [Integer] flags Bit flags that will be passed to the system level
134
146
  # wait call. See the Ruby core documentation for Process#wait for more
135
147
  # information on these flags.
@@ -138,8 +150,7 @@ class Servolux::Child
138
150
  #
139
151
  def wait( flags = 0 )
140
152
  return if @io.nil?
141
- Process.wait(@pid, flags)
142
- @status = $?
153
+ _, @status = Process.wait2(@pid, flags) unless @status
143
154
  exitstatus
144
155
  end
145
156
 
@@ -150,6 +161,7 @@ class Servolux::Child
150
161
  #
151
162
  def alive?
152
163
  return if @io.nil?
164
+ wait(Process::WNOHANG|Process::WUNTRACED)
153
165
  Process.kill(0, @pid)
154
166
  true
155
167
  rescue Errno::ESRCH, Errno::ENOENT
@@ -112,16 +112,16 @@ class Servolux::Daemon
112
112
  # The time (in seconds) to wait for the daemon process to either startup
113
113
  # or shutdown. An error is raised when this timeout is exceeded.
114
114
  #
115
- # @option opts [Boolen] :nochdir (false)
115
+ # @option opts [Boolean] :nochdir (false)
116
116
  # When set to true this flag directs the daemon process to keep the
117
117
  # current working directory. By default, the process of daemonizing will
118
118
  # cause the current working directory to be changed to the root folder
119
119
  # (thus preventing the daemon process from holding onto the directory
120
120
  # inode).
121
121
  #
122
- # @option opts [Boolen] :noclose (false)
122
+ # @option opts [Boolean] :noclose (false)
123
123
  # When set to true this flag keeps the standard input/output streams from
124
- # being reopend to /dev/null when the daemon process is created. Reopening
124
+ # being reopened to /dev/null when the daemon process is created. Reopening
125
125
  # the standard input/output streams frees the file descriptors which are
126
126
  # still being used by the parent process. This prevents zombie processes.
127
127
  #
@@ -131,7 +131,7 @@ class Servolux::Daemon
131
131
  #
132
132
  # @option opts [String] :log_file (nil)
133
133
  # This log file will be monitored to determine if the daemon process has
134
- # sucessfully started.
134
+ # successfully started.
135
135
  #
136
136
  # @option opts [String, Regexp] :look_for (nil)
137
137
  # This can be either a String or a Regexp. It defines a phrase to search
@@ -186,7 +186,7 @@ class Servolux::Daemon
186
186
  #
187
187
  # If the startup command is a Proc or a bound Method then it is invoked
188
188
  # using the +call+ method on the object. No arguments are passed to the
189
- # +call+ invocoation.
189
+ # +call+ invocation.
190
190
  #
191
191
  # Lastly, if the startup command is a Servolux::Server then its +startup+
192
192
  # method is called.
@@ -392,7 +392,7 @@ private
392
392
  started = wait_for {
393
393
  rv = started?
394
394
  err = @piper.gets
395
- raise StartupError, "Child raised error: #{err.inspect}" unless err.nil?
395
+ raise StartupError, "Child raised error: #{err.inspect}", err.backtrace unless err.nil?
396
396
  rv
397
397
  }
398
398
 
@@ -2,13 +2,13 @@
2
2
  require 'socket'
3
3
 
4
4
  # == Synopsis
5
- # A Piper is used to fork a child proces and then establish a communication
5
+ # A Piper is used to fork a child process and then establish a communication
6
6
  # pipe between the parent and child. This communication pipe is used to pass
7
7
  # Ruby objects between the two.
8
8
  #
9
9
  # == Details
10
10
  # When a new piper instance is created, the Ruby process is forked into two
11
- # porcesses - the parent and the child. Each continues execution from the
11
+ # processes - the parent and the child. Each continues execution from the
12
12
  # point of the fork. The piper establishes a pipe for communication between
13
13
  # the parent and the child. This communication pipe can be opened as read /
14
14
  # write / read-write (from the perspective of the parent).
@@ -124,6 +124,7 @@ class Servolux::Piper
124
124
  raise ArgumentError, "Unsupported mode #{mode.inspect}"
125
125
  end
126
126
 
127
+ @status = nil
127
128
  @timeout = opts.fetch(:timeout, nil)
128
129
  socket_pair = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
129
130
  @child_pid = Kernel.fork
@@ -207,7 +208,7 @@ class Servolux::Piper
207
208
  end
208
209
  end
209
210
 
210
- # Returns +true+ if this is the child prcoess and +false+ otherwise.
211
+ # Returns +true+ if this is the child process and +false+ otherwise.
211
212
  #
212
213
  # @return [Boolean]
213
214
  #
@@ -235,7 +236,7 @@ class Servolux::Piper
235
236
  end
236
237
  end
237
238
 
238
- # Returns +true+ if this is the parent prcoess and +false+ otherwise.
239
+ # Returns +true+ if this is the parent process and +false+ otherwise.
239
240
  #
240
241
  # @return [Boolean]
241
242
  #
@@ -309,11 +310,43 @@ class Servolux::Piper
309
310
  # the child process.
310
311
  #
311
312
  def signal( sig )
312
- return if @child_pid.nil?
313
- sig = Signal.list.invert[sig] if sig.is_a?(Integer)
313
+ return if child?
314
+ return unless alive?
314
315
  Process.kill(sig, @child_pid)
315
316
  end
316
317
 
318
+ # Waits for the child process to exit and returns its exit status. The
319
+ # global variable $? is set to a Process::Status object containing
320
+ # information on the child process.
321
+ #
322
+ # Always returns +nil+ when called from the child process.
323
+ #
324
+ # You can get more information about how the child status exited by calling
325
+ # the following methods on the piper instance:
326
+ #
327
+ # * coredump?
328
+ # * exited?
329
+ # * signaled?
330
+ # * stopped?
331
+ # * success?
332
+ # * exitstatus
333
+ # * stopsig
334
+ # * termsig
335
+ #
336
+ # @param [Integer] flags Bit flags that will be passed to the system level
337
+ # wait call. See the Ruby core documentation for Process#wait for more
338
+ # information on these flags.
339
+ # @return [Integer, nil] The exit status of the child process or +nil+ if
340
+ # the child process is not running.
341
+ #
342
+ def wait( flags = 0 )
343
+ return if child?
344
+ _, @status = Process.wait2(@child_pid, flags) unless @status
345
+ exitstatus
346
+ rescue Errno::ECHILD
347
+ nil
348
+ end
349
+
317
350
  # Returns +true+ if the child process is alive. Returns +nil+ if the child
318
351
  # process has not been started.
319
352
  #
@@ -322,12 +355,22 @@ class Servolux::Piper
322
355
  # @return [Boolean, nil]
323
356
  #
324
357
  def alive?
325
- return if @child_pid.nil?
358
+ return if child?
359
+ wait(Process::WNOHANG|Process::WUNTRACED)
326
360
  Process.kill(0, @child_pid)
327
361
  true
328
362
  rescue Errno::ESRCH, Errno::ENOENT
329
363
  false
330
364
  end
331
365
 
366
+ %w[coredump? exited? signaled? stopped? success? exitstatus stopsig termsig].
367
+ each { |method|
368
+ self.class_eval <<-CODE
369
+ def #{method}
370
+ return if @status.nil?
371
+ @status.#{method}
372
+ end
373
+ CODE
374
+ }
332
375
  end
333
376
 
@@ -127,7 +127,7 @@
127
127
  #
128
128
  class Servolux::Prefork
129
129
 
130
- Timeout = Class.new(::Servolux::Error)
130
+ CommunicationError = Class.new(::Servolux::Error)
131
131
  UnknownSignal = Class.new(::Servolux::Error)
132
132
  UnknownResponse = Class.new(::Servolux::Error)
133
133
 
@@ -138,8 +138,9 @@ class Servolux::Prefork
138
138
  HEARTBEAT = "\000<3".freeze # @private
139
139
  # :startdoc:
140
140
 
141
- attr_accessor :timeout # Communication timeout in seconds.
142
- attr_reader :harvest # List of child PIDs that need to be reaped
141
+ attr_accessor :timeout # Communication timeout in seconds.
142
+ attr_accessor :min_workers # Minimum number of workers
143
+ attr_accessor :max_workers # Maximum number of workers
143
144
 
144
145
  # call-seq:
145
146
  # Prefork.new { block }
@@ -156,15 +157,22 @@ class Servolux::Prefork
156
157
  # If you do not want to use the heartbeat then leave the :timeout unset or
157
158
  # manually set it to +nil+.
158
159
  #
160
+ # Additionally, :min_workers and :max_workers options are avilable. If
161
+ # :min_workers is given, the method +ensure_worker_pool_size+ will guarantee
162
+ # that at least :min_workers are up and running. If :max_workers is given,
163
+ # then +add_workers+ will NOT allow ou to spawn more workers than
164
+ # :max_workers.
165
+ #
159
166
  # The pre-forking worker pool makes no effort to restart dead workers. It is
160
167
  # left to the user to implement this functionality.
161
168
  #
162
169
  def initialize( opts = {}, &block )
163
170
  @timeout = opts.fetch(:timeout, nil)
164
171
  @module = opts.fetch(:module, nil)
172
+ @max_workers = opts.fetch(:max_workers, nil)
173
+ @min_workers = opts.fetch(:min_workers, nil)
165
174
  @module = Module.new { define_method :execute, &block } if block
166
175
  @workers = []
167
- @harvest = Queue.new
168
176
 
169
177
  raise ArgumentError, 'No code was given to execute by the workers.' unless @module
170
178
  end
@@ -177,13 +185,8 @@ class Servolux::Prefork
177
185
  #
178
186
  def start( number )
179
187
  @workers.clear
180
-
181
- number.times {
182
- @workers << Worker.new(self)
183
- @workers.last.extend @module
184
- }
185
- @workers.each { |worker| worker.start; pause }
186
- self
188
+ add_workers( number )
189
+ self
187
190
  end
188
191
 
189
192
  # Stop all workers. The current process will wait for each child process to
@@ -204,17 +207,14 @@ class Servolux::Prefork
204
207
  # @return [Prefork] self
205
208
  #
206
209
  def reap
207
- while !@harvest.empty?
208
- pid = @harvest.pop
209
- Process.wait pid rescue nil
210
- end
210
+ @workers.each { |worker| worker.alive? }
211
211
  self
212
212
  end
213
213
 
214
214
  # Send this given _signal_ to all child process. The default signal is
215
215
  # 'TERM'. The method waits for a short period of time after the signal is
216
216
  # sent to each child; this is done to alleviate a flood of signals being
217
- # sent simultaneously and overwhemlming the CPU.
217
+ # sent simultaneously and overwhelming the CPU.
218
218
  #
219
219
  # @param [String, Integer] signal The signal to send to child processes.
220
220
  # @return [Prefork] self
@@ -228,7 +228,7 @@ class Servolux::Prefork
228
228
  # call-seq:
229
229
  # each_worker { |worker| block }
230
230
  #
231
- # Iterates over all the works and yields each, in turn, to the given
231
+ # Iterates over all the workers and yields each, in turn, to the given
232
232
  # _block_.
233
233
  #
234
234
  def each_worker( &block )
@@ -236,6 +236,70 @@ class Servolux::Prefork
236
236
  self
237
237
  end
238
238
 
239
+ # call-seq:
240
+ # add_workers( number = 1 )
241
+ #
242
+ # Adds additional workers to the pool. It will not add more workers than
243
+ # the number set in :max_workers
244
+ #
245
+ def add_workers( number = 1 )
246
+ number.times do
247
+ break if at_max_workers?
248
+ worker = Worker.new( self )
249
+ worker.extend @module
250
+ worker.start
251
+ @workers << worker
252
+ pause
253
+ end
254
+ end
255
+
256
+ # call-seq:
257
+ # prune_workers()
258
+ #
259
+ # Remove workers that are no longer alive from the worker pool
260
+ #
261
+ def prune_workers
262
+ new_workers = @workers.find_all { |w| w.alive? }
263
+ @workers = new_workers
264
+ end
265
+
266
+ # call-seq:
267
+ # ensure_worker_pool_size()
268
+ #
269
+ # Make sure that the worker pool has >= the minimum number of workers and less
270
+ # than the maximum number of workers.
271
+ #
272
+ # Generally, this means prune the number of workers and then spawn workers up
273
+ # to the min_worker level. If min is not set, then we only prune
274
+ #
275
+ def ensure_worker_pool_size
276
+ prune_workers
277
+ while below_minimum_workers? do
278
+ add_workers
279
+ end
280
+ end
281
+
282
+ # call-seq:
283
+ # below_minimum_workers?
284
+ #
285
+ # Report if the number of workers is below the minimum threshold
286
+ #
287
+ def below_minimum_workers?
288
+ return false unless @min_workers
289
+ return @workers.size < @min_workers
290
+ end
291
+
292
+ # call-seq:
293
+ # at_max_workers?
294
+ #
295
+ # Return true or false if we are currently at or above the maximum number of
296
+ # workers allowed.
297
+ #
298
+ def at_max_workers?
299
+ return false unless @max_workers
300
+ return @workers.size >= @max_workers
301
+ end
302
+
239
303
  # call-seq:
240
304
  # errors { |worker| block }
241
305
  #
@@ -247,12 +311,40 @@ class Servolux::Prefork
247
311
  self
248
312
  end
249
313
 
314
+ # call-seq:
315
+ # worker_counts -> { :alive => 2, :dead => 1 }
316
+ #
317
+ # Returns a hash containing the counts of alive and dead workers
318
+ def worker_counts
319
+ counts = { :alive => 0, :dead => 0 }
320
+ each_worker do |worker|
321
+ state = worker.alive? ? :alive : :dead
322
+ counts[state] += 1
323
+ end
324
+ return counts
325
+ end
326
+
327
+ # call-seq:
328
+ # live_worker_count -> Integer
329
+ #
330
+ # Returns the number of live workers in the pool
331
+ def live_worker_count
332
+ worker_counts[:alive]
333
+ end
334
+
335
+ # call-seq:
336
+ # dead_worker_count -> Integer
337
+ #
338
+ # Returns the number of dead workers in the pool
339
+ def dead_worker_count
340
+ worker_counts[:dead]
341
+ end
250
342
 
251
343
  private
252
344
 
253
345
  # Pause script execution for a random time interval between 0.1 and 0.4
254
346
  # seconds. This method is used to slow down the starting and stopping of
255
- # child processes in order to avoiad the "thundering herd" problem.
347
+ # child processes in order to avoid the "thundering herd" problem.
256
348
  # http://en.wikipedia.org/wiki/Thundering_herd_problem
257
349
  #
258
350
  def pause
@@ -274,7 +366,6 @@ private
274
366
  #
275
367
  def initialize( prefork )
276
368
  @timeout = prefork.timeout
277
- @harvest = prefork.harvest
278
369
  @thread = nil
279
370
  @piper = nil
280
371
  @error = nil
@@ -317,7 +408,7 @@ private
317
408
  #
318
409
  def wait
319
410
  return if @piper.nil? or @piper.child?
320
- Process.wait(@piper.pid, Process::WNOHANG|Process::WUNTRACED)
411
+ @piper.wait(Process::WNOHANG|Process::WUNTRACED)
321
412
  end
322
413
 
323
414
  # Send this given _signal_ to the child process. The default signal is
@@ -356,9 +447,19 @@ private
356
447
  #
357
448
  def timed_out?
358
449
  return if @piper.nil? or @piper.child?
359
- Timeout === @error
450
+ CommunicationError === @error
360
451
  end
361
452
 
453
+ %w[pid coredump? exited? signaled? stopped? success? exitstatus stopsig termsig].
454
+ each { |method|
455
+ self.class_eval <<-CODE
456
+ def #{method}
457
+ return if @piper.nil?
458
+ @piper.#{method}
459
+ end
460
+ CODE
461
+ }
462
+
362
463
  private
363
464
 
364
465
  def close_parent
@@ -378,7 +479,6 @@ private
378
479
  rescue StandardError => err
379
480
  @error = err
380
481
  ensure
381
- @harvest << @piper.pid
382
482
  close_parent
383
483
  start if START == response and !Thread.current[:stop]
384
484
  end
@@ -390,6 +490,7 @@ private
390
490
  response = nil
391
491
  loop {
392
492
  break if Thread.current[:stop]
493
+ break unless @piper.alive?
393
494
  @piper.puts HEARTBEAT
394
495
  response = @piper.gets(ERROR)
395
496
  break if Thread.current[:stop]
@@ -398,8 +499,8 @@ private
398
499
  when HEARTBEAT; next
399
500
  when START; break
400
501
  when ERROR
401
- raise Timeout,
402
- "Child did not respond in a timely fashion. Timeout is set to #{@timeout.inspect} seconds."
502
+ raise CommunicationError,
503
+ "Unable to read data from Child process. Possible timeout, closing of pipe and/or child death."
403
504
  when Exception
404
505
  @error = response
405
506
  break
@@ -416,7 +517,6 @@ private
416
517
  # signals and communication with the parent.
417
518
  #
418
519
  def child
419
- @harvest = nil
420
520
 
421
521
  # if we get a HUP signal, then tell the parent process to stop this
422
522
  # child process and start a new one to replace it
@@ -458,8 +558,8 @@ private
458
558
  when HALT
459
559
  break
460
560
  when ERROR
461
- raise Timeout,
462
- "Parent did not respond in a timely fashion. Timeout is set to #{@timeout.inspect} seconds."
561
+ raise CommunicationError,
562
+ "Unable to read data from Parent process. Possible timeout, closing of pipe and/or parent death."
463
563
  else
464
564
  raise UnknownSignal,
465
565
  "Child received unknown signal: #{signal.inspect}"
@@ -16,7 +16,7 @@ require 'thread'
16
16
  # SIGINT and SIGTERM are handled by default. These signals will gracefully
17
17
  # shutdown the server by calling the +shutdown+ method (provided by default,
18
18
  # too). A few other signals can be handled by defining a few methods on your
19
- # server instance. For example, SIGINT is hanlded by the +int+ method (an
19
+ # server instance. For example, SIGINT is handled by the +int+ method (an
20
20
  # alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
21
21
  # (another alias for +shutdown+). The following signal methods are
22
22
  # recognized by the Server class:
@@ -43,7 +43,7 @@ require 'thread'
43
43
  # shutdown: +before_stopping+ and +after_stopping+. The first is called just
44
44
  # before the run loop thread is signaled for shutdown. The second is called
45
45
  # just after the run loop thread has died; the +after_stopping+ method is
46
- # guarnteed to NOT be called till after the run loop thread is well and
46
+ # guaranteed to NOT be called till after the run loop thread is well and
47
47
  # truly dead.
48
48
  #
49
49
  # == Usage
@@ -1,6 +1,6 @@
1
1
 
2
2
  # == Synopsis
3
- # The Threaded module is used to peform some activity at a specified
3
+ # The Threaded module is used to perform some activity at a specified
4
4
  # interval.
5
5
  #
6
6
  # == Details
@@ -14,7 +14,7 @@
14
14
  # Just before the thread is created the +before_starting+ method will be
15
15
  # called (if it is defined by the threaded object). Likewise, after the
16
16
  # thread is created the +after_starting+ method will be called (if it is
17
- # defeined by the threaded object).
17
+ # defined by the threaded object).
18
18
  #
19
19
  # The threaded object is stopped by calling the +stop+ method. This sets an
20
20
  # internal flag and then wakes up the thread. The thread gracefully exits
@@ -22,7 +22,7 @@
22
22
  # are defined for stopping as well. Just before the thread is stopped the
23
23
  # +before_stopping+ method will be called (if it is defined by the threaded
24
24
  # object). Likewise, after the thread has died the +after_stopping+ method
25
- # will be called (if it is defeined by the threaded object).
25
+ # will be called (if it is defined by the threaded object).
26
26
  #
27
27
  # Calling the +join+ method on a threaded object will cause the calling
28
28
  # thread to wait until the threaded object has stopped. An optional timeout
@@ -35,7 +35,7 @@
35
35
  module Servolux::Threaded
36
36
 
37
37
  # This method will be called by the activity thread at the desired
38
- # interval. Implementing classes are exptect to provide this
38
+ # interval. Implementing classes are expect to provide this
39
39
  # functionality.
40
40
  #
41
41
  def run
@@ -189,7 +189,7 @@ module Servolux::Threaded
189
189
  _activity_thread.continue_on_error = (value ? true : false)
190
190
  end
191
191
 
192
- # Returns +true+ if the threded object should continue running even if an
192
+ # Returns +true+ if the threaded object should continue running even if an
193
193
  # error is raised by the run method. The default is to return +false+. The
194
194
  # threaded object will stop running when an error is raised.
195
195
  #
data/spec/prefork_spec.rb CHANGED
@@ -9,7 +9,7 @@ if Servolux.fork?
9
9
  describe Servolux::Prefork do
10
10
 
11
11
  def pids
12
- workers.map! { |w| w.instance_variable_get(:@piper).pid }
12
+ workers.map! { |w| w.pid }
13
13
  end
14
14
 
15
15
  def workers
@@ -24,9 +24,11 @@ describe Servolux::Prefork do
24
24
  end
25
25
 
26
26
  def alive?( pid )
27
+ _, cstatus = Process.wait2( pid, Process::WNOHANG )
28
+ return false if cstatus
27
29
  Process.kill(0, pid)
28
30
  true
29
- rescue Errno::ESRCH, Errno::ENOENT
31
+ rescue Errno::ESRCH, Errno::ENOENT, Errno::ECHILD
30
32
  false
31
33
  end
32
34
 
@@ -121,6 +123,78 @@ describe Servolux::Prefork do
121
123
  pid.should_not == pids.last
122
124
  end
123
125
 
126
+ it "starts up a stopped worker" do
127
+ @prefork = Servolux::Prefork.new :module => @worker
128
+ @prefork.start 2
129
+ ary = workers
130
+ sleep 0.250 until ary.all? { |w| w.alive? }
131
+ sleep 0.250 until worker_count >= 2
132
+
133
+ pid = pids.last
134
+ ary.last.signal 'TERM'
135
+
136
+ @prefork.reap until !alive? pid
137
+ @prefork.each_worker do |worker|
138
+ worker.start unless worker.alive?
139
+ end
140
+ sleep 0.250 until ary.all? { |w| w.alive? }
141
+ pid.should_not == pids.last
142
+ end
143
+
144
+ it "adds a new worker to the worker pool" do
145
+ @prefork = Servolux::Prefork.new :module => @worker
146
+ @prefork.start 2
147
+ ary = workers
148
+ sleep 0.250 until ary.all? { |w| w.alive? }
149
+ sleep 0.250 until worker_count >= 2
150
+
151
+
152
+ @prefork.add_workers( 2 )
153
+ sleep 0.250 until worker_count >= 4
154
+ workers.size.should == 4
155
+ end
156
+
157
+ it "only adds workers up to the max_workers value" do
158
+ @prefork = Servolux::Prefork.new :module => @worker, :max_workers => 3
159
+ @prefork.start 2
160
+ ary = workers
161
+ sleep 0.250 until ary.all? { |w| w.alive? }
162
+ sleep 0.250 until worker_count >= 2
163
+
164
+ @prefork.add_workers( 2 )
165
+ sleep 0.250 until worker_count >= 3
166
+ workers.size.should == 3
167
+ end
168
+
169
+ it "prunes workers that are no longer running" do
170
+ @prefork = Servolux::Prefork.new :module => @worker
171
+ @prefork.start 2
172
+ ary = workers
173
+ sleep 0.250 until ary.all? { |w| w.alive? }
174
+ sleep 0.250 until worker_count >= 2
175
+
176
+ @prefork.add_workers( 2 )
177
+ sleep 0.250 until worker_count >= 3
178
+ workers.size.should be == 4
179
+
180
+ workers[0].stop
181
+ sleep 0.250 while workers[0].alive?
182
+
183
+ @prefork.prune_workers
184
+ workers.size.should be == 3
185
+ end
186
+
187
+ it "ensures that there are minimum number of workers" do
188
+ @prefork = Servolux::Prefork.new :module => @worker, :min_workers => 3
189
+ @prefork.start 1
190
+ ary = workers
191
+ sleep 0.250 until ary.all? { |w| w.alive? }
192
+ sleep 0.250 until worker_count >= 1
193
+
194
+ @prefork.ensure_worker_pool_size
195
+ sleep 0.250 until worker_count >= 3
196
+ workers.size.should be == 3
197
+ end
124
198
  end
125
199
  end # Servolux.fork?
126
200
 
data/spec/server_spec.rb CHANGED
@@ -2,6 +2,12 @@
2
2
  require File.expand_path('../spec_helper', __FILE__)
3
3
 
4
4
  describe Servolux::Server do
5
+
6
+ def wait_until( seconds = 5 )
7
+ start = Time.now
8
+ sleep 0.250 until (Time.now - start) > seconds or yield
9
+ end
10
+
5
11
  base = Class.new(Servolux::Server) do
6
12
  def initialize( &block )
7
13
  super('Test Server', :logger => Logging.logger['Servolux'], &block)
@@ -23,17 +29,17 @@ describe Servolux::Server do
23
29
  test(?e, @server.pid_file).should be_false
24
30
 
25
31
  t = Thread.new {@server.startup}
26
- Thread.pass until @server.running? and t.status == 'sleep'
32
+ wait_until { @server.running? and t.status == 'sleep' }
27
33
  test(?e, @server.pid_file).should be_true
28
34
 
29
35
  @server.shutdown
30
- Thread.pass until t.status == false
36
+ wait_until { t.status == false }
31
37
  test(?e, @server.pid_file).should be_false
32
38
  end
33
39
 
34
40
  it 'generates a PID file with mode rw-r----- by default' do
35
41
  t = Thread.new {@server.startup}
36
- Thread.pass until @server.running? and t.status == 'sleep'
42
+ wait_until { @server.running? and t.status == 'sleep' }
37
43
  test(?e, @server.pid_file).should be_true
38
44
 
39
45
  @log_output.readline.chomp.should be == %q(DEBUG Servolux : Server "Test Server" creating pid file "test_server.pid")
@@ -41,14 +47,14 @@ describe Servolux::Server do
41
47
  (File.stat(@server.pid_file).mode & 0777).should be == 0640
42
48
 
43
49
  @server.shutdown
44
- Thread.pass until t.status == false
50
+ wait_until { t.status == false }
45
51
  test(?e, @server.pid_file).should be_false
46
52
  end
47
53
 
48
54
  it 'generates PID file with the specified permissions' do
49
55
  @server.pid_file_mode = 0400
50
56
  t = Thread.new {@server.startup}
51
- Thread.pass until @server.running? and t.status == 'sleep'
57
+ wait_until { @server.running? and t.status == 'sleep' }
52
58
  test(?e, @server.pid_file).should be_true
53
59
 
54
60
  @log_output.readline.chomp.should be == %q(DEBUG Servolux : Server "Test Server" creating pid file "test_server.pid")
@@ -56,18 +62,18 @@ describe Servolux::Server do
56
62
  (File.stat(@server.pid_file).mode & 0777).should be == 0400
57
63
 
58
64
  @server.shutdown
59
- Thread.pass until t.status == false
65
+ wait_until { t.status == false }
60
66
  test(?e, @server.pid_file).should be_false
61
67
  end
62
68
 
63
69
  it 'shuts down gracefully when signaled' do
64
70
  t = Thread.new {@server.startup}
65
- Thread.pass until @server.running? and t.status == 'sleep'
66
- @server.running?.should be_true
71
+ wait_until { @server.running? and t.status == 'sleep' }
72
+ @server.should be_running
67
73
 
68
- Process.kill('INT', $$)
69
- Thread.pass until t.status == false
70
- @server.running?.should be_false
74
+ `kill -SIGINT #{$$}`
75
+ wait_until { t.status == false }
76
+ @server.should_not be_running
71
77
  end
72
78
 
73
79
  it 'responds to signals that have defined handlers' do
@@ -78,21 +84,31 @@ describe Servolux::Server do
78
84
  end
79
85
 
80
86
  t = Thread.new {@server.startup}
81
- Thread.pass until @server.running? and t.status == 'sleep'
87
+ wait_until { @server.running? and t.status == 'sleep' }
82
88
  @log_output.readline
83
-
84
- Process.kill('USR1', $$)
85
- @log_output.readline.strip.should be == 'INFO Servolux : usr1 was called'
86
-
87
- Process.kill('HUP', $$)
88
- @log_output.readline.strip.should be == 'INFO Servolux : hup was called'
89
-
90
- Process.kill('USR2', $$)
91
- @log_output.readline.strip.should be == 'INFO Servolux : usr2 was called'
92
-
93
- Process.kill('TERM', $$)
94
- Thread.pass until t.status == false
95
- @server.running?.should be_false
89
+ @log_output.readline.strip.should be == 'DEBUG Servolux : Starting'
90
+
91
+ line = nil
92
+ Process.kill 'SIGUSR1', $$
93
+ wait_until { line = @log_output.readline }
94
+ line.should_not be_nil
95
+ line.strip.should be == 'INFO Servolux : usr1 was called'
96
+
97
+ line = nil
98
+ Process.kill 'SIGHUP', $$
99
+ wait_until { line = @log_output.readline }
100
+ line.should_not be_nil
101
+ line.strip.should be == 'INFO Servolux : hup was called'
102
+
103
+ line = nil
104
+ Process.kill 'SIGUSR2', $$
105
+ wait_until { line = @log_output.readline }
106
+ line.should_not be_nil
107
+ line.strip.should be == 'INFO Servolux : usr2 was called'
108
+
109
+ Process.kill 'SIGTERM', $$
110
+ wait_until { t.status == false }
111
+ @server.should_not be_running
96
112
  end
97
113
  end
98
114
 
data/version.txt CHANGED
@@ -1 +1 @@
1
- 0.9.7
1
+ 0.10.0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servolux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.7
4
+ version: 0.10.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-19 00:00:00.000000000Z
12
+ date: 2012-02-18 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bones-rspec
16
- requirement: &2154204220 !ruby/object:Gem::Requirement
16
+ requirement: &2160654620 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,32 +21,32 @@ dependencies:
21
21
  version: 2.0.1
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2154204220
24
+ version_requirements: *2160654620
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bones-git
27
- requirement: &2154203600 !ruby/object:Gem::Requirement
27
+ requirement: &2160654100 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
31
31
  - !ruby/object:Gem::Version
32
- version: 1.2.4
32
+ version: 1.2.5
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *2154203600
35
+ version_requirements: *2160654100
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: logging
38
- requirement: &2154203040 !ruby/object:Gem::Requirement
38
+ requirement: &2160653660 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
42
42
  - !ruby/object:Gem::Version
43
- version: 1.6.1
43
+ version: 1.6.2
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *2154203040
46
+ version_requirements: *2160653660
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bones
49
- requirement: &2154202560 !ruby/object:Gem::Requirement
49
+ requirement: &2160653220 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,13 +54,13 @@ dependencies:
54
54
  version: 3.7.2
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *2154202560
57
+ version_requirements: *2160653220
58
58
  description: ! 'Serv-O-Lux is a collection of Ruby classes that are useful for daemon
59
59
  and
60
60
 
61
61
  process management, and for writing your own Ruby services. The code is well
62
62
 
63
- documented and tested. It works with Ruby and JRuby supporing both 1.8 and 1.9
63
+ documented and tested. It works with Ruby and JRuby supporting both 1.8 and 1.9
64
64
 
65
65
  interpreters.'
66
66
  email: tim.pease@gmail.com
@@ -77,6 +77,7 @@ files:
77
77
  - Rakefile
78
78
  - examples/beanstalk.rb
79
79
  - examples/echo.rb
80
+ - examples/preforking_server.rb
80
81
  - examples/server_beanstalk.rb
81
82
  - lib/servolux.rb
82
83
  - lib/servolux/child.rb