class Autorespawn::Manager

Manager of a bunch of autorespawn slaves

Attributes

active_slaves[R]

@return [Hash<Slave>] list of active slaves

name[R]

@return [Object] an object that is used to identify the manager itself

parallel_level[RW]

@return [Integer] the number of processes allowed to work in parallel

queued_slaves[R]

@return [Array<Slave>] list of slaves explicitely queued with {#queue}

seed[R]

@return [ProgramID] a seed object that is passed to new slaves to

represent the currently known state of file, to avoid unnecessary
respawning

@see add_seed_file

self_slave[R]

@return [Self] an object that has the same API than [Slave] to

represent the manager's process itself. It is always included in
{#workers} and {#active_slaves}
tracked_files[R]

@return [Hash<Pathname,TrackedFile>] the whole set of files that are

tracked by this manager's slaves
workers[R]

@return [Array<Slave>] declared worker processes, as a hash from

the PID to a Slave object

Public Class Methods

new(name: nil, parallel_level: 1) click to toggle source

@!endgroup

# File lib/autorespawn/manager.rb, line 75
def initialize(name: nil, parallel_level: 1)
    @parallel_level = parallel_level
    @workers   = Array.new
    @name = name
    @seed = ProgramID.for_self
    @tracked_files = Hash.new

    @self_slave = Self.new(name: name)
    @workers << self_slave
    @queued_slaves = Array.new
    @active_slaves = Hash[self_slave.pid => self_slave]
end

Public Instance Methods

active?(slave) click to toggle source

Tests whether this slave is currently active on self

# File lib/autorespawn/manager.rb, line 118
def active?(slave)
    active_slaves[slave.pid] == slave
end
add_slave(*cmdline, name: nil, **spawn_options) click to toggle source

Spawns a worker, i.e. a program that will perform the intended work and report the program state

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

reporting / tracking
# File lib/autorespawn/manager.rb, line 127
def add_slave(*cmdline, name: nil, **spawn_options)
    slave = Slave.new(*cmdline, name: name, seed: seed, **spawn_options)
    slave.needed!
    register_slave(slave)
    slave
end
clear() click to toggle source

Kill and remove all workers from this manager

@see kill

# File lib/autorespawn/manager.rb, line 210
def clear
    kill
    workers.dup.each do |w|
        if w != self_slave
            remove_slave(w)
        end
    end
end
collect_finished_slaves() click to toggle source

@api private

Collect information about the finished slaves

@return [Array<Slave>] the slaves that finished

# File lib/autorespawn/manager.rb, line 164
def collect_finished_slaves
    finished_slaves = Array.new
    while finished_child = Process.waitpid2(-1, Process::WNOHANG)
        finished_slaves << process_finished_slave(*finished_child)
    end
    finished_slaves
rescue Errno::ECHILD
    finished_slaves
end
has_active_slaves?() click to toggle source

Tests whether this manager has some slaves that are active

# File lib/autorespawn/manager.rb, line 113
def has_active_slaves?
    active_slaves.size != 1
end
has_slaves?() click to toggle source

Check whether this manager has slaves

# File lib/autorespawn/manager.rb, line 97
def has_slaves?
    # There's always a worker for self
    workers.size != 1
end
include?(slave) click to toggle source

Tests whether this slave is registered as a worker on self

# File lib/autorespawn/manager.rb, line 108
def include?(slave)
    workers.include?(slave)
end
kill() click to toggle source

Kill all active slaves

@see clear

# File lib/autorespawn/manager.rb, line 198
def kill
    active_slaves.each_value { |s| s.kill(join: false) }
    while has_active_slaves?
        finished_child = Process.waitpid2(-1)
        process_finished_slave(*finished_child)
    end
rescue Errno::ECHILD
end
on_slave_new(&block) click to toggle source

Register a callback for when a new slave is added by {#add_slave}

@param [#call] block the callback @yieldparam [Slave] the new slave

# File lib/autorespawn/manager.rb, line 38
def on_slave_new(&block)
    __on_slave_new(&block)
    workers.each do |w|
        block.call(w)
    end
end
on_slave_start(&block) click to toggle source

Register a callback that should be called when a new slave has been spawned by {#poll}

@param [#call] block the callback @yieldparam [Slave] the newly started slave

# File lib/autorespawn/manager.rb, line 51
def on_slave_start(&block)
    __on_slave_start(&block)
    active_slaves.each_value do |w|
        block.call(w)
    end
end
poll(autospawn: true, update_files: true) click to toggle source

Wait for children to terminate and spawns them when needed

# File lib/autorespawn/manager.rb, line 245
def poll(autospawn: true, update_files: true)
    finished_slaves = collect_finished_slaves
    new_slaves = Array.new

    trigger_slaves_as_necessary
    active_slaves.each_value(&:poll)

    while active_slaves.size < parallel_level + 1
        if slave = queued_slaves.find { |s| !s.running? }
            queued_slaves.delete(slave)
        elsif autospawn
            needed_slaves, _remaining = workers.partition { |s| !s.running? && s.needed? }
            failed, normal = needed_slaves.partition { |s| s.finished? && !s.success? }
            slave = failed.first || normal.first
        end

        if slave
            slave.spawn(send_initial_dump: false)
            # We manually track the slave's needed flag, just forcefully
            # set it to false at that point
            slave.not_needed!
            run_hook :__on_slave_start, slave
            new_slaves << slave
            active_slaves[slave.pid] = slave
        else
            break
        end
    end
    return new_slaves, finished_slaves
end
process_finished_slave(pid, status) click to toggle source
# File lib/autorespawn/manager.rb, line 174
def process_finished_slave(pid, status)
    return if !(slave = active_slaves.delete(pid))

    if slave.finished(status).empty?
        # Do not register the slave if it is already marked as needed?
        slave.each_tracked_file(with_status: true) do |path, mtime, size|
            tracker = (tracked_files[path] ||= TrackedFile.new(path, mtime: mtime, size: size))
            tracker.slaves << slave
        end
        slave.not_needed!
    end

    slave.subcommands.each do |name, cmdline, spawn_options|
        add_slave(*cmdline, name: name, **spawn_options)
    end
    seed.merge!(slave.program_id)

    run_hook :on_slave_finished, slave
    slave
end
queue(slave) click to toggle source

Queue a slave for execution

# File lib/autorespawn/manager.rb, line 155
def queue(slave)
    queued_slaves << slave
end
register_seed_files(files, search_patch = seed.ruby_load_path, ignore_not_found: true) click to toggle source

Add files to {#seed}

(see ProgramID#register_files)

# File lib/autorespawn/manager.rb, line 91
def register_seed_files(files, search_patch = seed.ruby_load_path, ignore_not_found: true)
    files = seed.resolve_file_list(files, search_path, ignore_not_found: ignore_not_found)
    seed.register_files(files)
end
register_slave(slave) click to toggle source

@api private

Registers a slave

# File lib/autorespawn/manager.rb, line 148
def register_slave(slave)
    workers << slave
    run_hook :__on_slave_new, slave
    slave
end
remove_slave(slave) click to toggle source

Remove a worker from this manager

@raise [ArgumentError] if the slave is still running

# File lib/autorespawn/manager.rb, line 137
def remove_slave(slave)
    if active?(slave)
        raise ArgumentError, "#{slave} is still running"
    end
    workers.delete(slave)
    run_hook :on_slave_removed, slave
end
run() click to toggle source
# File lib/autorespawn/manager.rb, line 219
def run
    while true
        poll
        sleep 1
    end

rescue Interrupt
ensure
    active_slaves.values.each do |slave|
        slave.kill
    end
end
slave_count() click to toggle source

The number of slaves registered

# File lib/autorespawn/manager.rb, line 103
def slave_count
    workers.size - 1
end
trigger_slaves_as_necessary() click to toggle source
# File lib/autorespawn/manager.rb, line 232
def trigger_slaves_as_necessary
    tracked_files.delete_if do |path, tracker|
        tracker.slaves.delete_if(&:needed?)
        if tracker.slaves.empty?
            true
        elsif tracker.update
            tracker.slaves.each(&:needed!)
            true
        end
    end
end