spawner 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +17 -0
- data/README.rdoc +62 -0
- data/Rakefile +29 -0
- data/bin/spawner +8 -0
- data/lib/spawner.rb +344 -0
- data/lib/spawner/main.rb +89 -0
- data/tasks/ann.rake +81 -0
- data/tasks/bones.rake +21 -0
- data/tasks/gem.rake +126 -0
- data/tasks/git.rake +41 -0
- data/tasks/manifest.rake +49 -0
- data/tasks/notes.rake +28 -0
- data/tasks/post_load.rake +39 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +268 -0
- metadata +72 -0
data/History.txt
ADDED
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
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
|