Class: Autorespawn::Slave

Inherits:
Object
  • Object
show all
Defined in:
lib/autorespawn/slave.rb

Overview

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

Direct Known Subclasses

Self

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*cmdline, name: nil, seed: ProgramID.new, env: Hash.new, **spawn_options) ⇒ Slave

Returns a new instance of Slave.

Parameters:

  • name (Object) (defaults to: nil)

    an arbitrary object that can be used for reporting / tracking reasons

  • seed (ProgramID) (defaults to: ProgramID.new)

    a seed object with some relevant files already registered, to avoid respawning the slave unnecessarily.



55
56
57
58
59
60
61
62
63
64
65
66
67
# 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

Instance Attribute Details

#cmdlineObject (readonly)

The command line of the subprocess



17
18
19
# File 'lib/autorespawn/slave.rb', line 17

def cmdline
  @cmdline
end

#initial_dumpString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns what is remaining of the initial dump that should be passed to the slave.

Returns:

  • (String)

    what is remaining of the initial dump that should be passed to the slave



49
50
51
# File 'lib/autorespawn/slave.rb', line 49

def initial_dump
  @initial_dump
end

#initial_wIO (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the IO used to transmit initial information to the slave.

Returns:

  • (IO)

    the IO used to transmit initial information to the slave



44
45
46
# File 'lib/autorespawn/slave.rb', line 44

def initial_w
  @initial_w
end

#nameObject (readonly)

The slave’s name

It is an arbitrary object useful for reporting/tracking

Returns:

  • (Object)


13
14
15
# File 'lib/autorespawn/slave.rb', line 13

def name
  @name
end

#pidnil, Integer (readonly)

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

Returns:

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



25
26
27
# File 'lib/autorespawn/slave.rb', line 25

def pid
  @pid
end

#program_idObject (readonly)

The currently known program ID



15
16
17
# File 'lib/autorespawn/slave.rb', line 15

def program_id
  @program_id
end

#result_bufferString (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the result data as received.

Returns:

  • (String)

    the result data as received



40
41
42
# File 'lib/autorespawn/slave.rb', line 40

def result_buffer
  @result_buffer
end

#result_rIO (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the result I/O.

Returns:

  • (IO)

    the result I/O



36
37
38
# File 'lib/autorespawn/slave.rb', line 36

def result_r
  @result_r
end

#spawn_envObject (readonly)

Environment that should be set in the subprocess



19
20
21
# File 'lib/autorespawn/slave.rb', line 19

def spawn_env
  @spawn_env
end

#spawn_optionsObject (readonly)

Options that should be passed to Kernel.spawn



21
22
23
# File 'lib/autorespawn/slave.rb', line 21

def spawn_options
  @spawn_options
end

#statusProcess::Status (readonly)

Returns the exit status of the last run. Is nil while the process is running.

Returns:

  • (Process::Status)

    the exit status of the last run. Is nil while the process is running



28
29
30
# File 'lib/autorespawn/slave.rb', line 28

def status
  @status
end

#subcommandsArray<String> (readonly)

Returns a list of commands that this slave requests.

Returns:

  • (Array<String>)

    a list of commands that this slave requests



31
32
33
# File 'lib/autorespawn/slave.rb', line 31

def subcommands
  @subcommands
end

Instance Method Details

#each_tracked_file(with_status: false, &block) ⇒ Object

Enumerate the files that are tracked for #needed?



74
75
76
# 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) ⇒ Array<Pathname>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Parameters:

  • the (Process::Status)

    exit status

Returns:

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



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# 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?Boolean

Whether the slave has already ran, and is finished

Returns:

  • (Boolean)


199
200
201
# File 'lib/autorespawn/slave.rb', line 199

def finished?
    pid && status
end

#inspectObject



69
70
71
# File 'lib/autorespawn/slave.rb', line 69

def inspect
    "#<Autorespawn::Slave #{object_id.to_s(16)} #{cmdline.join(" ")}>"
end

#joinObject

Wait for the slave to terminate and call #finished



216
217
218
219
# File 'lib/autorespawn/slave.rb', line 216

def join
    _, status = Process.waitpid2(pid)
    finished(status)
end

#kill(signal = 'TERM', join: true) ⇒ Object

Kill the slave

Parameters:

  • join (Boolean) (defaults to: true)

    whether the method should wait for the child to end

See Also:



208
209
210
211
212
213
# File 'lib/autorespawn/slave.rb', line 208

def kill(signal = 'TERM', join: true)
    Process.kill signal, pid
    if join
        self.join
    end
end

#needed!Object

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



177
178
179
# File 'lib/autorespawn/slave.rb', line 177

def needed!
    @needed = true
end

#needed?Boolean

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

Returns:

  • (Boolean)


162
163
164
165
166
167
168
# File 'lib/autorespawn/slave.rb', line 162

def needed?
    if running? then false
    elsif !@needed.nil?
        @needed
    else program_id.changed?
    end
end

#needed_autoObject



189
190
191
# File 'lib/autorespawn/slave.rb', line 189

def needed_auto
    @needed = nil
end

#not_needed!Object

Forces #needed? to return false

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



185
186
187
# File 'lib/autorespawn/slave.rb', line 185

def not_needed!
    @needed = false
end

#pollObject

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



140
141
142
143
144
145
# File 'lib/autorespawn/slave.rb', line 140

def poll
    return unless running?

    write_initial_dump
    read_queued_result
end

#read_queued_resultObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Queue any pending result data sent by the slave



266
267
268
269
270
271
# 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) ⇒ Object

Register files on the program ID

(see ProgramID#register_files)



83
84
85
# File 'lib/autorespawn/slave.rb', line 83

def register_files(files)
    program_id.register_files(files)
end

#running?Boolean

Whether the slave is running

Returns:

  • (Boolean)


194
195
196
# File 'lib/autorespawn/slave.rb', line 194

def running?
    pid && !status
end

#spawn(send_initial_dump: true) ⇒ Integer

Start the slave

Parameters:

  • send_initial_dump (Boolean) (defaults to: true)

    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

Returns:

  • (Integer)

    the slave’s PID



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# 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?Boolean

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

Returns:

  • (Boolean)


226
227
228
229
230
231
# 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_sObject



78
# File 'lib/autorespawn/slave.rb', line 78

def to_s; inspect end

#write_initial_dumpObject

Write as much of the initial dump to the slave

To avoid blocking in #spawn, the initial dump



150
151
152
153
154
155
156
157
158
# 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