Module: Rbg

Defined in:
lib/rbg.rb,
lib/rbg/config.rb

Defined Under Namespace

Classes: Config, Error

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.child_processesObject

 An array of child PIDs for the current process which have been spawned



9
10
11
# File 'lib/rbg.rb', line 9

def child_processes
  @child_processes
end

.config_fileObject

The path to the config file that was specified



12
13
14
# File 'lib/rbg.rb', line 12

def config_file
  @config_file
end

Class Method Details

.configObject

Return a configration object for this backgroundable application.



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

def config
  @config ||= Rbg::Config.new
end

.fork_worker(id) ⇒ Object

Fork a single worker



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/rbg.rb', line 130

def fork_worker(id)
  pid = fork do
    # Set process name
    $0="#{config.name}[#{id}]"
    
    # Ending workers on INT is not useful or desirable
    Signal.trap('INT', proc {})
    Signal.trap('HUP', proc {})
    # Restore normal behaviour
    Signal.trap('TERM', proc {Process.exit(0)})
    
    # Execure before_fork code
    if config.after_fork.is_a?(Proc)
      self.config.after_fork.call
    end
    
    if self.config.script.is_a?(String)
      require self.config.script
    elsif self.config.script.is_a?(Proc)
      self.config.script.call
    end
  end
  
  # Print some debug info and save the pid
  logger.info "Spawned #{config.name}[#{id}] (with PID #{pid})"
  STDOUT.flush
  
  # Detach to eliminate Zombie processes later
  Process.detach(pid)
  
  # Save the worker PID into the Parent's child process list
  self.child_processes[id]                    ||= {}
  self.child_processes[id][:pid]              ||= pid
  self.child_processes[id][:respawns]         ||= 0
  self.child_processes[id][:started_at]         = Time.now
end

.fork_workers(n) ⇒ Object

Wrapper to fork multiple workers



123
124
125
126
127
# File 'lib/rbg.rb', line 123

def fork_workers(n)
  n.times do |i|
    self.fork_worker(i)
  end
end

.kill_child_process(id) ⇒ Object

Kill a given child process



168
169
170
171
172
173
174
175
176
177
178
# File 'lib/rbg.rb', line 168

def kill_child_process(id)
  if opts = self.child_processes[id]
    logger.info "Killing #{config.name}[#{id}] (with PID #{opts[:pid]})"
    STDOUT.flush
    begin
      Process.kill('TERM', opts[:pid])
    rescue
      logger.info "Process already gone away"
    end
  end
end

.kill_child_processesObject

Kill all child processes



181
182
183
184
185
186
# File 'lib/rbg.rb', line 181

def kill_child_processes
  logger.info 'Killing child processes...'
  STDOUT.flush
  self.child_processes.keys.each { |id| kill_child_process(id) }
  self.child_processes = Hash.new
end

.load_configObject

Load or reload the config file defined at startup



251
252
253
254
255
256
257
258
# File 'lib/rbg.rb', line 251

def load_config
  @config = nil
  if File.exist?(self.config_file.to_s)
    load self.config_file
  else
    raise Error, "Configuration file not found at '#{config_file}'"
  end
end

.loggerObject

Return a logger object for this application



20
21
22
# File 'lib/rbg.rb', line 20

def logger
  self.config.logger
end

.master_processObject

This is the master process, it spawns some workers then loops



189
190
191
192
193
194
195
196
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/rbg.rb', line 189

def master_process
  # Log the master PID
  logger.info "New master process: #{Process.pid}"
  STDOUT.flush
  
  # Set the process name
  $0="#{self.config.name}[Master]"
  
  # Fork a Parent process
  # This will load the before_fork in a clean process then fork the script as required
  self.start_parent
  
  # A restart is not required yet...
  restart_needed = false
  
  # If we get a USR1, set this process as waiting for a restart
  Signal.trap("USR1", proc {
    logger.info "Master got a USR1."
    restart_needed = true
  })
  
  # If we get a TERM, send the existing workers a TERM before bowing out
  Signal.trap("TERM", proc {
    logger.info "Master got a TERM."
    STDOUT.flush
    kill_child_processes
    Process.exit(0)
  })
  
  # INT is useful for when we don't want to background
  Signal.trap("INT", proc {
    logger.info "Master got an INT."
    STDOUT.flush
    kill_child_processes
    Process.exit(0)
  })

  Signal.trap('HUP', proc {})
  
  # Main loop, we mostly idle, but check if the parent we created has died and exit
  loop do
    sleep 2
    if restart_needed
      STDOUT.flush
      self.kill_child_processes
      load_config
      self.start_parent
      restart_needed = false
    end
    
    self.child_processes.each do |id, opts|
      begin
        Process.getpgid(opts[:pid])
      rescue Errno::ESRCH
        logger.info "Parent process #{config.name}[#{id}] has died (from PID #{opts[:pid]}), exiting master"
        Process.exit(0)
      end
    end
  end
end

.pid_from_fileObject

Get the PID from the pidfile defined in the config

Raises:



316
317
318
319
320
321
322
323
# File 'lib/rbg.rb', line 316

def pid_from_file
  raise Error, "PID not defined in '#{config_file}'" unless self.config.pid_path
  begin
    File.read(self.config.pid_path).strip.to_i
  rescue
    raise Error, "PID file not found"
  end
end

.reload(config_file, options = {}) ⇒ Object

Reload the running instance



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/rbg.rb', line 345

def reload(config_file, options = {})
  options[:environment] ||= "development"
  $rbg_env = options[:environment].dup

  # Define the config file then load it
  self.config_file = config_file
  self.load_config
  
  pid = self.pid_from_file
  
  begin
    Process.kill('USR1', pid)
    puts "Sent USR1 to PID #{pid}"
  rescue
    raise Error, "Process #{pid} not found"
  end
end

.start(config_file, options = {}) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/rbg.rb', line 260

def start(config_file, options = {})
  options[:background]  ||= false
  options[:environment] ||= "development"
  $rbg_env = options[:environment].dup
  
  # Define the config file then load it
  self.config_file = config_file
  self.load_config
  
  # If the PID file is set and exists, check that the process is not running
  if self.config.pid_path and File.exists?(self.config.pid_path)
    oldpid = File.read(self.config.pid_path)
    begin
      Process.getpgid( oldpid.to_i )
      raise Error, "Process already running! PID #{oldpid}"
    rescue Errno::ESRCH
      # No running process
      false
    end
  end
  
  # Initialize child process array
  self.child_processes = Hash.new
  
  if options[:background]
    # Fork the master control process and return to a shell
    master_pid = fork do
      # Ignore input and log to a file
      STDIN.reopen('/dev/null')
      if self.config.log_path
        STDOUT.reopen(self.config.log_path, 'a')
        STDOUT.sync = true
        STDERR.reopen(self.config.log_path, 'a')
        STDERR.sync = true
      else
        raise Error, "Log location not specified in '#{config_file}'"
      end
      
      self.master_process
    end
    
    # Ensure the process is properly backgrounded
    Process.detach(master_pid)
    if self.config.pid_path
      File.open(self.config.pid_path, 'w') {|f| f.write(master_pid) }
    end
    
    logger.info "Master started as PID #{master_pid}"
  else
    # Run using existing STDIN / STDOUT and set logger to use use STDOUT regardless
    self.config.logger = MonoLogger.new(STDOUT)
    self.master_process
  end
end

.start_parentObject

Creates a ‘parent’ process. This is responsible for executing ‘before_fork’ and then forking the worker processes.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rbg.rb', line 26

def start_parent
  # Record the PID of this parent in the Master
  parent_pid = fork do
    # Clear the child process list as this fork doesn't have any children yet
    self.child_processes = Hash.new

    # Set the process name (Parent)
    $0="#{self.config.name}[Parent]"

    # Debug information
    logger.info "New parent process: #{Process.pid}"
    STDOUT.flush
    
    # Run the before_fork function
    if config.before_fork.is_a?(Proc)
      self.config.before_fork.call
    end
    
    # Fork an appropriate number of workers
    self.fork_workers(self.config.workers)
    
    # If we get a TERM, send the existing workers a TERM then exit
    Signal.trap("TERM", proc {
      # Debug output
      logger.info "Parent got a TERM."
      STDOUT.flush

      # Send TERM to workers
      kill_child_processes
      
      # Exit the parent
      Process.exit(0)
    })
    
    # Ending parent processes on INT is not useful or desirable
    # especially when running in the foreground
    Signal.trap('INT', proc {})
    Signal.trap('HUP', proc {})
    
    # Parent loop, the purpose of this is simply to do nothing until we get a signal
    # We will exit if all child processes die
    # We may add memory management code here in the future
    loop do
      sleep 2
      child_processes.dup.each do |id, opts|
        begin
          
          Process.getpgid(opts[:pid])
          
          if config.memory_limit
            # Lookup the memory usge for this PID
            memory_usage = `ps -o rss= -p #{opts[:pid]}`.strip.to_i / 1024
            if memory_usage > config.memory_limit
              logger.info "#{self.config.name}[#{id}] is using #{memory_usage}MB of memory (limit: #{config.memory_limit}MB). It will be killed."
              kill_child_process(id)
            end
          end
          
        rescue Errno::ESRCH
          logger.info "Child process #{config.name}[#{id}] has died (from PID #{opts[:pid]})"
          child_processes[id][:pid] = nil
          
          if config.respawn
            if opts[:started_at] > Time.now - config.respawn_limits[1]
              if opts[:respawns] >= config.respawn_limits[0]
                logger.info "Process #{config.name}[#{id}] has instantly respawned #{opts[:respawns]} times. It won't be respawned again."
                child_processes.delete(id)
              else
                logger.info "Process has died within #{config.respawn_limits[1]}s of the last spawn."
                child_processes[id][:respawns] += 1
                fork_worker(id)
              end
            else
              logger.info "Process was started more than #{config.respawn_limits[1]}s since the last spawn. Resetting spawn counter"
              child_processes[id][:respawns] = 0
              fork_worker(id)
            end
          else
            child_processes.delete(id)
          end
        end
      end
      
      if child_processes.empty?
        logger.info "All child processes died, exiting parent"
        Process.exit(0)
      end
    end
  end
  
  # Store the PID for the parent
  child_processes[0] = {:pid => parent_pid, :respawns => 0}
  # Ensure the new parent is detached
  Process.detach(parent_pid)
end

.stop(config_file, options = {}) ⇒ Object

Stop the running instance



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/rbg.rb', line 326

def stop(config_file, options = {})
  options[:environment] ||= "development"
  $rbg_env = options[:environment].dup

  # Define the config file then load it
  self.config_file = config_file
  self.load_config
  
  pid = self.pid_from_file
  
  begin
    Process.kill('TERM', pid)
    puts "Sent TERM to PID #{pid}"
  rescue
    raise Error, "Process #{pid} not found"
  end
end