diff --git a/History.md b/History.md index 6cfb0659..0e11b546 100644 --- a/History.md +++ b/History.md @@ -1,7 +1,7 @@ ## Master * Features - * Your feature goes here (#Github Number) + * Add pumactl `thread-backtraces` command to print thread backtraces (#2053) * Bugfixes * Your bugfix goes here (#Github Number) diff --git a/lib/puma/app/status.rb b/lib/puma/app/status.rb index 2e4b4e4c..19f6d877 100644 --- a/lib/puma/app/status.rb +++ b/lib/puma/app/status.rb @@ -55,6 +55,14 @@ module Puma when /\/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 rack_response 404, "Unsupported action", 'text/plain' end diff --git a/lib/puma/control_cli.rb b/lib/puma/control_cli.rb index 6d4118f0..53cbd978 100644 --- a/lib/puma/control_cli.rb +++ b/lib/puma/control_cli.rb @@ -11,7 +11,8 @@ require 'socket' module Puma 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) @state = nil @@ -187,7 +188,7 @@ module Puma end message "Command #{@command} sent success" - message response.last if @command == "stats" || @command == "gc-stats" + message response.last if PRINTABLE_COMMANDS.include?(@command) end ensure server.close if server && !server.closed? diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb index 2a0cd9be..81ba4570 100644 --- a/lib/puma/launcher.rb +++ b/lib/puma/launcher.rb @@ -205,6 +205,17 @@ module Puma @binder.close_listeners 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 || [""] + + yield name, backtrace + end + end + private # If configured, write the pid of the current process out @@ -323,21 +334,6 @@ module Puma log "- Goodbye!" 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 "" - end - end - end - def set_process_title Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title end @@ -457,7 +453,10 @@ module Puma begin Signal.trap "SIGINFO" do - log_thread_status + thread_status do |name, backtrace| + @events.log name + @events.log backtrace + end end rescue Exception # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying diff --git a/test/test_cli.rb b/test/test_cli.rb index d4923624..e2e55f7b 100644 --- a/test/test_cli.rb +++ b/test/test_cli.rb @@ -203,6 +203,30 @@ class TestCLI < Minitest::Test t.join if UNIX_SKT_EXIST 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) cli = Puma::CLI.new ["-b", uri, "--control-url", cntl, diff --git a/test/test_integration_cluster.rb b/test/test_integration_cluster.rb index 9679f274..a7f58f84 100644 --- a/test/test_integration_cluster.rb +++ b/test/test_integration_cluster.rb @@ -44,7 +44,7 @@ class TestIntegrationCluster < TestIntegration Process.kill :INT , @pid t.join - assert_match "Thread TID", output.join + assert_match "Thread: TID", output.join end def test_usr2_restart diff --git a/test/test_integration_single.rb b/test/test_integration_single.rb index 8325231f..7f44fde5 100644 --- a/test/test_integration_single.rb +++ b/test/test_integration_single.rb @@ -99,6 +99,6 @@ class TestIntegrationSingle < TestIntegration Process.kill :INT , @pid t.join - assert_match "Thread TID", output.join + assert_match "Thread: TID", output.join end end diff --git a/test/test_launcher.rb b/test/test_launcher.rb index 9de31b91..be3a1038 100644 --- a/test/test_launcher.rb +++ b/test/test_launcher.rb @@ -57,10 +57,9 @@ class TestLauncher < Minitest::Test end def test_prints_thread_traces - launcher.send(:log_thread_status) - events.stdout.rewind - - assert_match "Thread TID", events.stdout.read + launcher.thread_status do |name, _backtrace| + assert_match "Thread: TID", name + end end def test_pid_file