Permalink
Browse files

Add ability to restart by reexecing and pumactl to use it

This allows all existing requests to finish, but does not keep the same
socket alive across the exec, so this is not a graceful as it could be.
  • Loading branch information...
1 parent 8184b08 commit d8026e87f433877d308ed537d4505eeddc0771a3 @evanphx evanphx committed Dec 5, 2011
Showing with 199 additions and 4 deletions.
  1. +1 −0 bin/puma
  2. +12 −0 bin/pumactl
  3. +10 −1 lib/puma/app/status.rb
  4. +63 −2 lib/puma/cli.rb
  5. +112 −0 lib/puma/control_cli.rb
  6. +1 −1 test/test_app_status.rb
View
1 bin/puma
@@ -10,6 +10,7 @@ cli = Puma::CLI.new ARGV
begin
cli.run
rescue => e
+ raise e if $DEBUG
STDERR.puts e.message
exit 1
end
View
12 bin/pumactl
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+
+require 'puma/control_cli'
+
+cli = Puma::ControlCLI.new ARGV
+
+begin
+ cli.run
+rescue => e
+ STDERR.puts e.message
+ exit 1
+end
View
11 lib/puma/app/status.rb
@@ -1,8 +1,9 @@
module Puma
module App
class Status
- def initialize(server)
+ def initialize(server, cli)
@server = server
+ @cli = cli
end
def call(env)
@@ -15,6 +16,14 @@ def call(env)
@server.halt
return [200, {}, ['{ "status": "ok" }']]
+ when "/restart"
+ if @cli and @cli.restart_on_stop!
+ @server.stop
+ return [200, {}, ['{ "status": "ok" }']]
+ else
+ return [200, {}, ['{ "status": "not configured" }']]
+ end
+
when "/stats"
b = @server.backlog
r = @server.running
View
65 lib/puma/cli.rb
@@ -31,7 +31,58 @@ def initialize(argv, stdout=STDOUT, stderr=STDERR)
@server = nil
@status = nil
+ @restart = false
+ @temp_status_path = nil
+
setup_options
+
+ generate_restart_data
+ end
+
+ def restart_on_stop!
+ if @restart_argv
+ @restart = true
+ return true
+ else
+ return false
+ end
+ end
+
+ def generate_restart_data
+ # Use the same trick as unicorn, namely favor PWD because
+ # it will contain an unresolved symlink, useful for when
+ # the pwd is /data/releases/current.
+ if dir = ENV['PWD']
+ s_env = File.stat(dir)
+ s_pwd = File.stat(Dir.pwd)
+
+ if s_env.ino == s_pwd.ino and s_env.dev == s_pwd.dev
+ @restart_dir = dir
+ end
+ end
+
+ @restart_dir ||= Dir.pwd
+
+ if defined? Rubinius::OS_ARGV
+ @restart_argv = Rubinius::OS_ARGV
+ else
+ require 'rubygems'
+
+ # if $0 is a file in the current directory, then restart
+ # it the same, otherwise add -S on there because it was
+ # picked up in PATH.
+ #
+ if File.exists?($0)
+ @restart_argv = [Gem.ruby, $0] + ARGV
+ else
+ @restart_argv = [Gem.ruby, "-S", $0] + ARGV
+ end
+ end
+ end
+
+ def restart!
+ Dir.chdir @restart_dir
+ Kernel.exec(*@restart_argv)
end
# Write +str+ to +@stdout+
@@ -87,7 +138,7 @@ def setup_options
end
o.on "--status [URL]", "The bind url to use for the status server" do |arg|
- if arg
+ if arg and arg != "@"
@options[:status_address] = arg
elsif IS_JRUBY
raise NotImplementedError, "No default url available on JRuby"
@@ -97,6 +148,8 @@ def setup_options
t = (Time.now.to_f * 1000).to_i
path = "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
+ @temp_status_path = path
+
@options[:status_address] = "unix://#{path}"
end
end
@@ -171,6 +224,7 @@ def run
load_rackup
write_pid
+ write_state
unless @options[:quiet]
@app = Rack::CommonLogger.new(@app, STDOUT)
@@ -219,7 +273,7 @@ def run
uri = URI.parse str
- app = Puma::App::Status.new server
+ app = Puma::App::Status.new server, self
status = Puma::Server.new app, @events
status.min_threads = 0
status.max_threads = 1
@@ -250,6 +304,13 @@ def run
server.stop(true)
log " - Goodbye!"
end
+
+ File.unlink @temp_status_path if @temp_status_path
+
+ if @restart
+ log "* Restarting..."
+ restart!
+ end
end
def stop
View
112 lib/puma/control_cli.rb
@@ -0,0 +1,112 @@
+require 'optparse'
+
+require 'puma/const'
+require 'yaml'
+require 'uri'
+
+require 'socket'
+
+module Puma
+ class ControlCLI
+
+ def initialize(argv)
+ @argv = argv
+ end
+
+ def setup_options
+ @parser = OptionParser.new do |o|
+ o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
+ @path = arg
+ end
+ end
+ end
+
+ def connect
+ if str = @state['status_address']
+ uri = URI.parse str
+ case uri.scheme
+ when "tcp"
+ return TCPSocket.new uri.host, uri.port
+ when "unix"
+ path = "#{uri.host}#{uri.path}"
+ return UNIXSocket.new path
+ else
+ raise "Invalid URI: #{str}"
+ end
+ end
+
+ raise "No status address configured"
+ end
+
+ def run
+ setup_options
+
+ @parser.parse! @argv
+
+ @state = YAML.load_file(@path)
+
+ cmd = @argv.shift
+
+ meth = "command_#{cmd}"
+
+ if respond_to?(meth)
+ __send__(meth)
+ else
+ raise "Unknown command: #{cmd}"
+ end
+ end
+
+ def command_pid
+ puts "#{@state['pid']}"
+ end
+
+ def command_stop
+ sock = connect
+ sock << "GET /stop HTTP/1.0\r\n\r\n"
+ rep = sock.read
+
+ body = rep.split("\r\n").last
+ if body != '{ "status": "ok" }'
+ raise "Invalid response: '#{body}'"
+ else
+ puts "Requested stop from server"
+ end
+ end
+
+ def command_halt
+ sock = connect
+ s << "GET /halt HTTP/1.0\r\n\r\n"
+ rep = s.read
+
+ body = rep.split("\r\n").last
+ if body != '{ "status": "ok" }'
+ raise "Invalid response: '#{body}'"
+ else
+ puts "Requested halt from server"
+ end
+ end
+
+ def command_restart
+ sock = connect
+ sock << "GET /restart HTTP/1.0\r\n\r\n"
+ rep = sock.read
+
+ body = rep.split("\r\n").last
+ if body != '{ "status": "ok" }'
+ raise "Invalid response: '#{body}'"
+ else
+ puts "Requested restart from server"
+ end
+ end
+
+ def command_stats
+ sock = connect
+ s << "GET /stats HTTP/1.0\r\n\r\n"
+ rep = s.read
+
+ body = rep.split("\r\n").last
+
+ puts body
+ end
+ end
+end
View
2 test/test_app_status.rb
@@ -23,7 +23,7 @@ def halt
def setup
@server = FakeServer.new
- @app = Puma::App::Status.new(@server)
+ @app = Puma::App::Status.new(@server, @server)
end
def test_unsupported

0 comments on commit d8026e8

Please sign in to comment.