Class: Fork

Inherits:
Object
  • Object
show all
Defined in:
lib/fork.rb,
lib/fork/version.rb

Overview

Note:

You should only interact between parent and fork by the means provided by the Fork class.

An object representing a fork, containing data about it like pid, exit_status, exception etc.

It also provides facilities for parent and child process to communicate with each other.

Examples:

Usage

def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
fork = Fork.new :return do
  fib(35)
end
fork.execute
puts "Forked child process with pid #{fork.pid} is currently #{fork.alive? ? 'alive' : 'dead'}"
puts fork.return_value # this blocks, until the fork finished, and returns the last value

The same, but a bit simpler

def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
fork = Fork.execute :return do
  fib(35)
end
puts fork.return_value # this blocks, until the fork finished, and returns the last value

And the simplest version, if all you care about is the return value

def fib(n) n < 2 ? n : fib(n-1)+fib(n-2); end # <-- bad implementation of fibonacci
future = Fork.future do
  fib(35)
end
puts future.call # this blocks, until the fork finished, and returns the last value

Defined Under Namespace

Classes: FlagNotSpecified, NotRunning, UndumpableException

Constant Summary collapse

IgnoreExceptions =

Exceptions that have to be ignored in the child’s handling of exceptions

[::SystemExit]
DefaultFlags =

The default flags Fork#initialize uses

Hash.new { |_hash, key| raise ArgumentError, "Unknown flag #{key}" }.merge({
  :exceptions   => false,
  :death_notice => false,
  :return       => false,
  :to_fork      => false,
  :from_fork    => false,
  :ctrl         => false,
})
Version =

The currently required version of the Fork gem

Gem::Version.new("1.0.1")

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*flags, &block) ⇒ Fork

Create a new Fork instance. The subprocess is not immediatly executed, you must invoke #execute on the Fork instance in order to get it executed. Only then #pid, #in, #out and #ctrl will be available. Also all IO related methods won’t work before that.

Parameters:

  • flags (Symbol, Hash)

    Tells the fork what facilities to provide. You can pass the flags either as a list of symbols, or as a Hash, or even mixed (the hash must be the last argument then).

    Valid flags are:

    • :return Make the value of the last expression (return value) available to

      the parent process
      
    • :exceptions Pass exceptions of the fork to the parent, making it available via

      Fork#exception
      
    • :death_notice Send the parent process an information when done processing

    • :to_fork You can write to the Fork from the parent process, and read in the

      child process
      
    • :from_fork You can read from the Fork in the parent process and write in the

      child process
      
    • :ctrl Provides an additional IO for control mechanisms

    Some flags implicitly set other flags. For example, :return will set :exceptions and :ctrl, :exceptions will set :ctrl and :death_notice will also set :ctrl.

Raises:

  • (ArgumentError)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/fork.rb', line 201

def initialize(*flags, &block)
  raise ArgumentError, "No block given" unless block
  if flags.last.is_a?(Hash) then
    @flags = DefaultFlags.merge(flags.pop)
  else
    @flags = DefaultFlags.dup
  end
  flags.each do |flag|
    raise ArgumentError, "Unknown flag #{flag.inspect}" unless @flags.has_key?(flag)
    @flags[flag] = true
  end
  @flags[:ctrl]       = true if @flags.values_at(:exceptions, :death_notice, :return).any?
  @flags[:exceptions] = true if @flags[:return]

  @parent         = true
  @alive          = nil
  @pid            = nil
  @process_status = nil
  @readable_io    = nil
  @writable_io    = nil
  @ctrl           = nil
  @block          = block
end

Instance Attribute Details

#ctrlObject (readonly)

Control IO (reserved for exception and death-notice passing)



176
177
178
# File 'lib/fork.rb', line 176

def ctrl
  @ctrl
end

#pidObject (readonly)

Note:

You *must not* directly interact with the forked process using the pid. This may lead to unexpected conflicts with Fork’s internal mechanisms.

The process id of the fork



162
163
164
# File 'lib/fork.rb', line 162

def pid
  @pid
end

#readable_ioObject (readonly)

Readable IO Allows the parent to read data from the fork, and the fork to read data from the parent. Requires the :to_fork and/or :from_fork flag to be set.



168
169
170
# File 'lib/fork.rb', line 168

def readable_io
  @readable_io
end

#writable_ioObject (readonly)

Writable IO Allows the parent to write data to the fork, and the fork to write data to the parent. Requires the :to_fork and/or :from_fork flag to be set.



173
174
175
# File 'lib/fork.rb', line 173

def writable_io
  @writable_io
end

Class Method Details

.execute(*args, &block) ⇒ Object

Create a Fork instance and immediatly start executing it. Equivalent to just call Fork.new(*args) { … }.execute

Returns the Fork instance. See Fork#initialize



153
154
155
# File 'lib/fork.rb', line 153

def self.execute(*args, &block)
  new(*args, &block).execute
end

.future(*args) ⇒ Proc

A simple forked-future implementation. Will process the block in a fork, blocks upon request of the result until the result is present. If the forked code raises an exception, invoking call on the proc will raise that exception in the parent process.

Examples:

Usage

# A
Fork.future { 1 }.call # => 1

# B
result = Fork.future { sleep 2; 1 } # assume a complex computation instead of sleep(2)
sleep 2                             # assume another complex computation
start  = Time.now
result.call                         # => 1
elapsed_time = Time.now-start       # => <1s as the work was done parallely

Parameters:

  • args

    All parameters passed to Fork.future are passed on to the block.

Returns:

  • (Proc)

    A lambda which upon invoking #call will block until the result of the block is calculated.



118
119
120
121
122
123
124
# File 'lib/fork.rb', line 118

def self.future(*args)
  obj = execute :return => true do |parent|
    yield(*args)
  end

  lambda { obj.return_value }
end

.read_marshalled(io) ⇒ Object

Reads an object sent via Fork.read_marshalled from the passed io. Raises EOFError if the io was closed on the remote end.

Returns:

  • (Object)

    The deserialized object which was sent through the IO

Raises:

  • (EOFError)

See Also:



75
76
77
78
79
80
81
# File 'lib/fork.rb', line 75

def self.read_marshalled(io)
  size       = io.read(4)
  raise EOFError unless size
  size       = size.unpack("I").first
  marshalled = io.read(size)
  Marshal.load(marshalled)
end

.return(*args) ⇒ Object

A simple forked-callback implementation. Will process the block in a fork, block until it has finished processing and returns the return value of the block. This can be useful if you want to process something that will (or might) irreversibly corrupt the environment. Doing that in a subprocess will leave the parent untouched.

Examples:

Usage

Fork.return { 1 } # => 1

Parameters:

  • args

    All parameters passed to Fork.return are passed on to the block.

Returns:

  • Returns the result of the block.



141
142
143
144
145
146
# File 'lib/fork.rb', line 141

def self.return(*args)
  obj = execute :return => true do |parent|
    yield(*args)
  end
  obj.return_value
end

.write_marshalled(io, obj) ⇒ Integer

Writes an object in serialized form to the passed IO. Important: certain objects are not marshallable, e.g. IOs, Procs and anonymous modules and classes.

Returns:

  • (Integer)

    The number of bytes written to the IO (see IO#write)

See Also:



90
91
92
93
94
# File 'lib/fork.rb', line 90

def self.write_marshalled(io, obj)
  marshalled = Marshal.dump(obj)
  io.write([marshalled.size].pack("I"))
  io.write(marshalled)
end

Instance Method Details

#alive?Boolean

Whether this fork is still running (= is alive) or already exited.

Returns:

  • (Boolean)


362
363
364
# File 'lib/fork.rb', line 362

def alive?
  @pid && !exit_status(false)
end

#cloneObject

Cloning a fork instance is prohibited. See Object#clone.

Raises:

  • (TypeError)


557
558
559
# File 'lib/fork.rb', line 557

def clone # :nodoc:
  raise TypeError, "can't clone #{self.class}"
end

#closeObject

Close all IOs

Raises:



542
543
544
545
546
547
# File 'lib/fork.rb', line 542

def close # :nodoc:
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  @readable_io.close if @readable_io
  @writable_io.close if @writable_io
  @ctrl.close if @ctrl
end

#complete!(pid, readable_io, writable_io, ctrl_io) ⇒ Object

Sets the io to communicate with the parent/child



298
299
300
301
302
303
304
# File 'lib/fork.rb', line 298

def complete!(pid, readable_io, writable_io, ctrl_io) # :nodoc:
  raise "Can't call complete! more than once" if @pid
  @pid          = pid
  @readable_io  = readable_io
  @writable_io  = writable_io
  @ctrl         = ctrl_io
end

#dead?Boolean

Whether this fork is still running or already exited (= is dead).

Returns:

  • (Boolean)


367
368
369
# File 'lib/fork.rb', line 367

def dead?
  !alive?
end

#death_notice?Boolean

Returns Whether this fork sends a death notice to the parent.

Returns:

  • (Boolean)

    Whether this fork sends a death notice to the parent



261
262
263
# File 'lib/fork.rb', line 261

def death_notice?
  @flags[:death_notice]
end

#dupObject

Duping a fork instance is prohibited. See Object#dup.

Raises:

  • (TypeError)


551
552
553
# File 'lib/fork.rb', line 551

def dup # :nodoc:
  raise TypeError, "can't dup #{self.class}"
end

#exception(blocking = true) ⇒ Object

The exception that terminated the fork Requires the :exceptions flag to be set when creating the fork.



343
344
345
346
347
348
349
# File 'lib/fork.rb', line 343

def exception(blocking=true)
  @exception || begin
    raise FlagNotSpecified, "You must set the :exceptions flag when forking in order to use this" unless handle_exceptions?
    _wait(blocking)
    @exception
  end
end

#executeself

Creates the fork (subprocess) and starts executing it.

Returns:

  • (self)


228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/fork.rb', line 228

def execute
  ctrl_read, ctrl_write, fork_read, parent_write, parent_read, fork_write = nil

  fork_read, parent_write = binary_pipe if @flags[:to_fork]
  parent_read, fork_write = binary_pipe if @flags[:from_fork]
  ctrl_read, ctrl_write   = binary_pipe if @flags[:ctrl]

  @alive = true

  pid = Process.fork do
    @parent = false
    parent_write.close if parent_write
    parent_read.close  if parent_read
    ctrl_read.close    if ctrl_read
    complete!(Process.pid, fork_read, fork_write, ctrl_write)

    child_process
  end

  fork_write.close if fork_write
  fork_read.close  if fork_read
  ctrl_write.close if ctrl_write
  complete!(pid, parent_read, parent_write, ctrl_read)

  self
end

#exit_status(blocking = true) ⇒ Object

The exit status of this fork. See Process::Status#exitstatus



316
317
318
319
320
321
322
323
# File 'lib/fork.rb', line 316

def exit_status(blocking=true)
  @exit_status || begin
    _wait(blocking)
    @exit_status
  rescue NotRunning
    raise if blocking # calling exit status on a not-yet started fork is an exception, nil otherwise
  end
end

#failure?Boolean

Blocks until the fork has exited.

Returns:

  • (Boolean)

    Whether the fork exited with an unsuccessful exit status (status code != 0).



337
338
339
# File 'lib/fork.rb', line 337

def failure?
  !success?
end

#fork?Boolean

Returns Whether the current code is executed in the fork, as opposed to the parent.

Returns:

  • (Boolean)

    Whether the current code is executed in the fork, as opposed to the parent.



293
294
295
# File 'lib/fork.rb', line 293

def fork?
  !@parent
end

#gets(*args) ⇒ String?

In the parent process: read data from the fork. In the forked process: read data from the parent. Works just like IO#gets.

Returns:

  • (String, nil)

    The data that the forked/parent process has written.



376
377
378
379
380
381
382
# File 'lib/fork.rb', line 376

def gets(*args)
  @readable_io.gets(*args)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
  raise
end

#handle_exceptions?Boolean

Returns Whether this fork sends the final exception to the parent.

Returns:

  • (Boolean)

    Whether this fork sends the final exception to the parent



256
257
258
# File 'lib/fork.rb', line 256

def handle_exceptions?
  @flags[:exceptions]
end

#has_ctrl?Boolean

Returns Whether parent and fork use a control-io.

Returns:

  • (Boolean)

    Whether parent and fork use a control-io.



281
282
283
# File 'lib/fork.rb', line 281

def has_ctrl?
  @flags[:ctrl]
end

#has_in?Boolean

Returns Whether the other process can write to this process.

Returns:

  • (Boolean)

    Whether the other process can write to this process.



271
272
273
# File 'lib/fork.rb', line 271

def has_in?
  @flags[parent? ? :from_fork : :to_fork]
end

#has_out?Boolean

Returns Whether this process can write to the other process.

Returns:

  • (Boolean)

    Whether this process can write to the other process.



276
277
278
# File 'lib/fork.rb', line 276

def has_out?
  @flags[parent? ? :to_fork : :from_fork]
end

#inspectObject

See Object#inspect



563
564
565
# File 'lib/fork.rb', line 563

def inspect # :nodoc:
  sprintf "#<%p pid=%p alive=%p>", self.class, @pid, @alive
end

#killObject

Sends the (SIG)HUP signal to this fork. This is “gently asking the process to terminate”. This gives the process a chance to perform some cleanup. See Fork#kill!, Fork#signal, Process.kill

Raises:



520
521
522
523
# File 'lib/fork.rb', line 520

def kill
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  Process.kill("HUP", @pid)
end

#kill!Object

Sends the (SIG)KILL signal to this fork. The process will be immediatly terminated and will not have a chance to do any cleanup. See Fork#kill, Fork#signal, Process.kill

Raises:



529
530
531
532
# File 'lib/fork.rb', line 529

def kill!
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  Process.kill("KILL", @pid)
end

#parent?Boolean

Returns Whether the current code is executed in the parent of the fork.

Returns:

  • (Boolean)

    Whether the current code is executed in the parent of the fork.



287
288
289
# File 'lib/fork.rb', line 287

def parent?
  @parent
end

#process_status(blocking = true) ⇒ Object

Process::Status for dead forks, nil for live forks



307
308
309
310
311
312
# File 'lib/fork.rb', line 307

def process_status(blocking=true)
  @process_status || begin
    _wait(blocking)
    @process_status
  end
end

#puts(*args) ⇒ nil

In the parent process: Write to the fork. In the forked process: Write to the parent. Works just like IO#puts

Returns:

  • (nil)


429
430
431
432
433
434
435
# File 'lib/fork.rb', line 429

def puts(*args)
  @writable_io.puts(*args)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
  raise
end

#read(*args) ⇒ String?

In the parent process: read data from the fork. In the forked process: read data from the parent. Works just like IO#read.

Returns:

  • (String, nil)

    The data that the forked/parent process has written.



389
390
391
392
393
394
395
# File 'lib/fork.rb', line 389

def read(*args)
  @readable_io.read(*args)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
  raise
end

#read_nonblock(*args) ⇒ String?

In the parent process: read data from the fork. In the forked process: read data from the parent. Works just like IO#read_nonblock.

Returns:

  • (String, nil)

    The data that the forked/parent process has written.



402
403
404
405
406
407
408
# File 'lib/fork.rb', line 402

def read_nonblock(*args)
  @readable_io.read_nonblock(*args)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :to_fork flag when forking in order to use this" unless @readable_io
  raise
end

#read_remaining_ctrl(_wait_upon_eof = true) ⇒ self

Read a single instruction sent via @ctrl, used by :exception, :death_notice and :return_value

Returns:

  • (self)


454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/fork.rb', line 454

def read_remaining_ctrl(_wait_upon_eof=true) # :nodoc:
  loop do # EOFError will terminate this loop
    instruction, data = *Fork.read_marshalled(@ctrl)
    case instruction
      when :exception
        @exception = data
      when :death_notice
        _wait if _wait_upon_eof
        _wait_upon_eof = false
      when :return_value
        @return_value = data
      else
        raise "Unknown control instruction #{instruction} in fork #{fork}"
    end
  end

  self
rescue EOFError # closed
  _wait(false) if _wait_upon_eof # update

  self
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :ctrl flag when forking in order to use this" unless @ctrl
  raise
end

#receive_objectObject

In the parent process: read on object sent by the fork. In the forked process: read on object sent by the parent.

Returns:

  • (Object)

    The object that the forked/parent process has sent.

See Also:



416
417
418
419
420
421
422
# File 'lib/fork.rb', line 416

def receive_object
  Fork.read_marshalled(@readable_io)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @readable_io
  raise
end

#return_value(blocking = true) ⇒ Object

Blocks until the fork returns



352
353
354
355
356
357
358
359
# File 'lib/fork.rb', line 352

def return_value(blocking=true)
  @return_value || begin
    raise FlagNotSpecified, "You must set the :return flag when forking in order to use this" unless returns?
    _wait(blocking)
    raise @exception if @exception
    @return_value
  end
end

#returns?Boolean

Returns Whether this forks terminal value is returned to the parent.

Returns:

  • (Boolean)

    Whether this forks terminal value is returned to the parent



266
267
268
# File 'lib/fork.rb', line 266

def returns?
  @flags[:return]
end

#send_object(obj) ⇒ Object

Sends an object to the parent process. The parent process can read it using Fork#receive_object.

Examples:

Usage

Demo = Struct.new(:a, :b, :c)
fork = Fork.new :from_fork do |parent|
  parent.send_object({:a => 'little', :nested => ['hash']})
  parent.send_object(Demo.new(1, :two, "three"))
end
p :received => fork.receive_object # -> {:received=>{:a=>"little", :nested=>["hash"]}}
p :received => fork.receive_object # -> {:received=>#<struct Demo a=1, b=:two, c="three">}

See Also:



494
495
496
497
498
499
500
501
# File 'lib/fork.rb', line 494

def send_object(obj)
  Fork.write_marshalled(@writable_io, obj)
  self
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
  raise
end

#signal(sig) ⇒ Object

Sends the given signal to this fork See Fork#kill, Fork#kill!, Process.kill

Raises:



536
537
538
539
# File 'lib/fork.rb', line 536

def signal(sig)
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  Process.kill(sig, @pid)
end

#success?Boolean

Blocks until the fork has exited.

Returns:

  • (Boolean)

    Whether the fork exited with a successful exit status (status code 0).



329
330
331
# File 'lib/fork.rb', line 329

def success?
  exit_status.zero?
end

#waitObject

Wait for this fork to terminate. Returns self

Examples:

Usage

start = Time.now
fork = Fork.new do sleep 20 end
fork.wait
(Time.now-start).floor # => 20


511
512
513
514
# File 'lib/fork.rb', line 511

def wait
  _wait unless @process_status
  self
end

#write(*args) ⇒ Integer

In the parent process: Write to the fork. In the forked process: Write to the parent. Works just like IO#write

Returns:

  • (Integer)

    The number of bytes written



442
443
444
445
446
447
448
# File 'lib/fork.rb', line 442

def write(*args)
  @writable_io.write(*args)
rescue NoMethodError
  raise NotRunning, "Fork is not running yet, you must invoke #execute first." unless @pid
  raise FlagNotSpecified, "You must set the :from_fork flag when forking in order to use this" unless @writable_io
  raise
end