puma 7.0.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +139 -0
  3. data/README.md +25 -13
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/grpc.md +62 -0
  10. data/docs/images/favicon.svg +1 -0
  11. data/docs/images/running-puma.svg +1 -0
  12. data/docs/images/standard-logo.svg +1 -0
  13. data/docs/jungle/README.md +1 -1
  14. data/docs/kubernetes.md +5 -12
  15. data/docs/plugins.md +2 -2
  16. data/docs/signals.md +10 -10
  17. data/docs/stats.md +2 -2
  18. data/docs/systemd.md +3 -3
  19. data/ext/puma_http11/http11_parser.java.rl +51 -65
  20. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  21. data/ext/puma_http11/org/jruby/puma/Http11.java +168 -104
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  23. data/ext/puma_http11/puma_http11.c +101 -109
  24. data/lib/puma/app/status.rb +10 -2
  25. data/lib/puma/cli.rb +1 -1
  26. data/lib/puma/client.rb +111 -91
  27. data/lib/puma/client_env.rb +171 -0
  28. data/lib/puma/cluster/worker.rb +10 -9
  29. data/lib/puma/cluster/worker_handle.rb +2 -2
  30. data/lib/puma/cluster.rb +12 -11
  31. data/lib/puma/cluster_accept_loop_delay.rb +17 -18
  32. data/lib/puma/configuration.rb +86 -16
  33. data/lib/puma/const.rb +2 -2
  34. data/lib/puma/control_cli.rb +1 -1
  35. data/lib/puma/detect.rb +11 -0
  36. data/lib/puma/dsl.rb +115 -18
  37. data/lib/puma/launcher.rb +35 -30
  38. data/lib/puma/reactor.rb +3 -12
  39. data/lib/puma/{request.rb → response.rb} +25 -194
  40. data/lib/puma/runner.rb +1 -1
  41. data/lib/puma/server.rb +102 -63
  42. data/lib/puma/server_plugin_control.rb +32 -0
  43. data/lib/puma/single.rb +2 -2
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +139 -24
  46. data/lib/rack/handler/puma.rb +1 -1
  47. data/tools/Dockerfile +13 -5
  48. metadata +16 -5
  49. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/server.rb CHANGED
@@ -11,7 +11,7 @@ require_relative 'reactor'
11
11
  require_relative 'client'
12
12
  require_relative 'binder'
13
13
  require_relative 'util'
14
- require_relative 'request'
14
+ require_relative 'response'
15
15
  require_relative 'configuration'
16
16
  require_relative 'cluster_accept_loop_delay'
17
17
 
@@ -19,9 +19,6 @@ require 'socket'
19
19
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
20
20
 
21
21
  module Puma
22
- # Add `Thread#puma_server` and `Thread#puma_server=`
23
- Thread.attr_accessor(:puma_server)
24
-
25
22
  # The HTTP Server itself. Serves out a single Rack app.
26
23
  #
27
24
  # This class is used by the `Puma::Single` and `Puma::Cluster` classes
@@ -34,15 +31,15 @@ module Puma
34
31
  # Each `Puma::Server` will have one reactor and one thread pool.
35
32
  class Server
36
33
  module FiberPerRequest
37
- def handle_request(client, requests)
34
+ def handle_request(processor, client, requests)
38
35
  Fiber.new do
39
36
  super
40
37
  end.resume
41
38
  end
42
39
  end
43
40
 
44
- include Puma::Const
45
- include Request
41
+ include Const
42
+ include Response
46
43
 
47
44
  attr_reader :options
48
45
  attr_reader :thread
@@ -262,7 +259,9 @@ module Puma
262
259
 
263
260
  @status = :run
264
261
 
265
- @thread_pool = ThreadPool.new(thread_name, options) { |client| process_client client }
262
+ @thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
263
+ process_client(processor, client)
264
+ end
266
265
 
267
266
  if @queue_requests
268
267
  @reactor = Reactor.new(@io_selector_backend) { |c|
@@ -302,12 +301,12 @@ module Puma
302
301
  # If read buffering is not done, and no other read buffering is performed (such as by an application server
303
302
  # such as nginx) then the application would be subject to a slow client attack.
304
303
  #
305
- # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
304
+ # For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/main/docs/architecture.md).
306
305
  #
307
306
  # The method checks to see if it has the full header and body with
308
307
  # the `Puma::Client#try_to_finish` method. If the full request has been sent,
309
308
  # then the request is passed to the ThreadPool (`@thread_pool << client`)
310
- # so that a "worker thread" can pick up the request and begin to execute application logic.
309
+ # so that a "processor thread" can pick up the request and begin to execute application logic.
311
310
  # The Client is then removed from the reactor (return `true`).
312
311
  #
313
312
  # If a client object times out, a 408 response is written, its connection is closed,
@@ -404,6 +403,7 @@ module Puma
404
403
  next
405
404
  end
406
405
  drain += 1 if shutting_down?
406
+
407
407
  client = new_client(io, sock)
408
408
  client.send(addr_send_name, addr_value) if addr_value
409
409
  pool << client
@@ -447,7 +447,9 @@ module Puma
447
447
  def new_client(io, sock)
448
448
  client = Client.new(io, @binder.env(sock))
449
449
  client.listener = sock
450
+ client.env_set_http_version = @env_set_http_version
450
451
  client.http_content_length_limit = @http_content_length_limit
452
+ client.supported_http_methods = @supported_http_methods
451
453
  client
452
454
  end
453
455
 
@@ -473,17 +475,14 @@ module Puma
473
475
  # Given a connection on +client+, handle the incoming requests,
474
476
  # or queue the connection in the Reactor if no request is available.
475
477
  #
476
- # This method is called from a ThreadPool worker thread.
478
+ # This method is called from a ThreadPool processor thread.
477
479
  #
478
480
  # This method supports HTTP Keep-Alive so it may, depending on if the client
479
481
  # indicates that it supports keep alive, wait for another request before
480
482
  # returning.
481
483
  #
482
484
  # Return true if one or more requests were processed.
483
- def process_client(client)
484
- # Advertise this server into the thread
485
- Thread.current.puma_server = self
486
-
485
+ def process_client(processor, client)
487
486
  close_socket = true
488
487
 
489
488
  requests = 0
@@ -502,31 +501,41 @@ module Puma
502
501
  client.finish(@first_data_timeout)
503
502
  end
504
503
 
505
- @requests_count += 1
506
- case handle_request(client, requests + 1)
507
- when false
508
- when :async
509
- close_socket = false
510
- when true
511
- requests += 1
504
+ can_loop = true
505
+ while can_loop
506
+ can_loop = false
507
+ @requests_count += 1
508
+ case handle_request(processor, client, requests + 1)
509
+ when :close
510
+ when :async
511
+ close_socket = false
512
+ when :keep_alive
513
+ requests += 1
512
514
 
513
- client.reset
515
+ client.reset
514
516
 
515
- # This indicates data exists in the client read buffer and there may be
516
- # additional requests on it, so process them
517
- next_request_ready = if client.has_back_to_back_requests?
518
- with_force_shutdown(client) { client.process_back_to_back_requests }
519
- else
520
- with_force_shutdown(client) { client.eagerly_finish }
521
- end
517
+ # This indicates data exists in the client read buffer and there may be
518
+ # additional requests on it, so process them
519
+ next_request_ready = if client.has_back_to_back_requests?
520
+ with_force_shutdown(client) { client.process_back_to_back_requests }
521
+ else
522
+ with_force_shutdown(client) { client.eagerly_finish }
523
+ end
522
524
 
523
- if next_request_ready
524
- @thread_pool << client
525
- close_socket = false
526
- elsif @queue_requests
527
- client.set_timeout @persistent_timeout
528
- if @reactor.add client
529
- close_socket = false
525
+ if next_request_ready
526
+ # When Puma has spare threads, allow this one to be monopolized
527
+ # Perf optimization for https://github.com/puma/puma/issues/3788
528
+ if @thread_pool.waiting > 0
529
+ can_loop = true
530
+ else
531
+ @thread_pool << client
532
+ close_socket = false
533
+ end
534
+ elsif @queue_requests
535
+ client.set_timeout @persistent_timeout
536
+ if @reactor.add client
537
+ close_socket = false
538
+ end
530
539
  end
531
540
  end
532
541
  end
@@ -573,7 +582,7 @@ module Puma
573
582
  lowlevel_error(e, client.env)
574
583
  @log_writer.ssl_error e, client.io
575
584
  when HttpParserError
576
- response_to_error(client, requests, e, 400)
585
+ response_to_error(client, requests, e, client.error_status_code || 400)
577
586
  @log_writer.parse_error e, client
578
587
  when HttpParserError501
579
588
  response_to_error(client, requests, e, 501)
@@ -606,7 +615,15 @@ module Puma
606
615
  end
607
616
 
608
617
  def response_to_error(client, requests, err, status_code)
609
- status, headers, res_body = lowlevel_error(err, client.env, status_code)
618
+ # @todo remove sometime later
619
+ if status_code == 413
620
+ status = 413
621
+ res_body = ["Payload Too Large"]
622
+ headers = {}
623
+ headers[CONTENT_LENGTH2] = 17
624
+ else
625
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
626
+ end
610
627
  prepare_response(status, headers, res_body, requests, client)
611
628
  end
612
629
  private :response_to_error
@@ -614,32 +631,11 @@ module Puma
614
631
  # Wait for all outstanding requests to finish.
615
632
  #
616
633
  def graceful_shutdown
617
- if options[:shutdown_debug]
618
- threads = Thread.list
619
- total = threads.size
620
-
621
- pid = Process.pid
622
-
623
- $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
624
-
625
- threads.each_with_index do |t,i|
626
- $stdout.syswrite "#{pid}: Thread #{i+1}/#{total}: #{t.inspect}\n"
627
- $stdout.syswrite "#{pid}: #{t.backtrace.join("\n#{pid}: ")}\n\n"
628
- end
629
- $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
630
- end
631
-
632
634
  if @status != :restart
633
635
  @binder.close
634
636
  end
635
637
 
636
- if @thread_pool
637
- if timeout = options[:force_shutdown_after]
638
- @thread_pool.shutdown timeout.to_f
639
- else
640
- @thread_pool.shutdown
641
- end
642
- end
638
+ @thread_pool.shutdown(options[:force_shutdown_after])
643
639
  end
644
640
 
645
641
  def notify_safely(message)
@@ -656,7 +652,7 @@ module Puma
656
652
  end
657
653
  private :notify_safely
658
654
 
659
- # Stops the acceptor thread and then causes the worker threads to finish
655
+ # Stops the acceptor thread and then causes the processor threads to finish
660
656
  # off the request queue before finally exiting.
661
657
 
662
658
  def stop(sync=false)
@@ -706,7 +702,7 @@ module Puma
706
702
 
707
703
  def reset_max
708
704
  @reactor.reactor_max = 0 if @reactor
709
- @thread_pool.reset_max
705
+ @thread_pool&.reset_max
710
706
  end
711
707
 
712
708
  # below are 'delegations' to binder
@@ -726,6 +722,49 @@ module Puma
726
722
  @binder.add_unix_listener path, umask, mode, backlog
727
723
  end
728
724
 
725
+ # Updates the minimum and maximum number of threads in the thread pool.
726
+ #
727
+ # This method allows dynamic adjustment of the thread pool size while the server
728
+ # is running. It validates the provided values and updates both the thread pool
729
+ # and the server's thread configuration.
730
+ #
731
+ # @param min [Integer] The minimum number of threads to maintain in the pool.
732
+ # Defaults to the current minimum if not specified. Must be greater than 0
733
+ # and less than or equal to max.
734
+ # @param max [Integer] The maximum number of threads allowed in the pool.
735
+ # Defaults to the current maximum if not specified. Must be greater than or
736
+ # equal to min.
737
+ #
738
+ # @return [void]
739
+ #
740
+ # @note If validation fails, a warning message is logged and no changes are made.
741
+ #
742
+ # @example Update both min and max threads
743
+ # server.update_thread_pool_min_max(min: 2, max: 8)
744
+ #
745
+ # @example Update only the minimum threads
746
+ # server.update_thread_pool_min_max(min: 4)
747
+ #
748
+ # @example Update only the maximum threads
749
+ # server.update_thread_pool_min_max(max: 16)
750
+ #
751
+ def update_thread_pool_min_max(min: @min_threads, max: @max_threads)
752
+ if min > max
753
+ @log_writer.log "`min' value cannot be greater than `max' value."
754
+ return
755
+ end
756
+
757
+ if min < 0
758
+ @log_writer.log "`min' value cannot be less than 0"
759
+ return
760
+ end
761
+
762
+ @thread_pool&.with_mutex do
763
+ @thread_pool.min, @thread_pool.max = min, max
764
+ @min_threads, @max_threads = min, max
765
+ end
766
+ end
767
+
729
768
  # @!attribute [r] connected_ports
730
769
  def connected_ports
731
770
  @binder.connected_ports
@@ -0,0 +1,32 @@
1
+ module Puma
2
+ # ServerPluginControl provides a control interface for server plugins to
3
+ # interact with and manage server settings dynamically.
4
+ #
5
+ # This class acts as a facade between plugins and the Puma server,
6
+ # allowing plugins to safely modify server configuration and thread pool
7
+ # settings without direct access to the server's internal state.
8
+ #
9
+ class ServerPluginControl
10
+ def initialize(server)
11
+ @server = server
12
+ end
13
+
14
+ # Returns the maximum number of threads in the thread pool.
15
+ def max_threads
16
+ @server.max_threads
17
+ end
18
+
19
+ # Returns the minimum number of threads in the thread pool.
20
+ def min_threads
21
+ @server.min_threads
22
+ end
23
+
24
+ # Updates the minimum and maximum number of threads in the thread pool.
25
+ #
26
+ # @see Puma::Server#update_thread_pool_min_max
27
+ #
28
+ def update_thread_pool_min_max(min: max_threads, max: min_threads)
29
+ @server.update_thread_pool_min_max(min: min, max: max)
30
+ end
31
+ end
32
+ end
data/lib/puma/single.rb CHANGED
@@ -49,8 +49,8 @@ module Puma
49
49
 
50
50
  start_control
51
51
 
52
- @server = server = start_server
53
- server_thread = server.run
52
+ @server = start_server
53
+ server_thread = @server.run
54
54
 
55
55
  log "Use Ctrl-C to stop"
56
56
 
@@ -32,10 +32,11 @@ module Puma
32
32
  "#{k}: \"#{v}\"\n" : "#{k}: #{v}\n")
33
33
  end
34
34
  end
35
+
35
36
  if permission
36
- File.write path, contents, mode: 'wb:UTF-8'
37
- else
38
37
  File.write path, contents, mode: 'wb:UTF-8', perm: permission
38
+ else
39
+ File.write path, contents, mode: 'wb:UTF-8'
39
40
  end
40
41
  end
41
42
 
@@ -3,8 +3,13 @@
3
3
  require 'thread'
4
4
 
5
5
  require_relative 'io_buffer'
6
+ require_relative 'server_plugin_control'
6
7
 
7
8
  module Puma
9
+
10
+ # Add `Thread#puma_server` and `Thread#puma_server=`
11
+ Thread.attr_accessor(:puma_server)
12
+
8
13
  # Internal Docs for A simple thread pool management object.
9
14
  #
10
15
  # Each Puma "worker" has a thread pool to process requests.
@@ -20,6 +25,51 @@ module Puma
20
25
  class ForceShutdown < RuntimeError
21
26
  end
22
27
 
28
+ class ProcessorThread
29
+ attr_accessor :thread
30
+ attr_writer :marked_as_io_thread
31
+
32
+ def initialize(pool)
33
+ @pool = pool
34
+ @thread = nil
35
+ @marked_as_io_thread = false
36
+ end
37
+
38
+ def mark_as_io_thread!
39
+ unless @marked_as_io_thread
40
+ @marked_as_io_thread = true
41
+
42
+ # Immediately signal the pool that it can spawn a new thread
43
+ # if there's some work in the queue.
44
+ @pool.spawn_thread_if_needed
45
+ end
46
+ end
47
+
48
+ def marked_as_io_thread?
49
+ @marked_as_io_thread
50
+ end
51
+
52
+ def alive?
53
+ @thread&.alive?
54
+ end
55
+
56
+ def join(...)
57
+ @thread.join(...)
58
+ end
59
+
60
+ def kill(...)
61
+ @thread.kill(...)
62
+ end
63
+
64
+ def [](key)
65
+ @thread[key]
66
+ end
67
+
68
+ def raise(...)
69
+ @thread.raise(...)
70
+ end
71
+ end
72
+
23
73
  # How long, after raising the ForceShutdown of a thread during
24
74
  # forced shutdown mode, to wait for the thread to try and finish
25
75
  # up its work before leaving the thread to die on the vine.
@@ -33,7 +83,9 @@ module Puma
33
83
  # The block passed is the work that will be performed in each
34
84
  # thread.
35
85
  #
36
- def initialize(name, options = {}, &block)
86
+ def initialize(name, options = {}, server: nil, &block)
87
+ @server = server
88
+
37
89
  @not_empty = ConditionVariable.new
38
90
  @not_full = ConditionVariable.new
39
91
  @mutex = Mutex.new
@@ -46,10 +98,13 @@ module Puma
46
98
  @name = name
47
99
  @min = Integer(options[:min_threads])
48
100
  @max = Integer(options[:max_threads])
101
+ @max_io_threads = Integer(options[:max_io_threads] || 0)
102
+
49
103
  # Not an 'exposed' option, options[:pool_shutdown_grace_time] is used in CI
50
104
  # to shorten @shutdown_grace_time from SHUTDOWN_GRACE_TIME. Parallel CI
51
105
  # makes stubbing constants difficult.
52
106
  @shutdown_grace_time = Float(options[:pool_shutdown_grace_time] || SHUTDOWN_GRACE_TIME)
107
+ @shutdown_debug = options[:shutdown_debug]
53
108
  @block = block
54
109
  @out_of_band = options[:out_of_band]
55
110
  @out_of_band_running = false
@@ -64,7 +119,7 @@ module Puma
64
119
  @trim_requested = 0
65
120
  @out_of_band_pending = false
66
121
 
67
- @workers = []
122
+ @processors = []
68
123
 
69
124
  @auto_trim = nil
70
125
  @reaper = nil
@@ -81,6 +136,7 @@ module Puma
81
136
  end
82
137
 
83
138
  attr_reader :spawned, :trim_requested, :waiting
139
+ attr_accessor :min, :max
84
140
 
85
141
  # generate stats hash so as not to perform multiple locks
86
142
  # @return [Hash] hash containing stat info from ThreadPool
@@ -90,8 +146,9 @@ module Puma
90
146
  @backlog_max = 0
91
147
  { backlog: @todo.size,
92
148
  running: @spawned,
93
- pool_capacity: @waiting + (@max - @spawned),
149
+ pool_capacity: pool_capacity,
94
150
  busy_threads: @spawned - @waiting + @todo.size,
151
+ io_threads: @processors.count(&:marked_as_io_thread?),
95
152
  backlog_max: temp
96
153
  }
97
154
  end
@@ -115,7 +172,7 @@ module Puma
115
172
 
116
173
  # @!attribute [r] pool_capacity
117
174
  def pool_capacity
118
- waiting + (@max - spawned)
175
+ (waiting + (@max - spawned)).clamp(0, Float::INFINITY)
119
176
  end
120
177
 
121
178
  # @!attribute [r] busy_threads
@@ -132,8 +189,12 @@ module Puma
132
189
  @spawned += 1
133
190
 
134
191
  trigger_before_thread_start_hooks
135
- th = Thread.new(@spawned) do |spawned|
192
+ processor = ProcessorThread.new(self)
193
+ processor.thread = Thread.new(processor, @spawned) do |processor, spawned|
136
194
  Puma.set_thread_name '%s tp %03i' % [@name, spawned]
195
+ # Advertise server into the thread
196
+ Thread.current.puma_server = @server
197
+
137
198
  todo = @todo
138
199
  block = @block
139
200
  mutex = @mutex
@@ -144,11 +205,23 @@ module Puma
144
205
  work = nil
145
206
 
146
207
  mutex.synchronize do
208
+ if processor.marked_as_io_thread?
209
+ if @processors.count { |t| !t.marked_as_io_thread? } < @max
210
+ # We're not at max processor threads, so the io thread can rejoin the normal population.
211
+ processor.marked_as_io_thread = false
212
+ else
213
+ # We're already at max threads, so we exit the extra io thread.
214
+ @processors.delete(processor)
215
+ trigger_before_thread_exit_hooks
216
+ Thread.exit
217
+ end
218
+ end
219
+
147
220
  while todo.empty?
148
221
  if @trim_requested > 0
149
222
  @trim_requested -= 1
150
223
  @spawned -= 1
151
- @workers.delete th
224
+ @processors.delete(processor)
152
225
  not_full.signal
153
226
  trigger_before_thread_exit_hooks
154
227
  Thread.exit
@@ -170,16 +243,16 @@ module Puma
170
243
  end
171
244
 
172
245
  begin
173
- @out_of_band_pending = true if block.call(work)
246
+ @out_of_band_pending = true if block.call(processor, work)
174
247
  rescue Exception => e
175
248
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
176
249
  end
177
250
  end
178
251
  end
179
252
 
180
- @workers << th
253
+ @processors << processor
181
254
 
182
- th
255
+ processor
183
256
  end
184
257
 
185
258
  private :spawn_thread
@@ -189,7 +262,7 @@ module Puma
189
262
 
190
263
  @before_thread_start.each do |b|
191
264
  begin
192
- b[:block].call
265
+ b[:block].call(ServerPluginControl.new(@server))
193
266
  rescue Exception => e
194
267
  STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
195
268
  end
@@ -248,6 +321,16 @@ module Puma
248
321
  @mutex.synchronize(&block)
249
322
  end
250
323
 
324
+ # :nodoc:
325
+ #
326
+ # Must be called with @mutex held!
327
+ #
328
+ def can_spawn_processor?
329
+ io_processors_count = @processors.count(&:marked_as_io_thread?)
330
+ extra_io_processors_count = io_processors_count > @max_io_threads ? io_processors_count - @max_io_threads : 0
331
+ (@spawned - io_processors_count) < (@max - extra_io_processors_count)
332
+ end
333
+
251
334
  # Add +work+ to the todo list for a Thread to pickup and process.
252
335
  def <<(work)
253
336
  with_mutex do
@@ -259,12 +342,21 @@ module Puma
259
342
  t = @todo.size
260
343
  @backlog_max = t if t > @backlog_max
261
344
 
262
- if @waiting < @todo.size and @spawned < @max
345
+ if @waiting < @todo.size and can_spawn_processor?
263
346
  spawn_thread
264
347
  end
265
348
 
266
349
  @not_empty.signal
267
350
  end
351
+ self
352
+ end
353
+
354
+ def spawn_thread_if_needed # :nodoc:
355
+ with_mutex do
356
+ if @waiting < @todo.size and can_spawn_processor?
357
+ spawn_thread
358
+ end
359
+ end
268
360
  end
269
361
 
270
362
  # If there are any free threads in the pool, tell one to go ahead
@@ -285,16 +377,12 @@ module Puma
285
377
  # spawned counter so that new healthy threads could be created again.
286
378
  def reap
287
379
  with_mutex do
288
- dead_workers = @workers.reject(&:alive?)
380
+ @processors, dead_processors = @processors.partition(&:alive?)
289
381
 
290
- dead_workers.each do |worker|
291
- worker.kill
382
+ dead_processors.each do |processor|
383
+ processor.kill
292
384
  @spawned -= 1
293
385
  end
294
-
295
- @workers.delete_if do |w|
296
- dead_workers.include?(w)
297
- end
298
386
  end
299
387
  end
300
388
 
@@ -353,7 +441,7 @@ module Puma
353
441
  # Next, wait an extra +@shutdown_grace_time+ seconds then force-kill remaining
354
442
  # threads. Finally, wait 1 second for remaining threads to exit.
355
443
  #
356
- def shutdown(timeout=-1)
444
+ def shutdown(timeout)
357
445
  threads = with_mutex do
358
446
  @shutdown = true
359
447
  @trim_requested = @spawned
@@ -362,8 +450,12 @@ module Puma
362
450
 
363
451
  @auto_trim&.stop
364
452
  @reaper&.stop
365
- # dup workers so that we join them all safely
366
- @workers.dup
453
+ # dup processors so that we join them all safely
454
+ @processors.dup
455
+ end
456
+
457
+ if @shutdown_debug == true
458
+ shutdown_debug("Shutdown initiated")
367
459
  end
368
460
 
369
461
  if timeout == -1
@@ -373,13 +465,16 @@ module Puma
373
465
  join = ->(inner_timeout) do
374
466
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
375
467
  threads.reject! do |t|
376
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
377
- t.join inner_timeout - elapsed
468
+ remaining = inner_timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
469
+ remaining > 0 && t.join(remaining)
378
470
  end
379
471
  end
380
472
 
381
473
  # Wait +timeout+ seconds for threads to finish.
382
474
  join.call(timeout)
475
+ if @shutdown_debug == :on_force && !threads.empty?
476
+ shutdown_debug("Shutdown timeout exceeded")
477
+ end
383
478
 
384
479
  # If threads are still running, raise ForceShutdown and wait to finish.
385
480
  @shutdown_mutex.synchronize do
@@ -389,6 +484,9 @@ module Puma
389
484
  end
390
485
  end
391
486
  join.call(@shutdown_grace_time)
487
+ if @shutdown_debug == :on_force && !threads.empty?
488
+ shutdown_debug("Shutdown grace timeout exceeded")
489
+ end
392
490
 
393
491
  # If threads are _still_ running, forcefully kill them and wait to finish.
394
492
  threads.each(&:kill)
@@ -396,7 +494,24 @@ module Puma
396
494
  end
397
495
 
398
496
  @spawned = 0
399
- @workers = []
497
+ @processors = []
498
+ end
499
+
500
+ private
501
+
502
+ def shutdown_debug(message)
503
+ pid = Process.pid
504
+ threads = Thread.list
505
+
506
+ $stdout.syswrite "#{pid}: #{message}\n"
507
+ $stdout.syswrite "#{pid}: === Begin thread backtrace dump ===\n"
508
+
509
+ threads.each_with_index do |thread, index|
510
+ $stdout.syswrite "#{pid}: Thread #{index + 1}/#{threads.size}: #{thread.inspect}\n"
511
+ $stdout.syswrite "#{pid}: #{(thread.backtrace || []).join("\n#{pid}: ")}\n\n"
512
+ end
513
+
514
+ $stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
400
515
  end
401
516
  end
402
517
  end
@@ -109,7 +109,7 @@ module Puma
109
109
  end
110
110
 
111
111
  if port
112
- host ||= ::Puma::Configuration::DEFAULTS[:tcp_host]
112
+ host ||= ::Puma::Configuration.default_tcp_host
113
113
  config.port port, host
114
114
  end
115
115
  end