diff --git a/History.md b/History.md index d019526e39..47be42fb19 100644 --- a/History.md +++ b/History.md @@ -7,6 +7,7 @@ * `GC.compact` is called before fork if available (#2093) * Add `requests_count` to workers stats. (#2106) * Increases maximum URI path length from 2048 to 8196 bytes (#2167) + * Sending SIGWINCH to any Puma worker now prints currently active threads and their backtraces (#2195) * Deprecations, Removals and Breaking API Changes * `Puma.stats` now returns a Hash instead of a JSON string (#2086) diff --git a/docs/signals.md b/docs/signals.md index 3625c72bda..83c441d04b 100644 --- a/docs/signals.md +++ b/docs/signals.md @@ -40,7 +40,7 @@ Puma cluster responds to these signals: - `USR1` restart workers in phases, a rolling restart. This will not reload configuration file. - `HUP` reopen log files defined in stdout_redirect configuration parameter. If there is no stdout_redirect option provided it will behave like `INT` - `INT` equivalent of sending Ctrl-C to cluster. Will attempt to finish then exit. -- `CHLD` +- `WINCH` (`INFO` on BSD-based systems) prints a thread backtrace. This is useful for debugging infinite loops or slow performance. ## Callbacks order in case of different signals diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb index 241546b36d..9eb3029b69 100644 --- a/lib/puma/launcher.rb +++ b/lib/puma/launcher.rb @@ -452,13 +452,18 @@ def setup_signals log "*** SIGHUP not implemented, signal based logs reopening unavailable!" end + begin + Signal.trap "SIGWINCH" do + log_backtrace + end + rescue Exception + log "*** SIGWINCH not implemented, signal based thread backtraces unavailable!" + end + begin unless Puma.jruby? # INFO in use by JVM already Signal.trap "SIGINFO" do - thread_status do |name, backtrace| - @events.log name - @events.log backtrace.map { |bt| " #{bt}" } - end + log_backtrace end end rescue Exception @@ -467,6 +472,13 @@ def setup_signals end end + def log_backtrace + thread_status do |name, backtrace| + @events.log name + @events.log backtrace.map { |bt| " #{bt}" } + end + end + def require_rubygems_min_version!(min_version, feature) return if min_version <= Gem::Version.new(Gem::VERSION) diff --git a/test/test_integration_cluster.rb b/test/test_integration_cluster.rb index a7f58f84ec..2ca9201ffe 100644 --- a/test/test_integration_cluster.rb +++ b/test/test_integration_cluster.rb @@ -33,18 +33,16 @@ def test_pre_existing_unix end end + def test_sigwinch_thread_print + skip_unless_signal_exist? :WINCH + + signal_thread_backtrace :WINCH + end + def test_siginfo_thread_print skip_unless_signal_exist? :INFO - cli_server "-w #{WORKERS} -q test/rackup/hello.ru" - worker_pids = get_worker_pids - output = [] - t = Thread.new { output << @server.readlines } - Process.kill :INFO, worker_pids.first - Process.kill :INT , @pid - t.join - - assert_match "Thread: TID", output.join + signal_thread_backtrace :INFO end def test_usr2_restart @@ -135,6 +133,18 @@ def test_stuck_phased_restart private + def signal_thread_backtrace(signal) + cli_server "-w #{WORKERS} -q test/rackup/hello.ru" + worker_pids = get_worker_pids + output = [] + t = Thread.new { output << @server.readlines } + Process.kill signal, worker_pids.first + Process.kill :INT , @pid + t.join + + assert_match "Thread: TID", output.join + end + # Send requests 10 per second. Send 10, then :TERM server, then send another 30. # No more than 10 should throw Errno::ECONNRESET. def term_closes_listeners(unix: false)