Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socket activation cleanup, fixes, and a doc #942

Merged
merged 7 commits into from Apr 1, 2016
Merged
78 changes: 78 additions & 0 deletions docs/systemd.md
@@ -0,0 +1,78 @@
# systemd

[systemd](https://www.freedesktop.org/wiki/Software/systemd/) is a
commonly available init system (PID 1) on many Linux distributions. It
offers process monitoring (including automatic restarts) and other
useful features for running Puma in production. Below is a sample
puma.service configuration file for systemd:

~~~~
[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
# Foreground process (do not use --daemon in ExecStart or config.rb)
Type=simple

# Preferably configure a non-privileged user
# User=

# Specify the path to your puma application root
# WorkingDirectory=

# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1

# The command to start Puma
# Here we are using a binstub generated via:
# `bundle binstubs puma --path ./sbin`
# in the WorkingDirectory (replace <WD> below)
# You can alternatively use `bundle exec --keep-file-descriptors puma`
# ExecStart=<WD>/sbin/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem

# Alternatively with a config file (in WorkingDirectory) and
# comparable `bind` directives
# ExecStart=<WorkingDirectory>/sbin/puma -C config.rb

Restart=always

[Install]
WantedBy=multi-user.target
~~~~

See [systemd.exec](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
for additional details.

## Socket Activation

systemd and puma also support socket activation, where systemd opens
the listening socket(s) in advance and provides them to the puma master
process on startup. Among other advantages, this keeps listening
sockets open across puma restarts and achieves graceful restarts. To
use socket activation, configure one or more `ListenStream`
sockets in a companion `*.socket` systemd config file. Here is a sample
puma.socket, matching the ports used in the above puma.service:

~~~~
[Unit]
Description=Puma HTTP Server Accept Sockets

[Socket]
ListenStream=0.0.0.0:9292
ListenStream=0.0.0.0:9293

# AF_UNIX domain socket
# SocketUser, SocketGroup, etc. may be needed for Unix domain sockets
# ListenStream=/run/puma.sock

# Socket options matching what Puma wants
NoDelay=true
ReusePort=true

[Install]
WantedBy=sockets.target
~~~~

See [systemd.socket](https://www.freedesktop.org/software/systemd/man/systemd.socket.html)
for additional details.
51 changes: 35 additions & 16 deletions lib/puma/binder.rb
Expand Up @@ -11,6 +11,7 @@ def initialize(events)
@events = events
@listeners = []
@inherited_fds = {}
@activated_sockets = {}
@unix_paths = []

@proto_env = {
Expand Down Expand Up @@ -55,24 +56,23 @@ def import_from_env
fd, url = v.split(":", 2)
@inherited_fds[url] = fd.to_i
remove << k
end
if k =~ /LISTEN_FDS/ && ENV['LISTEN_PID'].to_i == $$
elsif k == 'LISTEN_FDS' && ENV['LISTEN_PID'].to_i == $$
v.to_i.times do |num|
fd = num + 3
sock = TCPServer.for_fd(fd)
begin
url = "unix://" + Socket.unpack_sockaddr_un(sock.getsockname)
key = [ :unix, Socket.unpack_sockaddr_un(sock.getsockname) ]
rescue ArgumentError
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
if addr =~ /\:/
addr = "[#{addr}]"
end
url = "tcp://#{addr}:#{port}"
key = [ :tcp, addr, port ]
end
@inherited_fds[url] = sock
@activated_sockets[key] = sock
@events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
end
ENV.delete k
ENV.delete 'LISTEN_PID'
remove << k << 'LISTEN_PID'
end
end

Expand All @@ -89,6 +89,9 @@ def parse(binds, logger)
if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
io = inherit_tcp_listener uri.host, uri.port, fd
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
logger.log "* Activated #{str}"
io = inherit_tcp_listener uri.host, uri.port, sock
else
params = Util.parse_query uri.query

Expand All @@ -106,6 +109,9 @@ def parse(binds, logger)
if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
io = inherit_unix_listener path, fd
elsif sock = @activated_sockets.delete([ :unix, path ])
logger.log "* Activated #{str}"
io = inherit_unix_listener path, sock
else
logger.log "* Listening on #{str}"

Expand Down Expand Up @@ -191,7 +197,10 @@ def parse(binds, logger)

if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
io = inherited_ssl_listener fd, ctx
io = inherit_ssl_listener fd, ctx
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
logger.log "* Activated #{str}"
io = inherit_ssl_listener sock, ctx
else
logger.log "* Listening on #{str}"
io = add_ssl_listener uri.host, uri.port, ctx
Expand All @@ -209,12 +218,7 @@ def parse(binds, logger)
logger.log "* Closing unused inherited connection: #{str}"

begin
if fd.kind_of? TCPServer
fd.close
else
IO.for_fd(fd).close
end

IO.for_fd(fd).close
rescue SystemCallError
end

Expand All @@ -226,6 +230,17 @@ def parse(binds, logger)
end
end

# Also close any unsued activated sockets
@activated_sockets.each do |key, sock|
logger.log "* Closing unused activated socket: #{key.join ':'}"
begin
sock.close
rescue SystemCallError
end
# We have to unlink a unix socket path that's not being used
File.unlink key[1] if key[0] == :unix
end

end

# Tell the server to listen on host +host+, port +port+.
Expand Down Expand Up @@ -285,11 +300,15 @@ def add_ssl_listener(host, port, ctx,
s
end

def inherited_ssl_listener(fd, ctx)
def inherit_ssl_listener(fd, ctx)
require 'puma/minissl'
MiniSSL.check

s = TCPServer.for_fd(fd)
if fd.kind_of? TCPServer
s = fd
else
s = TCPServer.for_fd(fd)
end
ssl = MiniSSL::Server.new(s, ctx)

env = @proto_env.dup
Expand Down