Skip to content

Commit

Permalink
Promote worker process to optimize copy-on-write performance
Browse files Browse the repository at this point in the history
Add a hook (SIGURG) to promote an existing worker process to become
a new master, and spawn a new set of forked worker processes.
Because the promoted worker has been serving live requests, forking from
this process instead of the initial preloaded app can potentially
improve overall copy-on-write performance and reduce memory usage.
  • Loading branch information
wjordan committed Feb 11, 2020
1 parent 493b3f5 commit 8872e88
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 7 deletions.
1 change: 1 addition & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Configuration: `environment` is read from `RAILS_ENV`, if `RACK_ENV` can't be found (#2022)
* `Puma.stats` now returns a Hash instead of a JSON string (#2086)
* `GC.compact` is called before fork if available (#2093)
* Add command to `promote` worker process to optimize copy-on-write performance (#2099)

* Bugfixes
* Your bugfix goes here (#Github Number)
Expand Down
5 changes: 5 additions & 0 deletions lib/puma/app/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ def call(env)
end

rack_response(200, backtraces.to_json)

when /\/promote$/
Process.kill "SIGURG", $$
rack_response(200, OK_STATUS)

else
rack_response 404, "Unsupported action", 'text/plain'
end
Expand Down
33 changes: 30 additions & 3 deletions lib/puma/cluster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ def hup
Process.kill "HUP", @pid
rescue Errno::ESRCH
end

def promote
Process.kill "URG", @pid
Process.detach @pid
rescue Errno::ESRCH
end
end

def spawn_workers
Expand Down Expand Up @@ -249,7 +255,7 @@ def worker(index, master)
@master_read.close
@suicide_pipe.close

Thread.new do
parent_check = Thread.new do
Puma.set_thread_name "worker check pipe"
IO.select [@check_pipe]
log "! Detected parent died, dying"
Expand Down Expand Up @@ -277,6 +283,13 @@ def worker(index, master)
server.stop
end

# Promote worker
Signal.trap "SIGURG" do
@promoted = true
parent_check.kill
server.begin_restart(true)
end

begin
@worker_write << "b#{Process.pid}\n"
rescue SystemCallError, IOError
Expand Down Expand Up @@ -313,6 +326,7 @@ def worker(index, master)
ensure
@worker_write << "t#{Process.pid}\n" rescue nil
@worker_write.close
@launcher.run if @promoted
end

def restart
Expand Down Expand Up @@ -431,7 +445,7 @@ def run

if preload?
log "* Preloading application"
load_and_bind
load_and_bind unless @promoted

after = Thread.list

Expand All @@ -454,7 +468,7 @@ def run
exit 1
end

@launcher.binder.parse @options[:binds], self
@launcher.binder.parse @options[:binds], self unless @promoted
end

read, @wakeup = Puma::Util.pipe
Expand Down Expand Up @@ -488,12 +502,18 @@ def run
@launcher.config.run_hooks :before_fork, nil
GC.compact if GC.respond_to?(:compact)

@promoted = false
spawn_workers

Signal.trap "SIGINT" do
stop
end

Signal.trap "SIGURG" do
@promoted = @workers.shift
stop
end

@launcher.events.fire_on_booted!

begin
Expand Down Expand Up @@ -545,6 +565,7 @@ def run
end
end

promote if @promoted
stop_workers unless @status == :halt
ensure
@check_pipe.close
Expand All @@ -556,6 +577,12 @@ def run

private

def promote
@control.stop(true) if @control
close_control_listeners
@launcher.promote
@promoted.promote
end
# loops thru @workers, removing workers that exited, and calling
# `#term` if needed
def wait_workers
Expand Down
5 changes: 4 additions & 1 deletion lib/puma/control_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
module Puma
class ControlCLI

COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces}
COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces promote}
PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}

def initialize(argv, stdout=STDOUT, stderr=STDERR)
Expand Down Expand Up @@ -211,6 +211,9 @@ def send_signal
when "stop"
Process.kill "SIGTERM", @pid

when "promote"
Process.kill "SIGURG", @pid

when "stats"
puts "Stats not available via pid only"
return
Expand Down
6 changes: 5 additions & 1 deletion lib/puma/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ def restart
@runner.restart
end

def promote
@status = :promote
end

# Begin a phased restart if supported
def phased_restart
unless @runner.respond_to?(:phased_restart) and @runner.phased_restart
Expand Down Expand Up @@ -184,7 +188,7 @@ def run
when :exit
# nothing
end
close_binder_listeners unless @status == :restart
close_binder_listeners unless @status == :restart || @status == :promote
end

# Return which tcp port the launcher is using, if it's using TCP
Expand Down
3 changes: 2 additions & 1 deletion lib/puma/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1006,8 +1006,9 @@ def halt(sync=false)
@thread.join if @thread && sync
end

def begin_restart
def begin_restart(sync=false)
notify_safely(RESTART_COMMAND)
@thread.join if @thread && sync
end

def fast_write(io, str)
Expand Down
3 changes: 2 additions & 1 deletion test/helpers/integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ def teardown
io = nil
end
refute File.exist?(@bind_path), "Bind path must be removed after stop"
File.unlink(@bind_path) rescue nil

# wait until the end for OS buffering?
if defined?(@server) && @server
@server.close unless @server.closed?
@server = nil
end
ensure
File.unlink(@bind_path) rescue nil
end

private
Expand Down
18 changes: 18 additions & 0 deletions test/test_integration_pumactl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ def test_kill_unknown
assert_equal(1, e.status)
end

def test_promote
skip NO_FORK_MSG unless HAS_FORK
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST

cli_server "-w #{WORKERS} test/rackup/sleep.ru --control-url unix://#{@control_path} --control-token #{TOKEN} -S #{@state_path}", unix: true
cli_pumactl "promote", unix: true

_, status = Process.wait2(@pid)
assert_equal 0, status

new_pid = File.read(@state_path).match(/pid: (\d+)/m)[1].to_i
refute_equal @pid, new_pid

cli_pumactl "stop", unix: true
sleep 0.1 while File.exist?(@bind_path)
@server = nil
end

private

def cli_pumactl(argv, unix: false)
Expand Down

0 comments on commit 8872e88

Please sign in to comment.