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



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



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



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



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



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