Class: EventedBluepill::Process

Inherits:
Object
  • Object
show all
Defined in:
lib/evented_bluepill/process.rb

Constant Summary collapse

CONFIGURABLE_ATTRIBUTES =
[
  :start_command,
  :stop_command,
  :restart_command,

  :stdout,
  :stderr,
  :stdin,

  :daemonize,
  :pid_file,
  :working_dir,
  :environment,

  :start_grace_time,
  :stop_grace_time,
  :restart_grace_time,

  :uid,
  :gid,

  :monitor_children,
  :child_process_factory
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(process_name, checks, options = {}) ⇒ Process

Returns a new instance of Process.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/evented_bluepill/process.rb', line 116

def initialize(process_name, checks, options = {})
  @name = process_name
  @watches = []
  @triggers = []
  @children_timer = []
  @statistics = ProcessStatistics.new
  @actual_pid = options[:actual_pid]
  self.logger = options[:logger]

  checks.each do |name, opts|
    if EventedBluepill::Trigger[name]
      self.add_trigger(name, opts)
    else
      self.add_watch(name, opts)
    end
  end

  # These defaults are overriden below if it's configured to be something else.
  @monitor_children =  false
  @start_grace_time = @stop_grace_time = @restart_grace_time = 3
  @environment = {}

  CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
    self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
  end

  # Let state_machine do its initialization stuff
  super() # no arguments intentional
end

Instance Attribute Details

#children_timerObject (readonly)

Returns the value of attribute children_timer.



61
62
63
# File 'lib/evented_bluepill/process.rb', line 61

def children_timer
  @children_timer
end

#loggerObject

Returns the value of attribute logger.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def logger
  @logger
end

#nameObject

Returns the value of attribute name.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def name
  @name
end

#process_runningObject

Returns the value of attribute process_running.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def process_running
  @process_running
end

#skip_ticks_untilObject

Returns the value of attribute skip_ticks_until.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def skip_ticks_until
  @skip_ticks_until
end

#statisticsObject (readonly)

Returns the value of attribute statistics.



61
62
63
# File 'lib/evented_bluepill/process.rb', line 61

def statistics
  @statistics
end

#timerObject (readonly)

Returns the value of attribute timer.



61
62
63
# File 'lib/evented_bluepill/process.rb', line 61

def timer
  @timer
end

#triggersObject

Returns the value of attribute triggers.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def triggers
  @triggers
end

#watchesObject

Returns the value of attribute watches.



59
60
61
# File 'lib/evented_bluepill/process.rb', line 59

def watches
  @watches
end

Instance Method Details

#actual_pidObject



319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/evented_bluepill/process.rb', line 319

def actual_pid
  @actual_pid ||= begin
    if pid_file
      if File.exists?(pid_file)
        str = File.read(pid_file)
        str.to_i if str.size > 0
      else
        logger.warning("pid_file #{pid_file} does not exist or cannot be read")
        nil
      end
    end
  end
end

#actual_pid=(pid) ⇒ Object



333
334
335
# File 'lib/evented_bluepill/process.rb', line 333

def actual_pid=(pid)
  @actual_pid = pid
end

#add_trigger(name, options = {}) ⇒ Object



183
184
185
# File 'lib/evented_bluepill/process.rb', line 183

def add_trigger(name, options = {})
  self.triggers << Trigger[name].new(self, options.merge(:logger => self.logger))
end

#add_watch(name, options = {}) ⇒ Object

Watch related methods



179
180
181
# File 'lib/evented_bluepill/process.rb', line 179

def add_watch(name, options = {})
  self.watches << ProcessConditions[name].new(name, self, options.merge(:logger => self.logger))
end

#clear_pidObject



337
338
339
# File 'lib/evented_bluepill/process.rb', line 337

def clear_pid
  @actual_pid = nil
end

#daemonize?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'lib/evented_bluepill/process.rb', line 304

def daemonize?
  !!self.daemonize
end

#determine_initial_stateObject



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/evented_bluepill/process.rb', line 187

def determine_initial_state
  if self.process_running?(true)
    self.state = 'up'
  else
    # TODO: or "unmonitored" if evented_bluepill was started in no auto-start mode.
    self.state = 'down'
  end

  # TODO move into right position
  self.set_timer
end

#dispatch!(event, reason = nil) ⇒ Object

State machine methods



153
154
155
156
# File 'lib/evented_bluepill/process.rb', line 153

def dispatch!(event, reason = nil)
  @statistics.record_event(event, reason)
  self.send("#{event}")
end

#handle_user_command(cmd) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/evented_bluepill/process.rb', line 206

def handle_user_command(cmd)
  case cmd
  when "start"
    if self.process_running?(true)
      logger.warning("Refusing to re-run start command on an already running process.")
    else
      dispatch!(:start, "user initiated")
    end
  when "stop"
    stop_process
    dispatch!(:unmonitor, "user initiated")
  when "restart"
    restart_process
  when "unmonitor"
    # When the user issues an unmonitor cmd, reset any triggers so that
    # scheduled events gets cleared
    triggers.each {|t| t.reset! }
    dispatch!(:unmonitor, "user initiated")
  end
end

#monitor_children?Boolean

Returns:

  • (Boolean)


308
309
310
# File 'lib/evented_bluepill/process.rb', line 308

def monitor_children?
  !!self.monitor_children
end

#notify_triggers(transition) ⇒ Object



174
175
176
# File 'lib/evented_bluepill/process.rb', line 174

def notify_triggers(transition)
  self.triggers.each {|trigger| trigger.notify(transition)}
end

#prepare_command(command) ⇒ Object



380
381
382
# File 'lib/evented_bluepill/process.rb', line 380

def prepare_command(command)
  command.to_s.gsub("{{PID}}", actual_pid.to_s)
end

#process_running?(force = false) ⇒ Boolean

System Process Methods

Returns:

  • (Boolean)


228
229
230
231
232
233
234
235
# File 'lib/evented_bluepill/process.rb', line 228

def process_running?(force = false)
  @process_running = nil if force # clear existing state if forced

  @process_running ||= signal_process(0)
  # the process isn't running, so we should clear the PID
  self.clear_pid unless @process_running
  @process_running
end

#record_transition(transition) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/evented_bluepill/process.rb', line 158

def record_transition(transition)
  unless transition.loopback?
    # When a process changes state, we should clear the memory of all the watches
    self.watches.each { |w| w.clear_history! }

    # Also, when a process changes state, we should re-populate its child list
    if self.monitor_children?
      self.logger.warning "Clearing child list"
      self.children_timer.each {|timer| timer.detach }
      self.children_timer.each {|timer| timer.process.watches.each {|w| w.detach }}
      self.children_timer.clear
    end
    logger.info "Going from #{transition.from_name} => #{transition.to_name}"
  end
end

#refresh_children!Object



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/evented_bluepill/process.rb', line 356

def refresh_children!
  # First prune the list of dead children
  dead_children = self.children_timer.select {|timer| !timer.process.process_running?(true) }
  dead_children.each {|timer| timer.detach }
  dead_children.each {|timer| timer.process.watches.each {|w| w.detach }}
  @children_timer -= dead_children

  # Add new found children to the list
  new_children_pids = System.get_children(self.actual_pid) - self.children_timer.map {|timer| timer.process.actual_pid}

  unless new_children_pids.empty?
    logger.info "Existing children: #{self.children_timer.collect{|c| c.process.actual_pid}.join(",")}. Got new children: #{new_children_pids.inspect} for #{actual_pid}"
  end

  # Construct a new process wrapper for each new found children
  new_children_pids.each do |child_pid|
    name = "<child(pid:#{child_pid})>"
    logger = self.logger.prefix_with(name)

    child = self.child_process_factory.create_child_process(name, child_pid, logger)
    @children_timer << child.timer
  end
end

#restart_processObject



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/evented_bluepill/process.rb', line 281

def restart_process
  if restart_command
    cmd = self.prepare_command(restart_command)

    logger.warning "Executing restart command: #{cmd}"

    with_timeout(restart_grace_time) do
      result = System.execute_blocking(cmd, self.system_command_options)

      unless result[:exit_code].zero?
        logger.warning "Restart command execution returned non-zero exit code:"
        logger.warning result.inspect
      end
    end

    self.skip_ticks_for(restart_grace_time)
  else
    logger.warning "No restart_command specified. Must stop and start to restart"
    self.stop_process
    # the tick will bring it back.
  end
end

#set_timerObject



199
200
201
202
203
204
# File 'lib/evented_bluepill/process.rb', line 199

def set_timer
  @timer = EventedBluepill::ProcessTimer.new(self)
  EventedBluepill::Event.attach(self.timer)

  self.watches.each {|w| EventedBluepill::Event.attach(w) }
end

#signal_process(code) ⇒ Object



312
313
314
315
316
317
# File 'lib/evented_bluepill/process.rb', line 312

def signal_process(code)
  ::Process.kill(code, actual_pid)
  true
rescue
  false
end

#skip_ticks_for(seconds) ⇒ Object

Internal State Methods



346
347
348
349
350
# File 'lib/evented_bluepill/process.rb', line 346

def skip_ticks_for(seconds)
  # TODO: should this be addative or longest wins?
  #       i.e. if two calls for skip_ticks_for come in for 5 and 10, should it skip for 10 or 15?
  self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds.to_i
end

#skipping_ticks?Boolean

Returns:

  • (Boolean)


352
353
354
# File 'lib/evented_bluepill/process.rb', line 352

def skipping_ticks?
  self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
end

#start_processObject



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/evented_bluepill/process.rb', line 237

def start_process
  logger.warning "Executing start command: #{start_command}"

  if self.daemonize?
    System.daemonize(start_command, self.system_command_options)

  else
    # This is a self-daemonizing process
    with_timeout(start_grace_time) do
      result = System.execute_blocking(start_command, self.system_command_options)

      unless result[:exit_code].zero?
        logger.warning "Start command execution returned non-zero exit code:"
        logger.warning result.inspect
      end
    end
  end

  self.skip_ticks_for(start_grace_time)
end

#stop_processObject



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/evented_bluepill/process.rb', line 258

def stop_process
  if stop_command
    cmd = self.prepare_command(stop_command)
    logger.warning "Executing stop command: #{cmd}"

    with_timeout(stop_grace_time) do
      result = System.execute_blocking(cmd, self.system_command_options)

      unless result[:exit_code].zero?
        logger.warning "Stop command execution returned non-zero exit code:"
        logger.warning result.inspect
      end
    end

  else
    logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
    signal_process("TERM")
  end
  self.unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?

  self.skip_ticks_for(stop_grace_time)
end

#system_command_optionsObject



384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/evented_bluepill/process.rb', line 384

def system_command_options
  {
    :uid         => self.uid,
    :gid         => self.gid,
    :working_dir => self.working_dir,
    :environment => self.environment,
    :pid_file    => self.pid_file,
    :logger      => self.logger,
    :stdin       => self.stdin,
    :stdout      => self.stdout,
    :stderr      => self.stderr
  }
end


341
342
343
# File 'lib/evented_bluepill/process.rb', line 341

def unlink_pid
  File.unlink(pid_file) if pid_file && File.exists?(pid_file)
end

#with_timeout(secs, &blk) ⇒ Object



398
399
400
401
402
403
404
405
# File 'lib/evented_bluepill/process.rb', line 398

def with_timeout(secs, &blk)
  Timeout.timeout(secs.to_f, &blk)

rescue Timeout::Error
  logger.err "Execution is taking longer than expected. Unmonitoring."
  logger.err "Did you forget to tell evented_bluepill to daemonize this process?"
  self.dispatch!("unmonitor")
end