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
The command line of the subprocess
@api private
@return [String] what is remaining of the initial dump that should be
passed to the slave
@api private
@return [IO] the IO used to transmit initial information to the slave
The slave's name
It is an arbitrary object useful for reporting/tracking
@return [Object]
@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
The currently known program ID
@api private
@return [String] the result data as received
@api private
@return [IO] the result I/O
Environment that should be set in the subprocess
Options that should be passed to Kernel.spawn
@return [Process::Status] the exit status of the last run. Is nil
while the process is running
@return [Array<String>] a list of commands that this slave requests
Public Class Methods
@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
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
@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
Whether the slave has already ran, and is finished
# File lib/autorespawn/slave.rb, line 199 def finished? pid && status end
# File lib/autorespawn/slave.rb, line 69 def inspect "#<Autorespawn::Slave #{object_id.to_s(16)} #{cmdline.join(" ")}>" end
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 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
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
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
# File lib/autorespawn/slave.rb, line 189 def needed_auto @needed = nil end
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
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
@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 on the program ID
(see Autorespawn::ProgramID#register_files)
# File lib/autorespawn/slave.rb, line 83 def register_files(files) program_id.register_files(files) end
Whether the slave is running
# File lib/autorespawn/slave.rb, line 194 def running? pid && !status end
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
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
# File lib/autorespawn/slave.rb, line 78 def to_s; inspect end
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