class Autorespawn::Slave

Representation of an autorespawn-aware subprocess that is started by a {Manager}

Slaves have two roles: the one of discovery (what are the commands that need to be started) and the one of

Attributes

cmdline[R]

The command line of the subprocess

initial_dump[R]

@api private

@return [String] what is remaining of the initial dump that should be

passed to the slave
initial_w[R]

@api private

@return [IO] the IO used to transmit initial information to the slave

name[R]

The slave's name

It is an arbitrary object useful for reporting/tracking

@return [Object]

pid[R]

@return [nil,Integer] pid the PID of the current process or of the

last process if it is finished. It is non-nil only after {#spawn} has
been called
program_id[R]

The currently known program ID

result_buffer[R]

@api private

@return [String] the result data as received

result_r[R]

@api private

@return [IO] the result I/O

spawn_env[R]

Environment that should be set in the subprocess

spawn_options[R]

Options that should be passed to Kernel.spawn

status[R]

@return [Process::Status] the exit status of the last run. Is nil

while the process is running
subcommands[R]

@return [Array<String>] a list of commands that this slave requests

Public Class Methods

new(*cmdline, name: nil, seed: ProgramID.new, env: Hash.new, **spawn_options) click to toggle source

@param [Object] name an arbitrary object that can be used for

reporting / tracking reasons

@param [ProgramID] seed a seed object with some relevant files already

registered, to avoid respawning the slave unnecessarily.
# File lib/autorespawn/slave.rb, line 55
def initialize(*cmdline, name: nil, seed: ProgramID.new, env: Hash.new, **spawn_options)
    @name = name
    @program_id = seed.dup
    @cmdline    = cmdline
    @needed = true
    @spawn_env     = env
    @spawn_options = spawn_options
    @subcommands = Array.new
    @pid        = nil
    @status     = nil
    @result_r  = nil
    @result_buffer = nil
end

Public Instance Methods

each_tracked_file(with_status: false, &block) click to toggle source

Enumerate the files that are tracked for {#needed?}

# File lib/autorespawn/slave.rb, line 74
def each_tracked_file(with_status: false, &block)
    @program_id.each_tracked_file(with_status: with_status, &block)
end
finished(status) click to toggle source

@api private

Announce that the slave already finished, with the given exit status

@param [Process::Status] the exit status @return [Array<Pathname>] a set of files that either changed or got

added since the call to {#spawn}. If not empty, the slave calls
{#needed!} by itself to force a re-execution
# File lib/autorespawn/slave.rb, line 241
def finished(status)
    @status = status
    read_queued_result
    begin
        @subcommands, file_list = Marshal.load(result_buffer)
        @success = true
    rescue ArgumentError # "Marshal data too short"
        @subcommands = Array.new
        file_list = Array.new
        @success = false
    end
    @program_id = program_id.slice(file_list)
    modified = program_id.register_files(file_list)
    if !modified.empty?
        needed!
    end
    @success = @success && status.success?
    result_r.close
    initial_w.close
    modified
end
finished?() click to toggle source

Whether the slave has already ran, and is finished

# File lib/autorespawn/slave.rb, line 199
def finished?
    pid && status
end
inspect() click to toggle source
# File lib/autorespawn/slave.rb, line 69
def inspect
    "#<Autorespawn::Slave #{object_id.to_s(16)} #{cmdline.join(" ")}>"
end
join() click to toggle source

Wait for the slave to terminate and call {#finished}

# File lib/autorespawn/slave.rb, line 216
def join
    _, status = Process.waitpid2(pid)
    finished(status)
end
kill(signal = 'TERM', join: true) click to toggle source

Kill the slave

@param [Boolean] join whether the method should wait for the child to

end

@see join

# File lib/autorespawn/slave.rb, line 208
def kill(signal = 'TERM', join: true)
    Process.kill signal, pid
    if join
        self.join
    end
end
needed!() click to toggle source

Marks this slave for execution

Note that it will only be executed by the underlying {Manager} when (1) a slot is available and (2) it has stopped running

This is usually not called directly, but through {Manager#queue} which also ensures that the slave gets in front of the execution queue

# File lib/autorespawn/slave.rb, line 177
def needed!
    @needed = true
end
needed?() click to toggle source

Whether this slave would need to be spawned, either because it has never be, or because the program ID changed

# File lib/autorespawn/slave.rb, line 162
def needed?
    if running? then false
    elsif !@needed.nil?
        @needed
    else program_id.changed?
    end
end
needed_auto() click to toggle source
# File lib/autorespawn/slave.rb, line 189
def needed_auto
    @needed = nil
end
not_needed!() click to toggle source

Forces {#needed?} to return false

Call {#needed_auto} to revert back to determining if the slave is needed or not using the tracked files

# File lib/autorespawn/slave.rb, line 185
def not_needed!
    @needed = false
end
poll() click to toggle source

Must be called regularly to ensure a good communication with the slave

# File lib/autorespawn/slave.rb, line 140
def poll
    return unless running?

    write_initial_dump
    read_queued_result
end
read_queued_result() click to toggle source

@api private

Queue any pending result data sent by the slave

# File lib/autorespawn/slave.rb, line 266
def read_queued_result
    while true
        result_buffer << result_r.read_nonblock(1024)
    end
rescue IO::WaitReadable, EOFError
end
register_files(files) click to toggle source

Register files on the program ID

(see ProgramID#register_files)

# File lib/autorespawn/slave.rb, line 83
def register_files(files)
    program_id.register_files(files)
end
running?() click to toggle source

Whether the slave is running

# File lib/autorespawn/slave.rb, line 194
def running?
    pid && !status
end
spawn(send_initial_dump: true) click to toggle source

Start the slave

@param [Boolean] send_initial_dump Initial information is sent to the

slave through the {#initial_w} pipe. This transmission can be done
asynchronously by setting this flag to true. In this case, the
caller should make sure that {#write_initial_dump} is called after
{#spawn} until it returns true. Note that {Manager#poll} takes care
of this already

@return [Integer] the slave's PID

# File lib/autorespawn/slave.rb, line 97
def spawn(send_initial_dump: true)
    if running?
        raise AlreadyRunning, "cannot call #spawn on #{self}: already running"
    end

    initial_r, initial_w = IO.pipe
    result_r, result_w = IO.pipe
    env = self.spawn_env.merge(
        SLAVE_INITIAL_STATE_ENV => initial_r.fileno.to_s,
        SLAVE_RESULT_ENV        => result_w.fileno.to_s)

    program_id.refresh
    @needed = nil
    pid = Kernel.spawn(env, *cmdline, initial_r => initial_r, result_w => result_w, **spawn_options)
    initial_r.close
    result_w.close
    @initial_w = initial_w
    @initial_dump = Marshal.dump([name, program_id])
    initial_w.write([initial_dump.size].pack("L<"))
    if send_initial_dump
        while !write_initial_dump
            select([], [initial_w])
        end
    end

    @pid = pid
    @status = nil
    @result_buffer = ''
    @result_r = result_r
    pid

rescue Exception
    if pid
        Process.kill 'TERM', pid
    end
    initial_w.close if initial_w && !initial_w.closed?
    initial_r.close if initial_r && !initial_r.closed?
    result_w.close if result_w && !result_w.closed?
    result_r.close if result_r && !result_r.closed?
    raise
end
success?() click to toggle source

Whether the slave behaved properly

This does not indicate whether the slave's intended work has been done, only that it produced the data expected by Autorespawn. To check the child's success w.r.t. its execution, check {#status}

# File lib/autorespawn/slave.rb, line 226
def success?
    if !status
        raise NotFinished, "called {#success?} on a #{pid ? 'running' : 'non-started'} child"
    end
    @success
end
to_s() click to toggle source
# File lib/autorespawn/slave.rb, line 78
def to_s; inspect end
write_initial_dump() click to toggle source

Write as much of the initial dump to the slave

To avoid blocking in {#spawn}, the initial dump

# File lib/autorespawn/slave.rb, line 150
def write_initial_dump
    return if initial_dump.empty?

    written_bytes = initial_w.write_nonblock(initial_dump)
    @initial_dump = @initial_dump[written_bytes, initial_dump.size - written_bytes]
    initial_dump.empty?
rescue IO::WaitWritable
    true
end