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.
- checksums.yaml +4 -4
- data/History.md +139 -0
- data/README.md +25 -13
- data/docs/5.0-Upgrade.md +98 -0
- data/docs/6.0-Upgrade.md +56 -0
- data/docs/7.0-Upgrade.md +52 -0
- data/docs/8.0-Upgrade.md +100 -0
- data/docs/deployment.md +58 -23
- data/docs/grpc.md +62 -0
- data/docs/images/favicon.svg +1 -0
- data/docs/images/running-puma.svg +1 -0
- data/docs/images/standard-logo.svg +1 -0
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +5 -12
- data/docs/plugins.md +2 -2
- data/docs/signals.md +10 -10
- data/docs/stats.md +2 -2
- data/docs/systemd.md +3 -3
- data/ext/puma_http11/http11_parser.java.rl +51 -65
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +168 -104
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
- data/ext/puma_http11/puma_http11.c +101 -109
- data/lib/puma/app/status.rb +10 -2
- data/lib/puma/cli.rb +1 -1
- data/lib/puma/client.rb +111 -91
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +10 -9
- data/lib/puma/cluster/worker_handle.rb +2 -2
- data/lib/puma/cluster.rb +12 -11
- data/lib/puma/cluster_accept_loop_delay.rb +17 -18
- data/lib/puma/configuration.rb +86 -16
- data/lib/puma/const.rb +2 -2
- data/lib/puma/control_cli.rb +1 -1
- data/lib/puma/detect.rb +11 -0
- data/lib/puma/dsl.rb +115 -18
- data/lib/puma/launcher.rb +35 -30
- data/lib/puma/reactor.rb +3 -12
- data/lib/puma/{request.rb → response.rb} +25 -194
- data/lib/puma/runner.rb +1 -1
- data/lib/puma/server.rb +102 -63
- data/lib/puma/server_plugin_control.rb +32 -0
- data/lib/puma/single.rb +2 -2
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +139 -24
- data/lib/rack/handler/puma.rb +1 -1
- data/tools/Dockerfile +13 -5
- metadata +16 -5
- 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 '
|
|
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
|
|
45
|
-
include
|
|
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)
|
|
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/
|
|
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 "
|
|
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
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
+
client.reset
|
|
514
516
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
data/lib/puma/state_file.rb
CHANGED
|
@@ -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
|
|
data/lib/puma/thread_pool.rb
CHANGED
|
@@ -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
|
-
@
|
|
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:
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
253
|
+
@processors << processor
|
|
181
254
|
|
|
182
|
-
|
|
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
|
|
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
|
-
|
|
380
|
+
@processors, dead_processors = @processors.partition(&:alive?)
|
|
289
381
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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
|
|
366
|
-
@
|
|
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
|
-
|
|
377
|
-
t.join
|
|
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
|
-
@
|
|
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
|