slave 1.0.0 → 1.1.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/lib/slave.rb CHANGED
@@ -3,16 +3,20 @@ require 'fileutils'
3
3
  require 'tmpdir'
4
4
  require 'tempfile'
5
5
  require 'fcntl'
6
+ require 'socket'
7
+ require 'sync'
8
+
9
+ # TODO - lifeline need close-on-exec set in it!
6
10
 
7
11
  #
8
12
  # the Slave class encapsulates the work of setting up a drb server in another
9
- # process running on localhost. the slave process is attached to it's parent
10
- # via a Heartbeat which is designed such that the slave cannot out-live it's
11
- # parent and become a zombie, even if the parent dies and early death, such as
12
- # by 'kill -9'. the concept and purpose of the Slave class is to be able to
13
- # setup any server object in another process so easily that using a
14
- # multi-process, drb/ipc, based design is as easy, or easier, than a
15
- # multi-threaded one. eg
13
+ # process running on localhost via unix domain sockets. the slave process is
14
+ # attached to it's parent via a LifeLine which is designed such that the slave
15
+ # cannot out-live it's parent and become a zombie, even if the parent dies and
16
+ # early death, such as by 'kill -9'. the concept and purpose of the Slave
17
+ # class is to be able to setup any server object in another process so easily
18
+ # that using a multi-process, drb/ipc, based design is as easy, or easier,
19
+ # than a multi-threaded one. eg
16
20
  #
17
21
  # class Server
18
22
  # def add_two n
@@ -36,23 +40,20 @@ require 'fcntl'
36
40
  #
37
41
  class Slave
38
42
  #--{{{
39
- VERSION = '1.0.0'
43
+ VERSION = '1.1.0'
40
44
  def self.version() VERSION end
41
45
  #
42
- # config
46
+ # env config
47
+ #
48
+ DEFAULT_SOCKET_CREATION_ATTEMPTS = Integer(ENV['SLAVE_SOCKET_CREATION_ATTEMPTS'] || 42)
49
+ DEFAULT_DEBUG = (ENV['SLAVE_DEBUG'] ? true : false)
50
+ DEFAULT_THREADSAFE = (ENV['SLAVE_THREADSAFE'] ? true : false)
51
+ #
52
+ # class initialization
43
53
  #
44
- DEFAULT_SOCKET_CREATION_ATTEMPTS =
45
- Integer(ENV['SLAVE_SOCKET_CREATION_ATTEMPTS'] || 42)
46
-
47
- DEFAULT_PULSE_RATE =
48
- Float(ENV['SLAVE_PULSE_RATE'] || 8)
49
-
50
- DEFAULT_DEBUG =
51
- (ENV['SLAVE_DEBUG'] ? true : false)
52
-
53
54
  @socket_creation_attempts = DEFAULT_SOCKET_CREATION_ATTEMPTS
54
- @pulse_rate = DEFAULT_PULSE_RATE
55
55
  @debug = DEFAULT_DEBUG
56
+ @threadsafe = DEFAULT_THREADSAFE
56
57
  #
57
58
  # class methods
58
59
  #
@@ -62,13 +63,16 @@ require 'fcntl'
62
63
  # socket
63
64
  attr :socket_creation_attempts, true
64
65
 
65
- # defined the rate of pinging in the Heartbeat object
66
- attr :pulse_rate, true
67
-
68
66
  # if this is true and you are running from a terminal information is printed
69
67
  # on STDERR
70
68
  attr :debug, true
71
69
 
70
+ # if this is true all slave objects will be wrapped such that any call
71
+ # to the object is threadsafe. if you do not use this you must ensure
72
+ # that your objects are threadsafe __yourself__ as this is required of
73
+ # any object acting as a drb server
74
+ attr :threadsafe, true
75
+
72
76
  # get a default value
73
77
  def default key
74
78
  #--{{{
@@ -105,13 +109,174 @@ require 'fcntl'
105
109
  #--}}}
106
110
  end
107
111
 
112
+ #
113
+ # helper classes
114
+ #
115
+
116
+ #
117
+ # ThreadSafe is a delegate wrapper class used for implementing gross thread
118
+ # safety around existing objects. when an object is wrapped with this class
119
+ # as
120
+ #
121
+ # ts = ThreadSafe.new{ AnyObject.new }
122
+ #
123
+ # then ts can be used exactly as the normal object would have been, only all
124
+ # calls are now thread safe. this is the mechanism behind the
125
+ # 'threadsafe'/:threadsafe keyword to Slave#initialize
126
+ #
127
+ class ThreadSafe
128
+ #--{{{
129
+ instance_methods.each{|m| undef_method unless m[%r/__/]}
130
+ def initialize object
131
+ @object = object
132
+ @sync = Sync.new
133
+ end
134
+ def ex
135
+ @sync.synchronize{ yield }
136
+ end
137
+ def method_missing m, *a, &b
138
+ ex{ @object.send m, *a, &b }
139
+ end
140
+ def respond_to? m
141
+ ex{ @object.respond_to? m }
142
+ end
143
+ def inspect
144
+ ex{ @object.inspect }
145
+ end
146
+ def class
147
+ ex{ @object.class }
148
+ end
149
+ #--}}}
150
+ end
151
+ #
152
+ # a simple thread safe hash used to map object_id to a set of file
153
+ # descriptors in the LifeLine class. see LifeLine::FDS
154
+ #
155
+ class ThreadSafeHash < Hash
156
+ def self.new(*a, &b) ThreadSafe.new(super) end
157
+ end
158
+ #
159
+ # the LifeLine class is used to communitacte between child and parent
160
+ # processes and to prevent child processes from ever becoming zombies or
161
+ # otherwise abandoned by their parents. the basic concept is that a socket
162
+ # pair is setup between child and parent. the child process, because it is
163
+ # a Slave, sets up a handler such that, should it's socket ever grow stale,
164
+ # will exit the process. this class replaces the HeartBeat class from
165
+ # previous Slave versions.
166
+ #
167
+ class LifeLine
168
+ #--{{{
169
+ FDS = ThreadSafeHash.new
170
+
171
+ def initialize
172
+ @pair = Socket.pair Socket::AF_UNIX, Socket::SOCK_STREAM, 0
173
+ @owner = Process.pid
174
+ @pid = nil
175
+ @socket = nil
176
+ @object_id = object_id
177
+
178
+ @fds = @pair.map{|s| s.fileno}
179
+ oid, fds = @object_id, @fds
180
+ FDS[oid] = fds
181
+ ObjectSpace.define_finalizer(self){ FDS.delete oid }
182
+ end
183
+
184
+ def owner?
185
+ Process.pid == @owner
186
+ end
187
+
188
+ def throw *ignored
189
+ raise unless owner?
190
+ @pair[-1].close
191
+ @pair[-1] = nil
192
+ @pid = Process.pid
193
+ @socket = @pair[0]
194
+ @socket.sync = true
195
+ end
108
196
 
197
+ def catch *ignored
198
+ raise if owner?
199
+ @pair[0].close
200
+ @pair[0] = nil
201
+ @pid = Process.pid
202
+ @socket = @pair[-1]
203
+ @socket.sync = true
204
+ close_unused_sockets_after_forking
205
+ end
206
+
207
+ def close_unused_sockets_after_forking
208
+ begin
209
+ to_delete = []
210
+ begin
211
+ FDS.each do |oid, fds|
212
+ next if oid == @object_id
213
+ begin
214
+ IO.for_fd(fds.first).close
215
+ rescue Exception => e
216
+ STDERR.puts "#{ e.message } (#{ e.class })\n#{ e.backtrace.join 10.chr }"
217
+ ensure
218
+ to_delete << oid
219
+ end
220
+ end
221
+ ensure
222
+ FDS.ex{ to_delete.each{|oid| FDS.delete oid rescue 42} }
223
+ end
224
+ GC.start
225
+ rescue Exception => e
226
+ 42
227
+ end
228
+ end
229
+
230
+ def cut
231
+ raise unless owner?
232
+ raise unless @socket
233
+ @socket.close rescue nil
234
+ FDS.delete object_id
235
+ end
236
+ alias_method "release", "cut"
237
+
238
+ DELEGATED = %w( puts gets read write close flush each )
239
+
240
+ DELEGATED.each do |m|
241
+ code = <<-code
242
+ def #{ m }(*a, &b)
243
+ raise unless @socket
244
+ @socket.#{ m } *a, &b
245
+ end
246
+ code
247
+ module_eval code, __FILE__, __LINE__
248
+ end
249
+
250
+ def on_cut &b
251
+ at_exit{ begin; b.call; ensure; b = nil; end if b}
252
+ Thread.new(Thread.current){|current|
253
+ Thread.current.abort_on_exception = true
254
+ begin
255
+ each{|*a|}
256
+ rescue Exception
257
+ current.raise $!
258
+ 42
259
+ ensure
260
+ begin; b.call; ensure; b = nil; end if b
261
+ end
262
+ }
263
+ end
264
+
265
+ def cling &b
266
+ on_cut{ begin; b.call if b; ensure; Kernel.exit; end }.join
267
+ end
268
+ #--}}}
269
+ end
270
+
271
+ #
272
+ # attrs
273
+ #
109
274
  attr :obj
110
275
  attr :socket_creation_attempts
111
- attr :pulse_rate
112
276
  attr :debug
113
277
  attr :psname
114
278
  attr :at_exit
279
+ attr :dumped
115
280
 
116
281
  attr :shutdown
117
282
  attr :status
@@ -120,10 +285,37 @@ require 'fcntl'
120
285
  attr :ppid
121
286
  attr :uri
122
287
  attr :socket
123
-
124
288
  #
125
- # opts may contain the keys 'object', 'socket_creation_attempts',
126
- # 'pulse_rate', 'psname', 'dumped', or 'debug'
289
+ # sets up a child process serving any object as a DRb server running locally
290
+ # on unix domain sockets. the child process has a LifeLine established
291
+ # between it and the parent, making it impossible for the child to outlive
292
+ # the parent (become a zombie). the object to serve is specfied either
293
+ # directly using the 'object'/:object keyword
294
+ #
295
+ # Slave.new :object => MyServer.new
296
+ #
297
+ # or, preferably, using the block form
298
+ #
299
+ # Slave.new{ MyServer.new }
300
+ #
301
+ # when the block form is used the object is contructed in the child process
302
+ # itself. this is quite advantageous if the child object consumes resources
303
+ # or opens file handles (db connections, etc). by contructing the object in
304
+ # the child any resources are consumed from the child's address space and
305
+ # things like open file handles will not be carried into subsequent child
306
+ # processes (via standard unix fork semantics). in the event that a block
307
+ # is specified but the object cannot be constructed and, instead, throws and
308
+ # Exception, that exception will be propogated to the parent process.
309
+ #
310
+ # opts may contain the following keys, as either strings or symbols
311
+ #
312
+ # object : specify the slave object. otherwise block value is used.
313
+ # socket_creation_attempts : specify how many attempts to create a unix domain socket will be made
314
+ # debug : turn on some logging to STDERR
315
+ # psname : specify the name that will appear in 'top' ($0)
316
+ # at_exit : specify a lambda to be called in the *parent* when the child dies
317
+ # dumped : specify that the slave object should *not* be DRbUndumped (default is DRbUndumped)
318
+ # threadsafe : wrap the slave object with ThreadSafe to implement gross thread safety
127
319
  #
128
320
  def initialize opts = {}, &block
129
321
  #--{{{
@@ -131,24 +323,22 @@ require 'fcntl'
131
323
 
132
324
  @obj = getopt['object']
133
325
  @socket_creation_attempts = getopt['socket_creation_attempts'] || default('socket_creation_attempts')
134
- @pulse_rate = getopt['pulse_rate'] || default('pulse_rate')
135
326
  @debug = getopt['debug'] || default('debug')
136
327
  @psname = getopt['psname']
137
328
  @at_exit = getopt['at_exit']
138
329
  @dumped = getopt['dumped']
330
+ @threadsafe = getopt['threadsafe'] || default('threadsafe')
139
331
 
140
- raise ArgumentError, 'no slave object!' if
332
+ raise ArgumentError, 'no slave object or slave object block provided!' if
141
333
  @obj.nil? and block.nil?
142
334
 
143
335
  @shutdown = false
144
336
  @waiter = @status = nil
145
-
146
- @heartbeat = Heartbeat::new @pulse_rate, @debug
147
- @r, @w = IO::pipe
148
- @r2, @w2 = IO::pipe
337
+ @lifeline = LifeLine.new
149
338
 
150
339
  # weird syntax because dot/rdoc chokes on this!?!?
151
340
  init_failure = lambda do |e|
341
+ trace{ %Q[#{ e.message } (#{ e.class })\n#{ e.backtrace.join "\n" }] }
152
342
  o = Object.new
153
343
  class << o
154
344
  attr_accessor '__slave_object_failure__'
@@ -164,6 +354,7 @@ require 'fcntl'
164
354
  e = nil
165
355
  begin
166
356
  Kernel.at_exit{ Kernel.exit! }
357
+ @lifeline.catch
167
358
 
168
359
  if @obj
169
360
  @object = @obj
@@ -184,14 +375,16 @@ require 'fcntl'
184
375
  end
185
376
 
186
377
  $0 = (@psname ||= gen_psname(@object))
378
+
187
379
  unless @dumped or @object.respond_to?('__slave_object_failure__')
188
380
  @object.extend DRbUndumped
189
381
  end
190
382
 
191
- @ppid, @pid = Process::ppid, Process::pid
383
+ if @threadsafe
384
+ @object = ThreadSafe.new @object
385
+ end
192
386
 
193
- @r.close
194
- @r2.close
387
+ @ppid, @pid = Process::ppid, Process::pid
195
388
  @socket = nil
196
389
  @uri = nil
197
390
 
@@ -200,7 +393,7 @@ require 'fcntl'
200
393
  @socket_creation_attempts.times do |attempt|
201
394
  se = nil
202
395
  begin
203
- s = File::join(tmpdir, "#{ basename }_#{ attempt }")
396
+ s = File::join(tmpdir, "#{ basename }_#{ attempt }_#{ rand }")
204
397
  u = "drbunix://#{ s }"
205
398
  DRb::start_service u, @object
206
399
  @socket = s
@@ -214,20 +407,18 @@ require 'fcntl'
214
407
  end
215
408
 
216
409
  if @socket and @uri
217
- @heartbeat.start
218
-
219
410
  trap('SIGUSR2') do
220
- # @heartbeat.stop rescue nil
221
411
  DBb::thread.kill rescue nil
222
412
  FileUtils::rm_f @socket rescue nil
223
413
  exit
224
414
  end
225
415
 
226
- @w.write @socket
227
- @w.close
228
- DRb::thread.join
416
+ @lifeline.puts @socket
417
+ @lifeline.cling
229
418
  else
230
- @w.close
419
+ @lifeline.release
420
+ warn "slave(#{ $$ }) could not create socket!"
421
+ exit
231
422
  end
232
423
  rescue Exception => e
233
424
  trace{ %Q[#{ e.message } (#{ e.class })\n#{ e.backtrace.join "\n" }] }
@@ -240,24 +431,16 @@ require 'fcntl'
240
431
  #
241
432
  else
242
433
  detach
243
- @w.close
244
- @w2.close
245
- @socket = @r.read
246
- @r.close
434
+ @lifeline.throw
247
435
 
436
+ buf = @lifeline.gets
437
+ raise "failed to find slave socket" if buf.nil? or buf.strip.empty?
438
+ @socket = buf.strip
248
439
  trace{ "parent - socket <#{ @socket }>" }
249
440
 
250
441
  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
442
+ @at_exit_thread = @lifeline.on_cut{
443
+ @at_exit.respond_to?('call') ? @at_exit.call(self) : send(@at_exit.to_s, self)
261
444
  }
262
445
  end
263
446
 
@@ -265,7 +448,6 @@ require 'fcntl'
265
448
  Kernel.at_exit{ FileUtils::rm_f @socket }
266
449
  @uri = "drbunix://#{ socket }"
267
450
  trace{ "parent - uri <#{ @uri }>" }
268
- @heartbeat.start
269
451
  #
270
452
  # starting drb on localhost avoids dns lookups!
271
453
  #
@@ -274,6 +456,7 @@ require 'fcntl'
274
456
  if @object.respond_to? '__slave_object_failure__'
275
457
  c, m, bt = Marshal.load @object.__slave_object_failure__
276
458
  (e = c.new(m)).set_backtrace bt
459
+ trace{ %Q[#{ e.message } (#{ e.class })\n#{ e.backtrace.join "\n" }] }
277
460
  raise e
278
461
  end
279
462
  @psname ||= gen_psname(@object)
@@ -329,16 +512,16 @@ require 'fcntl'
329
512
  end
330
513
  alias :wait2 :wait
331
514
  #
332
- # stops the heartbeat thread and kills the child process - give the key
333
- # 'quiet' to ignore errors shutting down, including having already shutdown
515
+ # cuts the lifeline and kills the child process - give the key 'quiet' to
516
+ # ignore errors shutting down, including having already shutdown
334
517
  #
335
518
  def shutdown opts = {}
336
519
  #--{{{
337
520
  quiet = getopts(opts)['quiet']
338
521
  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
522
+ begin; Process::kill 'SIGUSR2', @pid; rescue Exception => e; end
523
+ begin; @lifeline.cut; rescue Exception; end
524
+ raise e if e unless quiet
342
525
  @shutdown = true
343
526
  #--}}}
344
527
  end
@@ -355,7 +538,7 @@ require 'fcntl'
355
538
  #
356
539
  def gen_psname obj
357
540
  #--{{{
358
- "#{ obj.class }_#{ obj.object_id }_#{ Process::ppid }_#{ Process::pid }".downcase.gsub(%r/\s+/,'_')
541
+ "slave_#{ obj.class }_#{ obj.object_id }_#{ Process::ppid }_#{ Process::pid }".downcase.gsub(%r/\s+/,'_')
359
542
  #--}}}
360
543
  end
361
544
  #
@@ -379,155 +562,62 @@ require 'fcntl'
379
562
  #
380
563
  def trace
381
564
  #--{{{
382
- STDERR.puts(yield) if @debug and STDERR.tty?
565
+ if @debug
566
+ STDERR.puts yield
567
+ STDERR.flush
568
+ end
383
569
  #--}}}
384
570
  end
571
+
385
572
  #
386
- # the Heartbeat class is essentially wrapper over an IPC channel that sends
387
- # a ping on the channel indicating process health. if either end of the
388
- # channel is detached the ping will fail and an error will be raised. in
389
- # this way it is ensured that Slave objects cannot continue to live without
390
- # their parent being alive.
573
+ # a simple convenience method which returns an *object* from another
574
+ # process. the object returned is the result of the supplied block. eg
391
575
  #
392
- class Heartbeat
393
- #--{{{
394
- def initialize pulse_rate = 4.2, debug = false
395
- #--{{{
396
- @pulse_rate = Float pulse_rate
397
- @debug = debug
398
- @r, @w = IO::pipe
399
- @pid = Process::pid
400
- @ppid = Process::ppid
401
- @cid = nil
402
- @thread = nil
403
- @ppid = nil
404
- @whoami = nil
405
- @beating = nil
406
- @pipe = nil
407
- #--}}}
408
- end
409
- def start
410
- #--{{{
411
- if Process::pid == @pid
412
- @w.close
413
- @pipe = @r
414
- @pipe.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
415
- parent_start
416
- else
417
- @r.close
418
- @pipe = @w
419
- child_start
420
- end
421
- @beating = true
422
- #--}}}
423
- end
424
- def parent_start
576
+ # object = Slave.object{ processor_intensive_object_built_in_child_process() }
577
+ #
578
+ # eg.
579
+ #
580
+ # the call can be made asynchronous via the 'async'/:async keyword
581
+ #
582
+ # thread = Slave.object(:async=>true){ long_processor_intensive_object_built_in_child_process() }
583
+ #
584
+ # # go on about your coding business then, later
585
+ #
586
+ # object = thread.value
587
+ #
588
+ def self.object opts = {}, &b
425
589
  #--{{{
426
- @whoami = 'parent'
427
- @thread =
428
- Thread::new(Thread::current) do |cur|
429
- begin
430
- loop do
431
- buf = @pipe.gets
432
- trace{ "<#{ @whoami }> <#{ @pid }> gets <#{ buf.inspect }>" }
433
- @cid = Integer buf.strip if @cid.nil? and buf =~ %r/^\s*\d+\s*$/
434
- end
435
- rescue => e
436
- cur.raise e
437
- ensure
438
- @pipe.close rescue nil
439
- end
440
- end
590
+ l = lambda{ begin; b.call; ensure; exit; end }
591
+
592
+ async = opts.delete('async') || opts.delete(:async)
593
+
594
+ opts['object'] = opts[:object] = l
595
+ opts['dumped'] = opts[:dumped] = true
596
+
597
+ slave = Slave.new opts
598
+
599
+ async ? Thread.new{ slave.object.call } : slave.object.call
441
600
  #--}}}
442
- end
443
- def child_start
601
+ end
602
+ def self.object opts = {}, &b
444
603
  #--{{{
445
- @whoami = 'child'
446
- @pid = Process::pid
447
- @ppid = Process::ppid
448
- @thread =
449
- Thread::new(Thread::current) do |cur|
450
- begin
451
- loop do
452
- trace{ "<#{ @whoami }> <#{ @pid }> puts <#{ @pid }>" }
453
- @pipe.puts @pid
454
- Process::kill 0, @ppid
455
- sleep @pulse_rate
456
- end
457
- rescue => e
458
- cur.raise e
459
- ensure
460
- @pipe.close rescue nil
461
- end
462
- end
463
- #--}}}
464
- end
604
+ async = opts.delete('async') || opts.delete(:async)
465
605
 
466
- def start
467
- #--{{{
468
- if Process::pid == @pid
469
- @r.close
470
- @pipe = @w
471
- @pipe.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
472
- parent_start
473
- else
474
- @w.close
475
- @pipe = @r
476
- child_start
606
+ opts['object'] = opts[:object] = lambda(&b)
607
+ opts['dumped'] = opts[:dumped] = true
608
+
609
+ slave = Slave.new opts
610
+
611
+ value = lambda do |slave|
612
+ begin
613
+ slave.object.call
614
+ ensure
615
+ slave.shutdown
477
616
  end
478
- @beating = true
479
- #--}}}
480
- end
481
- def parent_start
482
- #--{{{
483
- @whoami = 'parent'
484
- @thread =
485
- Thread::new(Thread::current) do |cur|
486
- begin
487
- sleep
488
- rescue => e
489
- cur.raise e
490
- ensure
491
- @pipe.close rescue nil
492
- end
493
- end
494
- #--}}}
495
- end
496
- def child_start
497
- #--{{{
498
- @whoami = 'child'
499
- @pid = Process::pid
500
- @ppid = Process::ppid
501
- @thread =
502
- Thread::new(Thread::current) do |cur|
503
- begin
504
- trace{ "child reading..." }
505
- @pipe.read
506
- trace{ "child read." }
507
- trace{ "child exiting." }
508
- exit
509
- rescue => e
510
- cur.raise e
511
- ensure
512
- @pipe.close rescue nil
513
- end
514
- end
515
- #--}}}
516
- end
517
- def stop
518
- #--{{{
519
- raise "not beating" unless @beating
520
- @thread.kill
521
- @pipe.close rescue nil
522
- @beating = false
523
- #--}}}
524
- end
525
- def trace
526
- #--{{{
527
- STDERR.puts(yield) if @debug and STDERR.tty?
528
- #--}}}
529
617
  end
618
+
619
+ async ? Thread.new{ value[slave] } : value[slave]
530
620
  #--}}}
531
- end # class Heartbeat
621
+ end
532
622
  #--}}}
533
623
  end # class Slave