Skip to content

Instantly share code, notes, and snippets.

@ahoward
Created October 10, 2011 01:55
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ahoward/1274473 to your computer and use it in GitHub Desktop.
#! /usr/local/rbenv/versions/1.8.7-p352-redmine/bin/ruby
### file: ./script/passenger
##
# get busy!
#
Background.logger.info("process #{ Process.pid } starting...")
logger = Background.logger
loop do
cmd = "passenger start -p 4200"
Background.logger.info("cmd: #{ cmd }")
system(cmd)
seconds = Rails.env == 'production' ? 42 : 2
sleep(seconds) # NOTE - signals will wake this up!
end
BEGIN {
ENV['PATH'] = "/usr/local/rbenv/versions/1.8.7-p352-redmine/bin:#{ ENV['PATH'] }"
script = __FILE__
require(File.dirname(script) + '/../lib/background.rb')
Background.run!(script)
}
##
# background.rb is magic. it let's you have background rails code running
# with
#
# . your app's env loaded
# . that restarts like magic on a re-deploy
#
# usage in, say, ./script/run_jobs_in_the_background.rb, looks like:
#
# #! /usr/bin/env ruby
#
# Background.logger.info("process #{ Process.pid } starting...")
#
# logger = Background.logger
#
# loop do
# begin
# Job.each_that_needs_to_be_run do |job|
# job.run!
# end
# rescue => e
# Background.logger.error(e)
# seconds = Rails.env == 'production' ? 42 : 2
# sleep(seconds) # NOTE - signals will wake this up!
# end
# end
#
# BEGIN {
# script = __FILE__
# require(File.dirname(script) + '/../lib/background.rb')
# Background.run!(script)
# }
#
# it includes some basic daemon-y stuff including a lock that prevents the
# script from running twice so cron'ing it every one minute will NOT stack
# processes
#
module Background
## libs
#
require 'fileutils'
require 'ostruct'
require 'rbconfig'
SIGHUP = Signal.list['HUP']
%w(
script
mode
dirname
basename
script_dir
rails_root
public_system
private_system
basename_dir
lock_file
log_file
pid_file
restart_txt
started_at
signals
).each{|a| attr(a)}
def run!(*args)
setup(*args).run()
end
## setup
#
def setup(script)
@script = File.expand_path(script)
@exec = command_line
@dirname = File.expand_path(File.dirname(@script))
@basename = File.basename(@script)
@script_dir = File.expand_path(File.dirname(@script))
@rails_root = File.dirname(@script_dir)
@public_system = File.expand_path(File.join(@rails_root, 'public', 'system'))
@private_system = File.expand_path(File.join(@rails_root, 'private', 'system'))
if test(?d, @private_system)
@background_dir = File.join(@private_system, 'background')
else
@background_dir = File.join(@public_system, 'background')
end
@basename_dir = File.join(@background_dir, @basename)
@lock_file = File.join(@basename_dir, 'lock')
@log_file = File.join(@basename_dir, 'log')
@pid_file = File.join(@basename_dir, 'pid')
@mode = (ARGV.shift || 'RUN').upcase
FileUtils.mkdir_p(@basename_dir) rescue nil
FileUtils.touch(@lock_file) rescue nil
FileUtils.touch(@log_file) rescue nil
@signals = []
@restart_txt = File.join(@rails_root, 'tmp', 'restart.txt')
@started_at = Time.now
@monitor = new_monitor
self
end
def command_line
[which_ruby, @script, *ARGV.map{|arg| arg.dup}]
end
def new_monitor
Thread.new(self) do |bg|
Thread.current.abort_on_exception = true
loop do
#if bg.updated?
#bg.logger.info('RESTART - update') if bg.logger
#bg.restart!
#end
if bg.redeployed?
bg.logger.info('RESTART - deploy') if bg.logger
bg.restart!
end
if bg.signaled?
bg.logger.info('RESTART - signal') if bg.logger
bg.restart!
end
sleep(42)
end
end
end
def restart!
if fork
unlock!
Kernel.exec(*@exec)
else
exit(42)
end
end
def updated?
a = File.stat(__FILE__).mtime rescue @started_at
b = File.stat(@script).mtime rescue @started_at
a > @started_at or b > @started_at
end
def redeployed?
t = File.stat(@restart_txt).mtime rescue @started_at
t > @started_at
end
def signaled?
!signals.empty?
end
def which_ruby
c = ::Config::CONFIG
ruby = File::join(c['bindir'], c['ruby_install_name']) << c['EXEEXT']
raise "ruby @ #{ ruby } not executable!?" unless test(?e, ruby)
ruby
end
# run based on mode
#
def run
case @mode
when /RUN/i
# aquire the lock and run, otherwise exit with 42 (already running)
#
lock!(:complain => true)
# record the pid
#
pid!
# setup signal handling
#
trap!
# boot the rails app
#
boot!
# setup logging
#
$logger = Logging.logger(STDERR)
when /RESTART/i
# try hard to find the running process by pid and signal it to restart
#
pid = Integer(IO.read(@pid_file)) rescue nil
Process.kill('HUP', pid) rescue nil if pid
exit(0)
when /START/i
# aquire the lock and run, otherwise exit with 42 (already running)
#
lock!(:complain => true)
# boot the rails app
#
boot!
# daemonize
#
daemonize!
# dump pid to file
#
pid!
# setup signal handling
#
trap!
# setup logging
#
number_rolled = 7
megabytes = 2 ** 20
max_size = 42 * megabytes
options = { :safe => true } # for multi-process safe rolling, needs lockfile gem!
logger = ::Logging.logger(@log_file, number_rolled, max_size, options)
$logger = logger # HACK
# redirect io to log_file
#
open(@log_file, 'r+') do |fd|
fd.sync = true
STDOUT.reopen(fd)
STDERR.reopen(fd)
end
STDOUT.sync = true
STDERR.sync = true
when /PID/i
# report the pid of running instance iff possible
#
pid = Integer(IO.read(@pid_file)) rescue nil
if pid
puts(pid)
exit(0)
else
exit(1)
end
exit(1)
when /FUSER/i
# try to use fuser to display processes holding the lock
#
exec("fuser #{ @lock_file.inspect }")
when /STOP/i
# try hard to find the running process by pid and kill it
#
pid = Integer(IO.read(@pid_file)) rescue nil
if pid
10.times do
Process.kill('TERM', pid) rescue nil
begin
Process.kill(0, pid)
sleep(rand)
rescue Errno::ESRCH
puts(pid)
exit(0)
end
end
Process.kill(-9, pid) rescue nil
sleep(rand)
begin
Process.kill(0, pid)
rescue Errno::ESRCH
puts(pid)
exit(0)
end
end
exit(1)
when /SIGNAL/i
# signal a running process, if any
#
pid = Integer(IO.read(@pid_file)) rescue nil
if pid
signal = ARGV.shift || 'HUP'
Process.kill(signal, pid)
puts(pid)
exit(0)
end
exit(42)
when /LOG/i
# display the log file
#
puts(@log_file)
exit(42)
when /TAIL/i
# tail the log file
#
system("tail -F #{ @log_file.inspect }")
exit(42)
end
end
# we'll be needing these
#
def boot!
Dir.chdir(@rails_root)
require File.join(@rails_root, 'config', 'boot')
require File.join(@rails_root, 'config', 'environment')
end
def lock!(options = {})
complain = options['complain'] || options[:complain]
fd = open(@lock_file, 'r+')
status = fd.flock(File::LOCK_EX|File::LOCK_NB)
unless status == 0
if complain
pid = Integer(IO.read(@pid_file)) rescue '?'
warn("instance(#{ pid }) is already running!")
end
exit(42)
end
@lock = fd # prevent garbage collection from closing the file!
end
def unlock!
@lock.flock(File::LOCK_UN|File::LOCK_NB) if @lock
end
def pid!
open(@pid_file, 'w+') do |fd|
fd.puts(Process.pid)
end
at_exit{ FileUtils.rm_f(@pid_file) }
end
def trap!
%w( SIGHUP SIGALRM SIGUSR1 SIGUSR2 ).each do |signal|
trap(signal){|s| Background.signals.push(s) }
end
trap('SIGTERM'){ exit(0) }
trap('SIGINT'){ exit(0) }
end
def logger
$logger || (
require 'logger' unless defined?(Logger)
Logger.new(STDERR)
)
end
def logging_errors(&block)
begin
block.call()
rescue SignalException => e
logger.info(e)
exit(0)
rescue => e
logger.error(e)
end
end
def daemonize!(options = {})
# setup
#
chdir = options[:chdir] || options['chdir'] || '.'
umask = options[:umask] || options['umask'] || 0
# setup pipes we'll use for IPC
#
ra, wa = IO.pipe
rb, wb = IO.pipe
pid = fork
parent = !!pid
child = !parent
# in the parent we wait for the child to send back the grandchild's pid
#
if parent
at_exit{ exit! }
wa.close
r = ra
rb.close
w = wb
pid = r.gets
w.puts(pid)
exit(0)
end
# in the child start a grandchild. handshake the pid with the parent.
#
if child
ra.close
w = wa
wb.close
r = rb
open("/dev/null", "r+") do |fd|
#STDIN.reopen(fd)
#STDOUT.reopen(fd)
#STDERR.reopen(fd)
end
Process::setsid rescue nil
pid = fork
if pid
w.puts(pid)
r.gets
exit!
else
Dir::chdir(chdir)
File::umask(umask)
$DAEMON = true
at_exit{ exit! }
end
end
end
extend(self)
end
@andyl
Copy link

andyl commented Apr 21, 2012

How come you're not using something like upstart ??

@ahoward
Copy link
Author

ahoward commented Apr 21, 2012

  1. this works on any machine, heroku, aws, home-spun, localhost....
  2. upstart is stupid with restarts, this hooks into the 'normal' rails' restart mechanism that passenger, et al, use - ./tmp/restart.txt
  3. this requires zero config: deploy, forget about it.
  4. i like having all background logs/pids, etc centralized in my app dir, not scatter around on some unix fs layout

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment