Class: Bluepill::Process

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

Constant Summary collapse

CONFIGURABLE_ATTRIBUTES =
[
  :start_command, 
  :stop_command, 
  :restart_command, 
  
  :stdout,
  :stderr,
  :stdin,
  
  :daemonize, 
  :pid_file, 
  :working_dir,
  
  :start_grace_time, 
  :stop_grace_time, 
  :restart_grace_time,
  
  :uid,
  :gid,
  
  :monitor_children,
  :child_process_template
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Process.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/bluepill/process.rb', line 87

def initialize(process_name, options = {})      
  @name = process_name
  @event_mutex = Monitor.new
  @transition_history = Util::RotationalArray.new(10)
  @watches = []
  @triggers = []
  @children = []
  @statistics = ProcessStatistics.new
  
  @monitor_children = options[:monitor_children] || false
  
  %w(start_grace_time stop_grace_time restart_grace_time).each do |grace|
    instance_variable_set("@#{grace}", options[grace.to_sym] || 3)
  end
  
  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()
end

Instance Attribute Details

#childrenObject (readonly)

Returns the value of attribute children.



32
33
34
# File 'lib/bluepill/process.rb', line 32

def children
  @children
end

#loggerObject

Returns the value of attribute logger.



30
31
32
# File 'lib/bluepill/process.rb', line 30

def logger
  @logger
end

#nameObject

Returns the value of attribute name.



30
31
32
# File 'lib/bluepill/process.rb', line 30

def name
  @name
end

#skip_ticks_untilObject

Returns the value of attribute skip_ticks_until.



30
31
32
# File 'lib/bluepill/process.rb', line 30

def skip_ticks_until
  @skip_ticks_until
end

#statisticsObject (readonly)

Returns the value of attribute statistics.



32
33
34
# File 'lib/bluepill/process.rb', line 32

def statistics
  @statistics
end

#triggersObject

Returns the value of attribute triggers.



30
31
32
# File 'lib/bluepill/process.rb', line 30

def triggers
  @triggers
end

#watchesObject

Returns the value of attribute watches.



30
31
32
# File 'lib/bluepill/process.rb', line 30

def watches
  @watches
end

Instance Method Details

#actual_pidObject



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

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



334
335
336
# File 'lib/bluepill/process.rb', line 334

def actual_pid=(pid)
  @actual_pid = pid
end

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



169
170
171
# File 'lib/bluepill/process.rb', line 169

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



165
166
167
# File 'lib/bluepill/process.rb', line 165

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

#clear_pidObject



338
339
340
# File 'lib/bluepill/process.rb', line 338

def clear_pid
  @actual_pid = nil
end

#daemonize?Boolean

Returns:

  • (Boolean)


305
306
307
# File 'lib/bluepill/process.rb', line 305

def daemonize?
  !!self.daemonize
end

#deep_copyObject



383
384
385
# File 'lib/bluepill/process.rb', line 383

def deep_copy
  Marshal.load(Marshal.dump(self))
end

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

State machine methods



137
138
139
140
141
142
# File 'lib/bluepill/process.rb', line 137

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

#handle_user_command(cmd) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/bluepill/process.rb', line 197

def handle_user_command(cmd)
  case cmd
  when "boot"
    # This is only called when bluepill is initially starting up
    if process_running?(true)
      # process was running even before bluepill was
      self.state = 'up'
    else
      self.state = 'starting'
    end
    
  when "start"
    if process_running?(true) && daemonize?
      logger.warning("Refusing to re-run start command on an automatically daemonized process to preserve currently running process pid file.")
      return
    end
    dispatch!(:start, "user initiated")

  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)


309
310
311
# File 'lib/bluepill/process.rb', line 309

def monitor_children?
  !!self.monitor_children
end

#notify_triggers(transition) ⇒ Object



160
161
162
# File 'lib/bluepill/process.rb', line 160

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

#process_command(cmd) ⇒ Object



387
388
389
# File 'lib/bluepill/process.rb', line 387

def process_command(cmd)
  cmd.to_s.gsub("{{PID}}", actual_pid.to_s)
end

#process_running?(force = false) ⇒ Boolean

System Process Methods

Returns:

  • (Boolean)


231
232
233
234
235
236
# File 'lib/bluepill/process.rb', line 231

def process_running?(force = false)
  @process_running = nil if force
  @process_running ||= signal_process(0)
  self.clear_pid unless @process_running
  @process_running
end

#record_transition(transition) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/bluepill/process.rb', line 144

def record_transition(transition)
  unless transition.loopback?
    @transitioned = true
    
    # 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.clear
    end
    logger.info "Going from #{transition.from_name} => #{transition.to_name}"
  end
end

#refresh_children!Object



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

def refresh_children!
  # First prune the list of dead children
  @children.delete_if {|child| !child.process_running?(true) }
  
  # Add new found children to the list
  new_children_pids = System.get_children(self.actual_pid) - @children.map {|child| child.actual_pid}
 
  unless new_children_pids.empty?
    logger.info "Existing children: #{@children.collect{|c| c.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|
    child = self.child_process_template.deep_copy
    
    child.name = "<child(pid:#{child_pid})>"
    child.actual_pid = child_pid
    child.logger = self.logger.prefix_with(child.name)
    
    child.initialize_state_machines
    child.state = "up"
    
    @children << child
  end
end

#restart_processObject



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

def restart_process
  if restart_command
    cmd = process_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

#run_watchesObject



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/bluepill/process.rb', line 173

def run_watches
  now = Time.now.to_i

  threads = self.watches.collect do |watch|
    [watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
  end
  
  @transitioned = false
  
  threads.inject([]) do |events, (watch, thread)|
    thread.join
    if thread[:events].size > 0
      logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
      thread[:events].each do |event|
        events << [event, watch.to_s]
      end
    end
    events
  end.each do |(event, reason)|
    break if @transitioned
    self.dispatch!(event, reason)
  end
end

#signal_process(code) ⇒ Object



313
314
315
316
317
318
# File 'lib/bluepill/process.rb', line 313

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

#skip_ticks_for(seconds) ⇒ Object

Internal State Methods



347
348
349
350
351
# File 'lib/bluepill/process.rb', line 347

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)


353
354
355
# File 'lib/bluepill/process.rb', line 353

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

#start_processObject



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

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



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

def stop_process      
  if stop_command
    cmd = process_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



391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/bluepill/process.rb', line 391

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

#tickObject



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/bluepill/process.rb', line 110

def tick
  return if self.skipping_ticks?
  self.skip_ticks_until = nil

  # clear the memoization per tick
  @process_running = nil

  # run state machine transitions
  super

  if self.up?
    run_watches
    
    if monitor_children?
      refresh_children!
      children.each {|child| child.tick}
    end
  end
end


342
343
344
# File 'lib/bluepill/process.rb', line 342

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

#with_timeout(secs, &blk) ⇒ Object



404
405
406
407
408
409
410
411
# File 'lib/bluepill/process.rb', line 404

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 bluepill to daemonize this process?"
  self.dispatch!("unmonitor")
end