From 7a8ed3bbbf44b273622be0e324a616065f5656a8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Apr 2006 00:00:50 +0000 Subject: Added pid file usage to script/process/spawner and script/process/reaper along with a directive in default config/lighttpd.conf file to record the pid. They will all save their pid file in tmp/pids [DHH] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4294 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/CHANGELOG | 5 + railties/Rakefile | 2 +- railties/configs/lighttpd.conf | 1 + railties/lib/commands/process/reaper.rb | 145 +++++++++++---------- railties/lib/commands/process/spawner.rb | 58 ++++++--- .../generators/applications/app/app_generator.rb | 1 + railties/lib/tasks/tmp.rake | 9 +- 7 files changed, 133 insertions(+), 88 deletions(-) (limited to 'railties') diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 6e6ac8c30e..281e495865 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,3 +1,8 @@ +*SVN* + +* Added pid file usage to script/process/spawner and script/process/reaper along with a directive in default config/lighttpd.conf file to record the pid. They will all save their pid file in tmp/pids [DHH] + + *1.1.2* (April 9th, 2005) * Mention in docs that config.frameworks doesn't work when getting Rails via Gems. Closes #4857. [Alisdair McDiarmid] diff --git a/railties/Rakefile b/railties/Rakefile index 40f5f4e440..6f47b08301 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -30,7 +30,7 @@ RUBY_FORGE_USER = "webster132" BASE_DIRS = %w( app config/environments components db doc log lib lib/tasks public script script/performance script/process test vendor vendor/plugins - tmp/sessions tmp/cache tmp/sockets + tmp/sessions tmp/cache tmp/sockets tmp/pids ) APP_DIRS = %w( models controllers helpers views views/layouts ) diff --git a/railties/configs/lighttpd.conf b/railties/configs/lighttpd.conf index c23d152731..ed68d714bb 100644 --- a/railties/configs/lighttpd.conf +++ b/railties/configs/lighttpd.conf @@ -7,6 +7,7 @@ server.port = 3000 server.modules = ( "mod_rewrite", "mod_accesslog", "mod_fastcgi", "mod_compress", "mod_expire" ) server.error-handler-404 = "/dispatch.fcgi" +server.pid-file = CWD + "/tmp/pids/lighttpd.pid" server.document-root = CWD + "/public/" server.errorlog = CWD + "/log/lighttpd.error.log" diff --git a/railties/lib/commands/process/reaper.rb b/railties/lib/commands/process/reaper.rb index 73b6b97f48..6ac2f8e8bd 100644 --- a/railties/lib/commands/process/reaper.rb +++ b/railties/lib/commands/process/reaper.rb @@ -4,89 +4,89 @@ require 'uri' if RUBY_PLATFORM =~ /mswin32/ then abort("Reaper is only for Unix") end -# Instances of this class represent a single running process. Processes may -# be queried by "keyword" to find those that meet a specific set of criteria. -class ProgramProcess +class Killer class << self - # Searches for all processes matching the given keywords, and then invokes # a specific action on each of them. This is useful for (e.g.) reloading a # set of processes: # - # ProgramProcess.process_keywords(:reload, "basecamp") - def process_keywords(action, *keywords) - processes = keywords.collect { |keyword| find_by_keyword(keyword) }.flatten - - if processes.empty? - puts "Couldn't find any process matching: #{keywords.join(" or ")}" - else - processes.each do |process| - puts "#{action.capitalize}ing #{process}" - process.send(action) - end - end + # Killer.process(:reload, "/tmp/pids", "dispatcher.*.pid") + def process(action, pid_path, pattern) + new(pid_path, pattern).process(action) end - # Searches for all processes matching the given keyword: - # - # ProgramProcess.find_by_keyword("basecamp") - def find_by_keyword(keyword) - process_lines_with_keyword(keyword).split("\n").collect { |line| - next if line =~ /inq|ps axww|grep|spawn-fcgi|spawner|reaper/ - pid, *command = line.split - new(pid, command.join(" ")) - }.compact + # Forces the (rails) application to reload by sending a +HUP+ signal to the + # process. + def reload(pid) + `kill -s HUP #{pid}` end - private - def process_lines_with_keyword(keyword) - `ps axww -o 'pid command' | grep #{keyword}` - end - end - - # Create a new ProgramProcess instance that represents the process with the - # given pid, running the given command. - def initialize(pid, command) - @pid, @command = pid, command - end + # Force the (rails) application to restart by sending a +USR2+ signal to the + # process. + def restart(pid) + `kill -s USR2 #{pid}` + end - # Forces the (rails) application to reload by sending a +HUP+ signal to the - # process. - def reload - `kill -s HUP #{@pid}` - end + # Forces the (rails) application to gracefully terminate by sending a + # +TERM+ signal to the process. + def graceful(pid) + `kill -s TERM #{pid}` + end - # Forces the (rails) application to gracefully terminate by sending a - # +TERM+ signal to the process. - def graceful - `kill -s TERM #{@pid}` - end + # Forces the (rails) application to terminate immediately by sending a -9 + # signal to the process. + def kill(pid) + `kill -9 #{pid}` + end - # Forces the (rails) application to terminate immediately by sending a -9 - # signal to the process. - def kill - `kill -9 #{@pid}` + # Send a +USR1+ signal to the process. + def usr1(pid) + `kill -s USR1 #{pid}` + end end - # Send a +USR1+ signal to the process. - def usr1 - `kill -s USR1 #{@pid}` + def initialize(pid_path, pattern) + @pid_path, @pattern = pid_path, pattern end - # Force the (rails) application to restart by sending a +USR2+ signal to the - # process. - def restart - `kill -s USR2 #{@pid}` - end + def process(action) + pids = find_processes - def to_s #:nodoc: - "[#{@pid}] #{@command}" + if pids.empty? + puts "Couldn't find any pid file in '#{@pid_path}' matching '#{@pattern}'" + else + pids.each do |pid| + puts "#{action.capitalize}ing #{pid}" + self.class.send(action, pid) + end + + delete_pid_files if terminating?(action) + end end + + private + def terminating?(action) + [ "kill", "graceful" ].include?(action) + end + + def find_processes + pid_files.collect { |pid_file| File.read(pid_file).to_i } + end + + def delete_pid_files + pid_files.each { |pid_file| File.delete(pid_file) } + end + + def pid_files + Dir.glob(@pid_path + "/" + @pattern) + end end + OPTIONS = { - :action => "restart", - :dispatcher => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi') + :action => "restart", + :pid_path => File.expand_path(RAILS_ROOT + '/tmp/pids'), + :pattern => "dispatch.*.pid" } ARGV.options do |opts| @@ -96,9 +96,13 @@ ARGV.options do |opts| opts.on <<-EOF Description: - The reaper is used to restart, reload, gracefully exit, and forcefully exit FCGI processes - running a Rails Dispatcher. This is commonly done when a new version of the application - is available, so the existing processes can be updated to use the latest code. + The reaper is used to restart, reload, gracefully exit, and forcefully exit processes + running a Rails Dispatcher (or any other process responding to the same signals). This + is commonly done when a new version of the application is available, so the existing + processes can be updated to use the latest code. + + It uses pid files to work on the processes and by default assume them to be located + in RAILS_ROOT/tmp/pids. The reaper actions are: @@ -110,15 +114,16 @@ ARGV.options do |opts| Restart is the most common and default action. Examples: - reaper # restarts the default dispatcher - reaper -a reload - reaper -a exit -d /my/special/dispatcher.fcgi + reaper # restarts the default dispatchers + reaper -a reload # reload the default dispatchers + reaper -a kill -r *.pid # kill all processes that keep pids in tmp/pids EOF opts.on(" Options:") opts.on("-a", "--action=name", "reload|graceful|kill (default: #{OPTIONS[:action]})", String) { |OPTIONS[:action]| } - opts.on("-d", "--dispatcher=path", "default: #{OPTIONS[:dispatcher]}", String) { |OPTIONS[:dispatcher]| } + opts.on("-p", "--pidpath=path", "default: #{OPTIONS[:pid_path]}", String) { |OPTIONS[:pid_path]| } + opts.on("-r", "--pattern=pattern", "default: #{OPTIONS[:pattern]}", String) { |OPTIONS[:pattern]| } opts.separator "" @@ -127,4 +132,4 @@ ARGV.options do |opts| opts.parse! end -ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher]) \ No newline at end of file +Killer.process(OPTIONS[:action], OPTIONS[:pid_path], OPTIONS[:pattern]) \ No newline at end of file diff --git a/railties/lib/commands/process/spawner.rb b/railties/lib/commands/process/spawner.rb index 84364589c8..9424dacc74 100644 --- a/railties/lib/commands/process/spawner.rb +++ b/railties/lib/commands/process/spawner.rb @@ -1,5 +1,6 @@ require 'optparse' require 'socket' +require 'fileutils' def daemonize #:nodoc: exit if fork # Parent exits, child continues. @@ -12,28 +13,52 @@ def daemonize #:nodoc: STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile. end -def spawn(port) - print "Checking if something is already running on port #{port}..." - begin - srv = TCPServer.new('0.0.0.0', port) - srv.close - srv = nil - print "NO\n " - print "Starting FCGI on port: #{port}\n " - system("#{OPTIONS[:spawner]} -f #{OPTIONS[:dispatcher]} -p #{port}") - rescue - print "YES\n" +class Spawner + def self.record_pid(name = "spawner", id = Process.pid) + FileUtils.mkdir_p(OPTIONS[:pids]) + File.open(File.expand_path(OPTIONS[:pids] + "/#{name}.pid"), "w+") { |f| f.write(id) } + end + + def self.spawn_all + OPTIONS[:instances].times do |i| + port = OPTIONS[:port] + i + print "Checking if something is already running on port #{port}..." + + begin + srv = TCPServer.new('0.0.0.0', port) + srv.close + srv = nil + + print "NO\n " + print "Starting FCGI on port: #{port}\n " + + FileUtils.mkdir_p(OPTIONS[:pids]) + spawn(port) + rescue + print "YES\n" + end + end end end - -def spawn_all - OPTIONS[:instances].times { |i| spawn(OPTIONS[:port] + i) } + +class FcgiSpawner < Spawner + def self.spawn(port) + system("#{OPTIONS[:spawner]} -f #{OPTIONS[:dispatcher]} -p #{port} -P #{OPTIONS[:pids]}/dispatch.#{port}.pid") + end end +# TODO: +# class MongrelSpawner < Spawner +# def self.spawn(port) +# end +# end + + OPTIONS = { :environment => "production", :spawner => '/usr/bin/env spawn-fcgi', :dispatcher => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi'), + :pids => File.expand_path(RAILS_ROOT + "/tmp/pids"), :port => 8000, :instances => 3, :repeat => nil @@ -84,11 +109,12 @@ ENV["RAILS_ENV"] = OPTIONS[:environment] if OPTIONS[:repeat] daemonize trap("TERM") { exit } + FcgiSpawner.record_pid loop do - spawn_all + FcgiSpawner.spawn_all sleep(OPTIONS[:repeat]) end else - spawn_all + FcgiSpawner.spawn_all end diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index f1f800cf74..115acb6d01 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -142,6 +142,7 @@ class AppGenerator < Rails::Generator::Base tmp/sessions tmp/sockets tmp/cache + tmp/pids ) MYSQL_SOCKET_LOCATIONS = [ diff --git a/railties/lib/tasks/tmp.rake b/railties/lib/tasks/tmp.rake index 6ba5b11450..b191039d63 100644 --- a/railties/lib/tasks/tmp.rake +++ b/railties/lib/tasks/tmp.rake @@ -4,7 +4,7 @@ namespace :tmp do desc "Creates tmp directories for sessions, cache, and sockets" task :create do - FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets )) + FileUtils.mkdir_p(%w( tmp/sessions tmp/cache tmp/sockets tmp/pids )) end namespace :sessions do @@ -27,4 +27,11 @@ namespace :tmp do FileUtils.rm(Dir['tmp/sockets/[^.]*']) end end + + namespace :pids do + desc "Clears all files in tmp/pids" + task :clear do + FileUtils.rm(Dir['tmp/pids/[^.]*']) + end + end end \ No newline at end of file -- cgit v1.2.3