Skip to content

Commit

Permalink
Puma::ControlCLI - allow refork command to be sent as a request (#2868)
Browse files Browse the repository at this point in the history
* Puma::ControlCLI - allow refork command to be sent as a request

* Puma::ControlCLI - add signal only commands

* Puma::ControlCLI - check whether signal is available
  • Loading branch information
MSP-Greg authored and nateberkopec committed Aug 22, 2022
1 parent 470df09 commit 6454710
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 14 deletions.
3 changes: 3 additions & 0 deletions lib/puma/app/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def call(env)
when 'phased-restart'
@launcher.phased_restart ? 200 : 404

when 'refork'
@launcher.refork ? 200 : 404

when 'reload-worker-directory'
@launcher.send(:reload_worker_directory) ? 200 : 404

Expand Down
30 changes: 18 additions & 12 deletions lib/puma/control_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,30 @@ class ControlCLI
CMD_PATH_SIG_MAP = {
'gc' => nil,
'gc-stats' => nil,
'halt' => 'SIGQUIT',
'phased-restart' => 'SIGUSR1',
'refork' => 'SIGURG',
'halt' => 'SIGQUIT',
'info' => 'SIGINFO',
'phased-restart' => 'SIGUSR1',
'refork' => 'SIGURG',
'reload-worker-directory' => nil,
'restart' => 'SIGUSR2',
'reopen-log' => 'SIGHUP',
'restart' => 'SIGUSR2',
'start' => nil,
'stats' => nil,
'status' => '',
'stop' => 'SIGTERM',
'thread-backtraces' => nil
'stop' => 'SIGTERM',
'thread-backtraces' => nil,
'worker-count-down' => 'SIGTTOU',
'worker-count-up' => 'SIGTTIN'
}.freeze

# @deprecated 6.0.0
COMMANDS = CMD_PATH_SIG_MAP.keys.freeze

# commands that cannot be used in a request
NO_REQ_COMMANDS = %w{refork}.freeze
NO_REQ_COMMANDS = %w[info reopen-log worker-count-down worker-count-up].freeze

# @version 5.0.0
PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
PRINTABLE_COMMANDS = %w[gc-stats stats thread-backtraces].freeze

def initialize(argv, stdout=STDOUT, stderr=STDERR)
@state = nil
Expand Down Expand Up @@ -185,8 +189,6 @@ def send_request

if @command == 'status'
message 'Puma is started'
elsif NO_REQ_COMMANDS.include? @command
raise "Invalid request command: #{@command}"
else
url = "/#{@command}"

Expand Down Expand Up @@ -242,7 +244,11 @@ def send_signal
@stdout.flush unless @stdout.sync
return
elsif sig.start_with? 'SIG'
Process.kill sig, @pid
if Signal.list.key? sig.sub(/\ASIG/, '')
Process.kill sig, @pid
else
raise "Signal '#{sig}' not available'"
end
elsif @command == 'status'
begin
Process.kill 0, @pid
Expand All @@ -268,7 +274,7 @@ def run
return start if @command == 'start'
prepare_configuration

if Puma.windows? || @control_url
if Puma.windows? || @control_url && !NO_REQ_COMMANDS.include?(@command)
send_request
else
send_signal
Expand Down
11 changes: 11 additions & 0 deletions lib/puma/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ def phased_restart
true
end

# Begin a refork if supported
def refork
if clustered? && @runner.respond_to?(:fork_worker!) && @options[:fork_worker]
@runner.fork_worker!
true
else
log "* refork called but not available."
false
end
end

# Run the server. This blocks until the server is stopped
def run
previous_env =
Expand Down
2 changes: 1 addition & 1 deletion test/test_integration_cluster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def test_worker_index_is_with_in_options_limit
end

# use three workers to keep accepting clients
def test_refork
def test_fork_worker_on_refork
refork = Tempfile.new 'refork'
wrkrs = 3
cli_server "-w #{wrkrs} test/rackup/hello_with_delay.ru", config: <<RUBY
Expand Down
37 changes: 37 additions & 0 deletions test/test_integration_pumactl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,43 @@ def test_phased_restart_cluster
@server = nil
end

def test_refork_cluster
skip_unless :fork
wrkrs = 3
cli_server "-q -w #{wrkrs} test/rackup/sleep.ru --control-url unix://#{@control_path} --control-token #{TOKEN} -S #{@state_path}",
config: 'fork_worker 50',
unix: true

start = Time.now

s = UNIXSocket.new @bind_path
@ios_to_close << s
s << "GET /sleep1 HTTP/1.0\r\n\r\n"

# Get the PIDs of the phase 0 workers.
phase0_worker_pids = get_worker_pids 0, wrkrs
assert File.exist? @bind_path

cli_pumactl "refork", unix: true

# Get the PIDs of the phase 1 workers.
phase1_worker_pids = get_worker_pids 1, wrkrs - 1

msg = "phase 0 pids #{phase0_worker_pids.inspect} phase 1 pids #{phase1_worker_pids.inspect}"

assert_equal wrkrs , phase0_worker_pids.length, msg
assert_equal wrkrs - 1, phase1_worker_pids.length, msg
assert_empty phase0_worker_pids & phase1_worker_pids, "#{msg}\nBoth workers should be replaced with new"
assert File.exist?(@bind_path), "Bind path must exist after phased refork"

cli_pumactl "stop", unix: true

_, status = Process.wait2(@pid)
assert_equal 0, status
assert_operator Time.now - start, :<, (DARWIN ? 8 : 6)
@server = nil
end

def test_prune_bundler_with_multiple_workers
skip_unless :fork

Expand Down
38 changes: 37 additions & 1 deletion test/test_pumactl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def wait_booted

def teardown
@wait.close
@ready.close
@ready.close unless @ready.closed?
end

def with_config_file(path_to_config, port)
Expand Down Expand Up @@ -187,6 +187,42 @@ def test_control_url_and_status
assert_kind_of Thread, t.join, "server didn't stop"
end

# This checks that a 'signal only' command is sent
# they are defined by the `Puma::ControlCLI::NO_REQ_COMMANDS` array
# test is skipped unless NO_REQ_COMMANDS is defined
def test_control_url_with_signal_only_cmd
skip_if :windows
skip unless defined? Puma::ControlCLI::NO_REQ_COMMANDS
host = "127.0.0.1"
port = UniquePort.call
url = "tcp://#{host}:#{port}/"

opts = [
"--control-url", url,
"--control-token", "ctrl",
"--config-file", "test/config/app.rb",
"--pid", "1234"
]
cmd = Puma::ControlCLI::NO_REQ_COMMANDS.first
log = ''.dup
control_cli = Puma::ControlCLI.new (opts + [cmd]), @ready, @ready

def control_cli.send_signal
message "send_signal #{@command}\n"
end
def control_cli.send_request
message "send_request #{@command}\n"
end

control_cli.run
@ready.close

log = @wait.read

assert_includes log, "send_signal #{cmd}"
refute_includes log, 'send_request'
end

def test_control_ssl
skip_unless :ssl

Expand Down

0 comments on commit 6454710

Please sign in to comment.