1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Add pumactl command to print thread backtraces (#2054)

* Add pumactl command to print thread backtraces

Completes 1 of 2 items from #1964

This commit adds an endpoint to the status app to print thread
backtraces, and control cli command to call that endpoint.

I tried this locally by starting a server with:

```sh
bundle exec bin/puma test/rackup/hello.ru \
  --control-url="unix://test.sock" \
  --control-token="token"
```

and then printing the backtraces with:

```sh
bundle exec bin/pumactl thread-backtraces \
  --control-url="unix://test.sock" \
  --control-token="token"
```

* Log threads as JSON in control app

With this commit the status app sends the thread backtraces as an array
of objects with `name` and `backtrace` keys, rather than as a string
matching the SIGINFO output.

While working on this I noticed that we logged the thread TID twice.
This commit simplifies that so we only log the thread TID once, with
both the label (I don't know when the label would get set) and name if
they are available.
This commit is contained in:
Daniel Colson 2019-11-11 00:08:41 -05:00 committed by Nate Berkopec
parent 5fef2b715f
commit 39d16fadaf
8 changed files with 56 additions and 25 deletions

View file

@ -1,7 +1,7 @@
## Master ## Master
* Features * Features
* Your feature goes here (#Github Number) * Add pumactl `thread-backtraces` command to print thread backtraces (#2053)
* Bugfixes * Bugfixes
* Your bugfix goes here (#Github Number) * Your bugfix goes here (#Github Number)

View file

@ -55,6 +55,14 @@ module Puma
when /\/stats$/ when /\/stats$/
rack_response(200, @cli.stats) rack_response(200, @cli.stats)
when /\/thread-backtraces$/
backtraces = []
@cli.thread_status do |name, backtrace|
backtraces << { name: name, backtrace: backtrace }
end
rack_response(200, backtraces.to_json)
else else
rack_response 404, "Unsupported action", 'text/plain' rack_response 404, "Unsupported action", 'text/plain'
end end

View file

@ -11,7 +11,8 @@ require 'socket'
module Puma module Puma
class ControlCLI class ControlCLI
COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats} COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces}
PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}
def initialize(argv, stdout=STDOUT, stderr=STDERR) def initialize(argv, stdout=STDOUT, stderr=STDERR)
@state = nil @state = nil
@ -187,7 +188,7 @@ module Puma
end end
message "Command #{@command} sent success" message "Command #{@command} sent success"
message response.last if @command == "stats" || @command == "gc-stats" message response.last if PRINTABLE_COMMANDS.include?(@command)
end end
ensure ensure
server.close if server && !server.closed? server.close if server && !server.closed?

View file

@ -205,6 +205,17 @@ module Puma
@binder.close_listeners @binder.close_listeners
end end
def thread_status
Thread.list.each do |thread|
name = "Thread: TID-#{thread.object_id.to_s(36)}"
name += " #{thread['label']}" if thread['label']
name += " #{thread.name}" if thread.respond_to?(:name) && thread.name
backtrace = thread.backtrace || ["<no backtrace available>"]
yield name, backtrace
end
end
private private
# If configured, write the pid of the current process out # If configured, write the pid of the current process out
@ -323,21 +334,6 @@ module Puma
log "- Goodbye!" log "- Goodbye!"
end end
def log_thread_status
Thread.list.each do |thread|
log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
logstr += " #{thread.name}" if thread.respond_to?(:name)
log logstr
if thread.backtrace
log thread.backtrace.join("\n")
else
log "<no backtrace available>"
end
end
end
def set_process_title def set_process_title
Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
end end
@ -457,7 +453,10 @@ module Puma
begin begin
Signal.trap "SIGINFO" do Signal.trap "SIGINFO" do
log_thread_status thread_status do |name, backtrace|
@events.log name
@events.log backtrace
end
end end
rescue Exception rescue Exception
# Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying

View file

@ -203,6 +203,30 @@ class TestCLI < Minitest::Test
t.join if UNIX_SKT_EXIST t.join if UNIX_SKT_EXIST
end end
def test_control_thread_backtraces
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "unix://#{@tmp_path}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
"--control-url", url,
"--control-token", "",
"test/rackup/lobster.ru"], @events
t = Thread.new { cli.run }
wait_booted
s = UNIXSocket.new @tmp_path
s << "GET /thread-backtraces HTTP/1.0\r\n\r\n"
body = s.read
s.close
assert_match %r{Thread: TID-}, body.split("\r\n").last
ensure
cli.launcher.stop if cli
t.join if UNIX_SKT_EXIST
end
def control_gc_stats(uri, cntl) def control_gc_stats(uri, cntl)
cli = Puma::CLI.new ["-b", uri, cli = Puma::CLI.new ["-b", uri,
"--control-url", cntl, "--control-url", cntl,

View file

@ -44,7 +44,7 @@ class TestIntegrationCluster < TestIntegration
Process.kill :INT , @pid Process.kill :INT , @pid
t.join t.join
assert_match "Thread TID", output.join assert_match "Thread: TID", output.join
end end
def test_usr2_restart def test_usr2_restart

View file

@ -99,6 +99,6 @@ class TestIntegrationSingle < TestIntegration
Process.kill :INT , @pid Process.kill :INT , @pid
t.join t.join
assert_match "Thread TID", output.join assert_match "Thread: TID", output.join
end end
end end

View file

@ -57,10 +57,9 @@ class TestLauncher < Minitest::Test
end end
def test_prints_thread_traces def test_prints_thread_traces
launcher.send(:log_thread_status) launcher.thread_status do |name, _backtrace|
events.stdout.rewind assert_match "Thread: TID", name
end
assert_match "Thread TID", events.stdout.read
end end
def test_pid_file def test_pid_file