spawner 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2008-09-14
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
data/Manifest.txt ADDED
@@ -0,0 +1,17 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ bin/spawner
6
+ lib/spawner.rb
7
+ lib/spawner/main.rb
8
+ tasks/ann.rake
9
+ tasks/bones.rake
10
+ tasks/gem.rake
11
+ tasks/git.rake
12
+ tasks/manifest.rake
13
+ tasks/notes.rake
14
+ tasks/post_load.rake
15
+ tasks/rdoc.rake
16
+ tasks/rubyforge.rake
17
+ tasks/setup.rb
data/README.rdoc ADDED
@@ -0,0 +1,62 @@
1
+ Spawner
2
+ by Tim Pease
3
+ http://codeforpeople.rubyforge.org/spawner
4
+
5
+ == DESCRIPTION:
6
+
7
+ Spawn multiple child processes from Ruby and re-spawn those processes if they die.
8
+
9
+ Spawner works on Mac OS X, Linux, Windows, Solaris, AIX -- anywhere that Ruby can run. It is useful for load testing other applications or just keeping things alive. The Spawner class allows the number of child processes to be changed dynamiclly so they can be brought up and down as needed without restarting the spawner.
10
+
11
+ There is also a handy command line spawner app that's really useful for load testing services -- spawn twenty clients and see how things hold up.
12
+
13
+ == SYNOPSIS:
14
+
15
+ Start three 'foo' processes and re-spawn immediately when one dies.
16
+
17
+ spawner = Spawner.new( 'foo', :spawn => 3 )
18
+ spawner.start
19
+
20
+ Start two 'bar' processes, pause for 10 seconds before re-spawning, and capture output to a file.
21
+
22
+ spawner = Spawner.new( 'bar', :spawn = 2, :pause => 10, :stdout => 'stdout.txt' )
23
+ spawner.start
24
+
25
+ Start 10 'baz' process and kill off one process each minute until none are left running.
26
+
27
+ spawner = Spawner.new( 'baz', :spawn => 10 )
28
+ spawner.start
29
+
30
+ until (spawner.spawn == 0)
31
+ sleep 60
32
+ spawner.spawn -= 1
33
+ end
34
+
35
+ == INSTALL:
36
+
37
+ sudo gem install spawner
38
+
39
+ == LICENSE:
40
+
41
+ The MIT License
42
+
43
+ Copyright (c) 2008
44
+
45
+ Permission is hereby granted, free of charge, to any person obtaining
46
+ a copy of this software and associated documentation files (the
47
+ 'Software'), to deal in the Software without restriction, including
48
+ without limitation the rights to use, copy, modify, merge, publish,
49
+ distribute, sublicense, and/or sell copies of the Software, and to
50
+ permit persons to whom the Software is furnished to do so, subject to
51
+ the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be
54
+ included in all copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
57
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
58
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
59
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
60
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
61
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
62
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ load 'tasks/setup.rb'
6
+
7
+ ensure_in_path 'lib'
8
+ require 'spawner'
9
+
10
+ task :default => 'manifest'
11
+
12
+ PROJ.name = 'spawner'
13
+ PROJ.authors = 'Tim Pease'
14
+ PROJ.email = 'tim.pease@gmail.com'
15
+ PROJ.url = 'http://codeforpeople.rubyforge.org/spawner'
16
+ PROJ.version = Spawner::VERSION
17
+ PROJ.rubyforge.name = 'codeforpeople'
18
+ PROJ.readme_file = 'README.rdoc'
19
+
20
+ PROJ.rdoc.main = 'README.rdoc'
21
+ PROJ.rdoc.include << 'README.rdoc'
22
+ PROJ.rdoc.remote_dir = 'spawner'
23
+
24
+ PROJ.ann.email[:server] = 'smtp.gmail.com'
25
+ PROJ.ann.email[:port] = 587
26
+
27
+ task 'gem:package' => 'manifest:assert'
28
+
29
+ # EOF
data/bin/spawner ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib spawner main]))
5
+
6
+ Spawner::Main.run ARGV
7
+
8
+ # EOF
data/lib/spawner.rb ADDED
@@ -0,0 +1,344 @@
1
+ require 'rbconfig'
2
+ require 'thread'
3
+ require 'tempfile'
4
+
5
+ begin require 'fastthread'; rescue LoadError; end
6
+
7
+ # == Synopsis
8
+ #
9
+ # A class for spawning child processes and ensuring those children continue
10
+ # running.
11
+ #
12
+ # == Details
13
+ #
14
+ # When a spawner is created it is given the command to run in a child
15
+ # process. This child process has +stdin+, +stdout+, and +stderr+ redirected
16
+ # to +/dev/null+ (this works even on Windows). When the child dies for any
17
+ # reason, the spawner will restart a new child process in the exact same
18
+ # manner as the original.
19
+ #
20
+ class Spawner
21
+
22
+ VERSION = '1.0.0'
23
+
24
+ @dev_null = test(?e, "/dev/null") ? "/dev/null" : "NUL:"
25
+
26
+ c = ::Config::CONFIG
27
+ ruby = File.join(c['bindir'], c['ruby_install_name']) << c['EXEEXT']
28
+ @ruby = if system('%s -e exit' % ruby) then ruby
29
+ elsif system('ruby -e exit') then 'ruby'
30
+ else warn 'no ruby in PATH/CONFIG'
31
+ end
32
+
33
+ class << self
34
+ attr_reader :ruby
35
+ attr_reader :dev_null
36
+
37
+ def finalizer( cids )
38
+ pid = $$
39
+ lambda do
40
+ break unless pid == $$
41
+ cids.kill 'TERM', :all
42
+ end # lambda
43
+ end # finalizer
44
+
45
+ def version
46
+ VERSION
47
+ end
48
+ end
49
+
50
+ # call-seq:
51
+ # Spawner.new( command, *args, opts = {} )
52
+ #
53
+ # Creates a new spawner that will execute the given external _command_ in
54
+ # a sub-process. The calling semantics of <code>Kernel::exec</code> are
55
+ # used to execute the _command_. Any number of optional _args_ can be
56
+ # passed to the _command_.
57
+ #
58
+ # Available options:
59
+ #
60
+ # :spawn => the number of child processes to spawn
61
+ # :pause => wait time (in seconds) before respawning after termination
62
+ # :ruby => the Ruby interpreter to use when spawning children
63
+ # :env => a hash for the child process environment
64
+ # :cwd => the current working directory to use for the child process
65
+ # :stdin => stdin child processes will read from
66
+ # :stdout => stdout child processes will write to
67
+ # :stderr => stderr child processes will write to
68
+ #
69
+ # The <code>:env</code> option is used to add environemnt variables to
70
+ # child processes when they are spawned.
71
+ #
72
+ # *Note:* all spawned child processes will use the same stdin, stdout, and
73
+ # stderr if they are given in the options. Otherwise they all default to
74
+ # <code>/dev/null</code> on *NIX and <code>NUL:</code> on Windows.
75
+ #
76
+ def initialize( *args )
77
+ config = {
78
+ :ruby => self.class.ruby,
79
+ :spawn => 1,
80
+ :pause => 0,
81
+ :stdin => self.class.dev_null,
82
+ :stdout => self.class.dev_null,
83
+ :stderr => self.class.dev_null
84
+ }
85
+ config.merge! args.pop if Hash === args.last
86
+ config[:argv] = args
87
+
88
+ raise ArgumentError, 'wrong number of arguments' if args.empty?
89
+
90
+ @stop = true
91
+ @cids = []
92
+ @group = ThreadGroup.new
93
+
94
+ @spawn = config.delete(:spawn)
95
+ @pause = config.delete(:pause)
96
+ @ruby = config.delete(:ruby)
97
+
98
+ @tmp = child_program(config)
99
+
100
+ class << @cids
101
+ # call-seq:
102
+ # sync {block}
103
+ #
104
+ # Executes the given block in a synchronized fashion -- i.e. only a
105
+ # single thread can execute at a time. Uses Mutex under the hood.
106
+ #
107
+ def sync(&b)
108
+ @mutex ||= Mutex.new
109
+ @mutex.synchronize(&b)
110
+ end
111
+
112
+ # call-seq:
113
+ # kill( signal, num ) => number killed
114
+ # kill( signal, :all ) => number killed
115
+ #
116
+ # Send the _signal_ to a given _num_ of child processes or all child
117
+ # processes if <code>:all</code> is given instead of a number. Returns
118
+ # the number of child processes killed.
119
+ #
120
+ def kill( signal, arg )
121
+ return if empty?
122
+
123
+ ary = sync do
124
+ case arg
125
+ when :all; self.dup
126
+ when Integer; self.slice(0,arg)
127
+ else raise ArgumentError end
128
+ end
129
+
130
+ ary.each do |cid|
131
+ begin
132
+ Process.kill(signal, cid)
133
+ rescue SystemCallError
134
+ sync {delete cid}
135
+ end
136
+ end
137
+ ary.length
138
+ end # def kill
139
+ end # class << @cids
140
+
141
+ end # def initialize
142
+
143
+ attr_reader :spawn
144
+ attr_accessor :pause
145
+
146
+ # call-seq:
147
+ # spawner.spawn = num
148
+ #
149
+ # Set the number of child processes to spawn. If the new spawn number is
150
+ # less than the current number, then spawner threads will die
151
+ #
152
+ def spawn=( num )
153
+ num = num.abs
154
+ diff, @spawn = num - @spawn, num
155
+ return unless running?
156
+
157
+ if diff > 0
158
+ diff.times {_spawn}
159
+ elsif diff < 0
160
+ @cids.kill 'TERM', diff.abs
161
+ end
162
+ end
163
+
164
+ # call-seq:
165
+ # start => self
166
+ #
167
+ # Spawn the sub-processes.
168
+ #
169
+ def start
170
+ return self if running?
171
+ @stop = false
172
+
173
+ @cleanup = Spawner.finalizer(@cids)
174
+ ObjectSpace.define_finalizer(self, @cleanup)
175
+
176
+ @spawn.times {_spawn}
177
+ self
178
+ end
179
+
180
+ # call-seq:
181
+ # stop( timeout = 5 ) => self
182
+ #
183
+ # Stop any spawned sub-processes.
184
+ #
185
+ def stop( timeout = 5 )
186
+ return self unless running?
187
+ @stop = true
188
+
189
+ @cleanup.call
190
+ ObjectSpace.undefine_finalizer(self)
191
+
192
+ # the cleanup call sends SIGTERM to all the child processes
193
+ # however, some might still be hanging around, so we are going to wait
194
+ # for a timeout interval and then send a SIGKILL to any remaining child
195
+ # processes
196
+ nap_time = 0.05 * timeout # sleep for 5% of the timeout interval
197
+ timeout = Time.now + timeout
198
+
199
+ until @cids.empty?
200
+ sleep nap_time
201
+ unless Time.now < timeout
202
+ @cids.kill 'KILL', :all
203
+ @cids.clear
204
+ @group.list.each {|t| t.kill}
205
+ break
206
+ end
207
+ end
208
+
209
+ self
210
+ end
211
+
212
+ # call-seq:
213
+ # restart( timeout = 5 )
214
+ #
215
+ def restart( timeout = 5 )
216
+ stop( timeout )
217
+ start
218
+ end
219
+
220
+ # call-seq:
221
+ # running?
222
+ #
223
+ # Returns +true+ if the spawner is currently running; returns +false+
224
+ # otherwise.
225
+ #
226
+ def running?
227
+ !@stop
228
+ end
229
+
230
+ # call-seq:
231
+ # join( timeout = nil ) => spawner or nil
232
+ #
233
+ # The calling thread will suspend execution until all child processes have
234
+ # been stopped. Does not return until all spawner threads have exited (the
235
+ # child processes have been stopped) or until _timeout seconds have
236
+ # passed. If the timeout expires +nil+ will be returned; otherwise the
237
+ # spawner is returned.
238
+ #
239
+ def join( limit = nil )
240
+ loop do
241
+ t = @group.list.first
242
+ break if t.nil?
243
+ return nil unless t.join(limit)
244
+ end
245
+ self
246
+ end
247
+
248
+
249
+ private
250
+
251
+ # call-seq:
252
+ # _spawn => thread
253
+ #
254
+ # Creates a thread that will spawn the sub-process via
255
+ # <code>IO::popen</code>. If the sub-process terminates, it will be
256
+ # respawned until the +stop+ message is sent to this spawner.
257
+ #
258
+ # If an Exception is encountered during the spawning process, a message
259
+ # will be printed to stderr and the thread will exit.
260
+ #
261
+ def _spawn
262
+ t = Thread.new do
263
+ catch(:die) do
264
+ loop do
265
+ begin
266
+ io = IO.popen("#{@ruby} #{@tmp.path}", 'r')
267
+ cid = io.gets.to_i
268
+
269
+ @cids.sync {@cids << cid} if cid > 0
270
+ Process.wait cid
271
+ rescue Exception => e
272
+ STDERR.puts e.inspect
273
+ STDERR.puts e.backtrace.join("\n")
274
+ throw :die
275
+ ensure
276
+ io.close rescue nil
277
+ @cids.sync {
278
+ @cids.delete cid
279
+ throw :die unless @cids.length < @spawn
280
+ }
281
+ end
282
+
283
+ throw :die if @stop
284
+ sleep @pause
285
+
286
+ end # loop
287
+ end # catch(:die)
288
+ end # Thread.new
289
+
290
+ @group.add t
291
+ t
292
+ end
293
+
294
+ # call-seq:
295
+ # child_program( config ) => tempfile
296
+ #
297
+ # Creates a child Ruby program based on the given _config_ hash. The
298
+ # following hash keys are used:
299
+ #
300
+ # :argv => command and arguments passed to <code>Kernel::exec</code>
301
+ # :env => environment variables for the child process
302
+ # :cwd => the current working directory to use for the child process
303
+ # :stdin => stdin the child process will read from
304
+ # :stdout => stdout the child process will write to
305
+ # :stderr => stderr the child process will write to
306
+ #
307
+ def child_program( config )
308
+ config = Marshal.dump(config)
309
+
310
+ tmp = Tempfile.new(self.class.name.downcase)
311
+ tmp.write <<-PROG
312
+ begin
313
+ config = Marshal.load(#{config.inspect})
314
+
315
+ argv = config[:argv]
316
+ env = config[:env]
317
+ cwd = config[:cwd]
318
+ stdin = config[:stdin]
319
+ stdout = config[:stdout]
320
+ stderr = config[:stderr]
321
+
322
+ Dir.chdir cwd if cwd
323
+ env.each {|k,v| ENV[k.to_s] = v.to_s} if env
324
+ rescue Exception => e
325
+ STDERR.warn e
326
+ abort
327
+ end
328
+
329
+ STDOUT.puts Process.pid
330
+ STDOUT.flush
331
+
332
+ STDIN.reopen stdin, 'r'
333
+ STDOUT.reopen stdout, 'a'
334
+ STDERR.reopen stderr, 'a'
335
+
336
+ exec *argv
337
+ PROG
338
+
339
+ tmp.close
340
+ tmp
341
+ end
342
+ end # class Spawner
343
+
344
+ # EOF