Skip to content

Commit

Permalink
Add ability to restart by reexecing and pumactl to use it
Browse files Browse the repository at this point in the history
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
evanphx committed Dec 5, 2011
1 parent 8184b08 commit d8026e8
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 4 deletions.
1 change: 1 addition & 0 deletions bin/puma
Expand Up @@ -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
12 changes: 12 additions & 0 deletions 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
11 changes: 10 additions & 1 deletion 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)
Expand All @@ -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
Expand Down
65 changes: 63 additions & 2 deletions lib/puma/cli.rb
Expand Up @@ -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+
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -171,6 +224,7 @@ def run

load_rackup
write_pid
write_state

unless @options[:quiet]
@app = Rack::CommonLogger.new(@app, STDOUT)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions 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
2 changes: 1 addition & 1 deletion test/test_app_status.rb
Expand Up @@ -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
Expand Down

0 comments on commit d8026e8

Please sign in to comment.