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 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