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,
  :environment,
  
  :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.



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 88

def initialize(process_name, options = {})      
  @name = process_name
  @event_mutex = Monitor.new
  @transition_history = Util::RotationalArray.new(10)
  @watches = []
  @triggers = []
  @children = []
  @statistics = ProcessStatistics.new
  
  # 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

#childrenObject (readonly)

Returns the value of attribute children.



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

def children
  @children
end

#loggerObject

Returns the value of attribute logger.



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

def logger
  @logger
end

#nameObject

Returns the value of attribute name.



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

def name
  @name
end

#skip_ticks_untilObject

Returns the value of attribute skip_ticks_until.



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

def skip_ticks_until
  @skip_ticks_until
end

#statisticsObject (readonly)

Returns the value of attribute statistics.



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

def statistics
  @statistics
end

#triggersObject

Returns the value of attribute triggers.



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

def triggers
  @triggers
end

#watchesObject

Returns the value of attribute watches.



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

def watches
  @watches
end

Instance Method Details

#actual_pidObject



319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/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/bluepill/process.rb', line 333

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



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

def clear_pid
  @actual_pid = nil
end

#daemonize?Boolean

Returns:

  • (Boolean)


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

def daemonize?
  !!self.daemonize
end

#deep_copyObject



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

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

#determine_initial_stateObject



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

def determine_initial_state
  if self.process_running?(true)
    self.state = 'up'
  else
    # TODO: or "unmonitored" if bluepill was started in no auto-start mode.
    self.state = 'down'
  end
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



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/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/bluepill/process.rb', line 308

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

#prepare_command(command) ⇒ Object



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

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/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



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



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

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



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/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

#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



312
313
314
315
316
317
# File 'lib/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/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/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/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/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



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

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

#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?
    self.run_watches
    
    if self.monitor_children?
      refresh_children!
      children.each {|child| child.tick}
    end
  end
end


341
342
343
# File 'lib/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



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