slave 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/slave.rb CHANGED
@@ -20,14 +20,24 @@ require 'fcntl'
20
20
  # end
21
21
  # end
22
22
  #
23
- # slave = Slave.new Server.new
23
+ # slave = Slave.new 'object' => Server.new
24
24
  # server = slave.object
25
25
  #
26
26
  # p server.add_two(40) #=> 42
27
+ #
28
+ # two other methods of providing server objects exist:
29
+ #
30
+ # a) server = Server.new "this is called the parent" }
31
+ # Slave.new(:object=>server){|s| puts "#{ s.inspect } passed to block in child process"}
32
+ #
33
+ # b) Slave.new{ Server.new "this is called only in the child" }
34
+ #
35
+ # of the two 'b' is preferred.
27
36
  #
28
37
  class Slave
29
38
  #--{{{
30
- VERSION = '0.2.0'
39
+ VERSION = '1.0.0'
40
+ def self.version() VERSION end
31
41
  #
32
42
  # config
33
43
  #
@@ -59,14 +69,27 @@ require 'fcntl'
59
69
  # on STDERR
60
70
  attr :debug, true
61
71
 
62
- # look up a value in an option hash failing back to class defaults
63
- def getval key, opts = {}
72
+ # get a default value
73
+ def default key
64
74
  #--{{{
65
- keys = [key, key.to_s, key.to_s.intern]
66
- keys.each{|k| return opts[k] if opts.has_key?(k)}
67
- send key rescue nil
75
+ send key
76
+ #--}}}
77
+ end
78
+
79
+ def getopts opts
80
+ #--{{{
81
+ raise ArgumentError, opts.class unless
82
+ opts.respond_to?('has_key?') and opts.respond_to?('[]')
83
+
84
+ lambda do |key, *defval|
85
+ defval = defval.shift
86
+ keys = [key, key.to_s, key.to_s.intern]
87
+ key = keys.detect{|k| opts.has_key? k } and break opts[key]
88
+ defval
89
+ end
68
90
  #--}}}
69
91
  end
92
+
70
93
  # just fork with out silly warnings
71
94
  def fork &block
72
95
  #--{{{
@@ -82,87 +105,126 @@ require 'fcntl'
82
105
  #--}}}
83
106
  end
84
107
 
85
- attr :object
108
+
86
109
  attr :obj
110
+ attr :socket_creation_attempts
111
+ attr :pulse_rate
112
+ attr :debug
87
113
  attr :psname
114
+ attr :at_exit
115
+
116
+ attr :shutdown
117
+ attr :status
118
+ attr :object
88
119
  attr :pid
89
120
  attr :ppid
90
121
  attr :uri
91
- attr :pulse_rate
92
122
  attr :socket
93
- attr :debug
94
- attr :status
95
123
 
96
124
  #
97
- # 'obj' can be any object and 'opts' may contain the keys
98
- # 'socket_creation_attempts', 'pulse_rate', 'psname', or 'debug'
125
+ # opts may contain the keys 'object', 'socket_creation_attempts',
126
+ # 'pulse_rate', 'psname', 'dumped', or 'debug'
99
127
  #
100
- def initialize obj = nil, opts = {}, &block
128
+ def initialize opts = {}, &block
101
129
  #--{{{
102
- raise ArgumentError, "no slave object!" if
103
- obj.nil? and block.nil?
130
+ getopt = getopts opts
104
131
 
105
- @obj = obj
132
+ @obj = getopt['object']
133
+ @socket_creation_attempts = getopt['socket_creation_attempts'] || default('socket_creation_attempts')
134
+ @pulse_rate = getopt['pulse_rate'] || default('pulse_rate')
135
+ @debug = getopt['debug'] || default('debug')
136
+ @psname = getopt['psname']
137
+ @at_exit = getopt['at_exit']
138
+ @dumped = getopt['dumped']
106
139
 
107
- @socket_creation_attempts = getval('socket_creation_attempts', opts)
108
- @pulse_rate = getval('pulse_rate', opts)
109
- @debug = getval('debug', opts)
110
- @psname = getval('psname', opts) || gen_psname(@obj)
111
-
112
- trace{ "socket_creation_attempts <#{ @socket_creation_attempts }>" }
113
- trace{ "pulse_rate <#{ @pulse_rate }>" }
114
- trace{ "psname <#{ @psname }>" }
140
+ raise ArgumentError, 'no slave object!' if
141
+ @obj.nil? and block.nil?
115
142
 
116
143
  @shutdown = false
117
144
  @waiter = @status = nil
118
145
 
119
146
  @heartbeat = Heartbeat::new @pulse_rate, @debug
120
147
  @r, @w = IO::pipe
148
+ @r2, @w2 = IO::pipe
149
+
150
+ # weird syntax because dot/rdoc chokes on this!?!?
151
+ init_failure = lambda do |e|
152
+ o = Object.new
153
+ class << o
154
+ attr_accessor '__slave_object_failure__'
155
+ end
156
+ o.__slave_object_failure__ = Marshal.dump [e.class, e.message, e.backtrace]
157
+ @object = o
158
+ end
159
+
121
160
  #
122
161
  # child
123
162
  #
124
163
  unless((@pid = Slave::fork))
125
164
  e = nil
126
165
  begin
127
- $0 = @psname
128
- @pid = Process::pid
129
- @ppid = Process::ppid
166
+ Kernel.at_exit{ Kernel.exit! }
167
+
168
+ if @obj
169
+ @object = @obj
170
+ else
171
+ begin
172
+ @object = block.call
173
+ rescue Exception => e
174
+ init_failure[e]
175
+ end
176
+ end
177
+
178
+ if block and @obj
179
+ begin
180
+ block[@obj]
181
+ rescue Exception => e
182
+ init_failure[e]
183
+ end
184
+ end
185
+
186
+ $0 = (@psname ||= gen_psname(@object))
187
+ unless @dumped or @object.respond_to?('__slave_object_failure__')
188
+ @object.extend DRbUndumped
189
+ end
190
+
191
+ @ppid, @pid = Process::ppid, Process::pid
130
192
 
131
193
  @r.close
194
+ @r2.close
132
195
  @socket = nil
133
196
  @uri = nil
134
197
 
135
- tmpdir = Dir::tmpdir
136
- basename = File::basename @psname
137
-
138
- server = @obj || block.call
198
+ tmpdir, basename = Dir::tmpdir, File::basename(@psname)
139
199
 
140
200
  @socket_creation_attempts.times do |attempt|
201
+ se = nil
141
202
  begin
142
203
  s = File::join(tmpdir, "#{ basename }_#{ attempt }")
143
204
  u = "drbunix://#{ s }"
144
- DRb::start_service u, server
205
+ DRb::start_service u, @object
145
206
  @socket = s
146
207
  @uri = u
147
208
  trace{ "child - socket <#{ @socket }>" }
148
209
  trace{ "child - uri <#{ @uri }>" }
149
210
  break
150
- rescue Errno::EADDRINUSE
211
+ rescue Errno::EADDRINUSE => se
151
212
  nil
152
213
  end
153
214
  end
154
215
 
155
216
  if @socket and @uri
156
217
  @heartbeat.start
157
- @w.write @socket
158
- @w.close
218
+
159
219
  trap('SIGUSR2') do
160
220
  # @heartbeat.stop rescue nil
161
221
  DBb::thread.kill rescue nil
162
222
  FileUtils::rm_f @socket rescue nil
163
- exit!
223
+ exit
164
224
  end
165
- block[obj] if block and obj
225
+
226
+ @w.write @socket
227
+ @w.close
166
228
  DRb::thread.join
167
229
  else
168
230
  @w.close
@@ -171,22 +233,36 @@ require 'fcntl'
171
233
  trace{ %Q[#{ e.message } (#{ e.class })\n#{ e.backtrace.join "\n" }] }
172
234
  ensure
173
235
  status = e.respond_to?('status') ? e.status : 1
174
- exit!(status)
236
+ exit(status)
175
237
  end
176
238
  #
177
239
  # parent
178
240
  #
179
241
  else
180
- #Process::detach @pid
181
242
  detach
182
243
  @w.close
244
+ @w2.close
183
245
  @socket = @r.read
184
246
  @r.close
185
247
 
186
248
  trace{ "parent - socket <#{ @socket }>" }
187
249
 
250
+ if @at_exit
251
+ @at_exit_thread = Thread.new{
252
+ Thread.current.abort_on_exception = true
253
+
254
+ @r2.read rescue 42
255
+
256
+ if @at_exit.respond_to? 'call'
257
+ @at_exit.call self
258
+ else
259
+ send @at_exit.to_s, self
260
+ end
261
+ }
262
+ end
263
+
188
264
  if @socket and File::exist? @socket
189
- at_exit{ FileUtils::rm_f @socket }
265
+ Kernel.at_exit{ FileUtils::rm_f @socket }
190
266
  @uri = "drbunix://#{ socket }"
191
267
  trace{ "parent - uri <#{ @uri }>" }
192
268
  @heartbeat.start
@@ -195,6 +271,12 @@ require 'fcntl'
195
271
  #
196
272
  DRb::start_service('druby://localhost:0', nil) unless DRb::thread
197
273
  @object = DRbObject::new nil, @uri
274
+ if @object.respond_to? '__slave_object_failure__'
275
+ c, m, bt = Marshal.load @object.__slave_object_failure__
276
+ (e = c.new(m)).set_backtrace bt
277
+ raise e
278
+ end
279
+ @psname ||= gen_psname(@object)
198
280
  else
199
281
  raise "failed to find slave socket <#{ @socket }>"
200
282
  end
@@ -202,34 +284,70 @@ require 'fcntl'
202
284
  #--}}}
203
285
  end
204
286
  #
205
- # starts a thread to attempt collecting the child status
287
+ # starts a thread to collect the child status and sets up at_exit handler to
288
+ # prevent zombies. the at_exit handler is canceled if the thread is able to
289
+ # collect the status
206
290
  #
207
291
  def detach
208
292
  #--{{{
209
- @waiter =
210
- Thread.new{ @status = Process::waitpid2(@pid).last }
293
+ reap = lambda do |cid|
294
+ begin
295
+ @status = Process::waitpid2(cid).last
296
+ rescue Exception => e
297
+ m, c, b = e.message, e.class, e.backtrace.join("\n")
298
+ warn "#{ m } (#{ c })\n#{ b }" unless e.is_a? Errno::ECHILD
299
+ end
300
+ end
301
+
302
+ Kernel.at_exit do
303
+ shutdown rescue nil
304
+ reap[@pid] rescue nil
305
+ end
306
+
307
+ @waiter =
308
+ Thread.new do
309
+ begin
310
+ @status = Process::waitpid2(@pid).last
311
+ ensure
312
+ reap = lambda{|cid| 'no-op' }
313
+ end
314
+ end
211
315
  #--}}}
212
316
  end
213
317
  #
214
- # wait for slave to finish
318
+ # wait for slave to finish. if the keyword 'non_block'=>true is given a
319
+ # thread is returned to do the waiting in an async fashion. eg
215
320
  #
216
- def wait
321
+ # thread = slave.wait(:non_block=>true){|value| "background <#{ value }>"}
322
+ #
323
+ def wait opts = {}, &b
217
324
  #--{{{
218
- @waiter.value
325
+ b ||= lambda{|exit_status|}
326
+ non_block = getopts(opts)['non_block']
327
+ non_block ? Thread.new{ b[ @waiter.value ] } : b[ @waiter.value ]
219
328
  #--}}}
220
329
  end
221
330
  alias :wait2 :wait
222
331
  #
223
- # stops the heartbeat thread and kills the child process
332
+ # stops the heartbeat thread and kills the child process - give the key
333
+ # 'quiet' to ignore errors shutting down, including having already shutdown
224
334
  #
225
- def shutdown
335
+ def shutdown opts = {}
226
336
  #--{{{
227
- raise "already shutdown" if @shutdown
228
- @heartbeat.stop rescue nil
229
- Process::kill('SIGUSR2', @pid) rescue nil
230
- Process::kill('SIGTERM', @pid) rescue nil
231
- FileUtils::rm_f @socket
337
+ quiet = getopts(opts)['quiet']
338
+ raise "already shutdown" if @shutdown unless quiet
339
+ failure = lambda{ raise $! unless quiet }
340
+ @heartbeat.stop rescue failure.call
341
+ Process::kill('SIGUSR2', @pid) rescue failure.call
232
342
  @shutdown = true
343
+ #--}}}
344
+ end
345
+ #
346
+ # true
347
+ #
348
+ def shutdown?
349
+ #--{{{
350
+ @shutdown
233
351
  #--}}}
234
352
  end
235
353
  #
@@ -237,15 +355,23 @@ require 'fcntl'
237
355
  #
238
356
  def gen_psname obj
239
357
  #--{{{
240
- "#{ obj.class }_slave_of_#{ Process::pid }".downcase.gsub(%r/\s+/,'_')
358
+ "#{ obj.class }_#{ obj.object_id }_#{ Process::ppid }_#{ Process::pid }".downcase.gsub(%r/\s+/,'_')
359
+ #--}}}
360
+ end
361
+ #
362
+ # see docs for Slave.default
363
+ #
364
+ def default key
365
+ #--{{{
366
+ self.class.default key
241
367
  #--}}}
242
368
  end
243
369
  #
244
- # see docs for Slave.getval
370
+ # see docs for Slave.getopts
245
371
  #
246
- def getval key, opts = {}
372
+ def getopts opts
247
373
  #--{{{
248
- self.class.getval key
374
+ self.class.getopts opts
249
375
  #--}}}
250
376
  end
251
377
  #
@@ -256,7 +382,6 @@ require 'fcntl'
256
382
  STDERR.puts(yield) if @debug and STDERR.tty?
257
383
  #--}}}
258
384
  end
259
-
260
385
  #
261
386
  # the Heartbeat class is essentially wrapper over an IPC channel that sends
262
387
  # a ping on the channel indicating process health. if either end of the
@@ -380,7 +505,7 @@ require 'fcntl'
380
505
  @pipe.read
381
506
  trace{ "child read." }
382
507
  trace{ "child exiting." }
383
- exit!
508
+ exit
384
509
  rescue => e
385
510
  cur.raise e
386
511
  ensure
@@ -1,19 +1,16 @@
1
1
  require 'slave'
2
-
3
2
  #
4
3
  # simple usage is simply to stand up a server object as a slave. you do not
5
4
  # need to wait for the server, join it, etc. it will die when the parent
6
5
  # process dies - even under 'kill -9' conditions
7
6
  #
8
-
9
- class Server
10
- def add_two n
11
- n + 2
7
+ class Server
8
+ def add_two n
9
+ n + 2
10
+ end
12
11
  end
13
- end
14
-
15
- slave = Slave.new Server.new
16
- server = slave.object
17
12
 
18
- p server.add_two(40) #=> 42
13
+ slave = Slave.new :object => Server.new
14
+ server = slave.object
19
15
 
16
+ p server.add_two(40) #=> 42
data/samples/b.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'slave'
2
+ #
3
+ # if certain operations need to take place in the child only a block can be
4
+ # used
5
+ #
6
+ class Server
7
+ def connect_to_db
8
+ "we only want to do this in the child process!"
9
+ @connection = :postgresql
10
+ end
11
+ attr :connection
12
+ end
13
+
14
+ slave = Slave.new('object' => Server.new){|s| s.connect_to_db}
15
+
16
+ server = slave.object
17
+
18
+ p server.connection #=> :postgresql
19
+ #
20
+ # errors in the child are detected and raised in the parent
21
+ #
22
+ slave = Slave.new('object' => Server.new){|s| s.typo} #=> raises an error!
data/samples/c.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'slave'
2
+ #
3
+ # if no slave object is given the block itself is used to contruct it
4
+ #
5
+ class Server
6
+ def initialize
7
+ "this is run only in the child"
8
+ @pid = Process.pid
9
+ end
10
+ attr 'pid'
11
+ end
12
+
13
+ slave = Slave.new{ Server.new }
14
+ server = slave.object
15
+
16
+ p Process.pid
17
+ p server.pid # not going to be the same as parents!
18
+ #
19
+ # errors are still detected though
20
+ #
21
+ slave = Slave.new{ fubar } # raises error in parent