puma 6.6.0 → 8.0.2
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 +309 -5
- data/README.md +41 -42
- 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/fork_worker.md +5 -5
- 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 +11 -16
- data/docs/plugins.md +2 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +4 -3
- data/docs/systemd.md +4 -4
- data/ext/puma_http11/extconf.rb +2 -17
- data/ext/puma_http11/http11_parser.java.rl +51 -65
- data/ext/puma_http11/mini_ssl.c +18 -8
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +174 -102
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
- data/ext/puma_http11/puma_http11.c +122 -118
- data/lib/puma/app/status.rb +10 -2
- data/lib/puma/binder.rb +12 -10
- data/lib/puma/cli.rb +4 -6
- data/lib/puma/client.rb +205 -131
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +17 -17
- data/lib/puma/cluster/worker_handle.rb +38 -7
- data/lib/puma/cluster.rb +44 -30
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +173 -58
- data/lib/puma/const.rb +11 -11
- data/lib/puma/control_cli.rb +7 -3
- data/lib/puma/detect.rb +13 -0
- data/lib/puma/dsl.rb +225 -108
- data/lib/puma/error_logger.rb +3 -1
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/launcher/bundle_pruner.rb +3 -5
- data/lib/puma/launcher.rb +57 -53
- data/lib/puma/log_writer.rb +8 -2
- data/lib/puma/minissl.rb +0 -1
- data/lib/puma/plugin/systemd.rb +3 -3
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/reactor.rb +19 -13
- data/lib/puma/{request.rb → response.rb} +56 -212
- data/lib/puma/runner.rb +9 -18
- data/lib/puma/server.rb +182 -97
- data/lib/puma/server_plugin_control.rb +32 -0
- data/lib/puma/single.rb +7 -4
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +170 -99
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +3 -3
- data/tools/Dockerfile +15 -5
- metadata +19 -7
- data/ext/puma_http11/ext_help.h +0 -15
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puma
|
|
4
|
+
|
|
5
|
+
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# This module is included in `Client`. It contains code to process the `env`
|
|
9
|
+
# before it is passed to the app.
|
|
10
|
+
#
|
|
11
|
+
module ClientEnv # :nodoc:
|
|
12
|
+
|
|
13
|
+
include Puma::Const
|
|
14
|
+
|
|
15
|
+
# Given a Hash +env+ for the request read from +client+, add
|
|
16
|
+
# and fixup keys to comply with Rack's env guidelines.
|
|
17
|
+
# @param env [Hash] see Puma::Client#env, from request
|
|
18
|
+
# @param client [Puma::Client] only needed for Client#peerip
|
|
19
|
+
#
|
|
20
|
+
def normalize_env
|
|
21
|
+
if host = @env[HTTP_HOST]
|
|
22
|
+
# host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
|
|
23
|
+
if colon = host.rindex("]:") # IPV6 with port
|
|
24
|
+
@env[SERVER_NAME] = host[0, colon+1]
|
|
25
|
+
@env[SERVER_PORT] = host[colon+2, host.bytesize]
|
|
26
|
+
elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
|
|
27
|
+
@env[SERVER_NAME] = host[0, colon]
|
|
28
|
+
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
|
29
|
+
else
|
|
30
|
+
@env[SERVER_NAME] = host
|
|
31
|
+
@env[SERVER_PORT] = default_server_port
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
@env[SERVER_NAME] = LOCALHOST
|
|
35
|
+
@env[SERVER_PORT] = default_server_port
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless @env[REQUEST_PATH]
|
|
39
|
+
# it might be a dumbass full host request header
|
|
40
|
+
uri = begin
|
|
41
|
+
URI.parse(@env[REQUEST_URI])
|
|
42
|
+
rescue URI::InvalidURIError
|
|
43
|
+
raise Puma::HttpParserError
|
|
44
|
+
end
|
|
45
|
+
@env[REQUEST_PATH] = uri.path
|
|
46
|
+
|
|
47
|
+
# A nil env value will cause a LintError (and fatal errors elsewhere),
|
|
48
|
+
# so only set the env value if there actually is a value.
|
|
49
|
+
@env[QUERY_STRING] = uri.query if uri.query
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@env[PATH_INFO] = @env[REQUEST_PATH].to_s # #to_s in case it's nil
|
|
53
|
+
|
|
54
|
+
# From https://www.ietf.org/rfc/rfc3875 :
|
|
55
|
+
# "Script authors should be aware that the REMOTE_ADDR and
|
|
56
|
+
# REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
|
|
57
|
+
# may not identify the ultimate source of the request.
|
|
58
|
+
# They identify the client for the immediate request to the
|
|
59
|
+
# server; that client may be a proxy, gateway, or other
|
|
60
|
+
# intermediary acting on behalf of the actual source client."
|
|
61
|
+
#
|
|
62
|
+
|
|
63
|
+
unless @env.key?(REMOTE_ADDR)
|
|
64
|
+
begin
|
|
65
|
+
addr = peerip
|
|
66
|
+
rescue Errno::ENOTCONN
|
|
67
|
+
# Client disconnects can result in an inability to get the
|
|
68
|
+
# peeraddr from the socket; default to unspec.
|
|
69
|
+
if peer_family == Socket::AF_INET6
|
|
70
|
+
addr = UNSPECIFIED_IPV6
|
|
71
|
+
else
|
|
72
|
+
addr = UNSPECIFIED_IPV4
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Set unix socket addrs to localhost
|
|
77
|
+
if addr.empty?
|
|
78
|
+
addr = peer_family == Socket::AF_INET6 ? LOCALHOST_IPV6 : LOCALHOST_IPV4
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@env[REMOTE_ADDR] = addr
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The legacy HTTP_VERSION header can be sent as a client header.
|
|
85
|
+
# Rack v4 may remove using HTTP_VERSION. If so, remove this line.
|
|
86
|
+
@env[HTTP_VERSION] = @env[SERVER_PROTOCOL] if @env_set_http_version
|
|
87
|
+
|
|
88
|
+
@env[PUMA_SOCKET] = @io
|
|
89
|
+
|
|
90
|
+
if @env[HTTPS_KEY] && @io.peercert
|
|
91
|
+
@env[PUMA_PEERCERT] = @io.peercert
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@env[HIJACK_P] = true
|
|
95
|
+
@env[HIJACK] = method(:full_hijack).to_proc
|
|
96
|
+
|
|
97
|
+
@env[RACK_INPUT] = @body || EmptyBody
|
|
98
|
+
@env[RACK_URL_SCHEME] ||= default_server_port == PORT_443 ? HTTPS : HTTP
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Fixup any headers with `,` in the name to have `_` now. We emit
|
|
102
|
+
# headers with `,` in them during the parse phase to avoid ambiguity
|
|
103
|
+
# with the `-` to `_` conversion for critical headers. But here for
|
|
104
|
+
# compatibility, we'll convert them back. This code is written to
|
|
105
|
+
# avoid allocation in the common case (ie there are no headers
|
|
106
|
+
# with `,` in their names), that's why it has the extra conditionals.
|
|
107
|
+
#
|
|
108
|
+
# @note If a normalized version of a `,` header already exists, we ignore
|
|
109
|
+
# the `,` version. This prevents clobbering headers managed by proxies
|
|
110
|
+
# but not by clients (Like X-Forwarded-For).
|
|
111
|
+
#
|
|
112
|
+
# @param env [Hash] see Puma::Client#env, from request, modifies in place
|
|
113
|
+
# @version 5.0.3
|
|
114
|
+
#
|
|
115
|
+
def req_env_post_parse
|
|
116
|
+
to_delete = nil
|
|
117
|
+
to_add = nil
|
|
118
|
+
|
|
119
|
+
@env.each do |k,v|
|
|
120
|
+
if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
|
|
121
|
+
if to_delete
|
|
122
|
+
to_delete << k
|
|
123
|
+
else
|
|
124
|
+
to_delete = [k]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
new_k = k.tr(",", "_")
|
|
128
|
+
if @env.key?(new_k)
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
unless to_add
|
|
133
|
+
to_add = {}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
to_add[new_k] = v
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if to_delete # rubocop:disable Style/SafeNavigation
|
|
141
|
+
to_delete.each { |k| env.delete(k) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if to_add
|
|
145
|
+
@env.merge! to_add
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A rack extension. If the app writes #call'ables to this
|
|
149
|
+
# array, we will invoke them when the request is done.
|
|
150
|
+
#
|
|
151
|
+
env[RACK_AFTER_REPLY] ||= []
|
|
152
|
+
env[RACK_RESPONSE_FINISHED] ||= []
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
HTTP_ON_VALUES = { "on" => true, HTTPS => true }
|
|
156
|
+
private_constant :HTTP_ON_VALUES
|
|
157
|
+
|
|
158
|
+
# @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
|
|
159
|
+
#
|
|
160
|
+
def default_server_port
|
|
161
|
+
if HTTP_ON_VALUES[@env[HTTPS_KEY]] ||
|
|
162
|
+
@env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
|
|
163
|
+
@env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
|
|
164
|
+
@env[HTTP_X_FORWARDED_SSL] == "on"
|
|
165
|
+
PORT_443
|
|
166
|
+
else
|
|
167
|
+
PORT_80
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/puma/cluster/worker.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Puma
|
|
|
14
14
|
class Worker < Puma::Runner # :nodoc:
|
|
15
15
|
attr_reader :index, :master
|
|
16
16
|
|
|
17
|
-
def initialize(index:, master:, launcher:, pipes:,
|
|
17
|
+
def initialize(index:, master:, launcher:, pipes:, app: nil)
|
|
18
18
|
super(launcher)
|
|
19
19
|
|
|
20
20
|
@index = index
|
|
@@ -23,7 +23,8 @@ module Puma
|
|
|
23
23
|
@worker_write = pipes[:worker_write]
|
|
24
24
|
@fork_pipe = pipes[:fork_pipe]
|
|
25
25
|
@wakeup = pipes[:wakeup]
|
|
26
|
-
@
|
|
26
|
+
@app = app
|
|
27
|
+
@server = nil
|
|
27
28
|
@hook_data = {}
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -57,7 +58,7 @@ module Puma
|
|
|
57
58
|
@config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
|
|
58
59
|
|
|
59
60
|
begin
|
|
60
|
-
|
|
61
|
+
@server = start_server
|
|
61
62
|
rescue Exception => e
|
|
62
63
|
log "! Unable to start worker"
|
|
63
64
|
log e
|
|
@@ -85,7 +86,7 @@ module Puma
|
|
|
85
86
|
if idx == -1 # stop server
|
|
86
87
|
if restart_server.length > 0
|
|
87
88
|
restart_server.clear
|
|
88
|
-
server.begin_restart(true)
|
|
89
|
+
@server.begin_restart(true)
|
|
89
90
|
@config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
|
|
90
91
|
end
|
|
91
92
|
elsif idx == -2 # refork cycle is done
|
|
@@ -103,20 +104,19 @@ module Puma
|
|
|
103
104
|
Signal.trap "SIGTERM" do
|
|
104
105
|
@worker_write << "#{PIPE_EXTERNAL_TERM}#{Process.pid}\n" rescue nil
|
|
105
106
|
restart_server.clear
|
|
106
|
-
server.stop
|
|
107
|
+
@server.stop
|
|
107
108
|
restart_server << false
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
begin
|
|
111
112
|
@worker_write << "#{PIPE_BOOT}#{Process.pid}:#{index}\n"
|
|
112
113
|
rescue SystemCallError, IOError
|
|
113
|
-
Puma::Util.purge_interrupt_queue
|
|
114
114
|
STDERR.puts "Master seems to have exited, exiting."
|
|
115
115
|
return
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
while restart_server.pop
|
|
119
|
-
server_thread = server.run
|
|
119
|
+
server_thread = @server.run
|
|
120
120
|
|
|
121
121
|
if @log_writer.debug? && index == 0
|
|
122
122
|
debug_loaded_extensions "Loaded Extensions - worker 0:"
|
|
@@ -128,16 +128,16 @@ module Puma
|
|
|
128
128
|
|
|
129
129
|
while true
|
|
130
130
|
begin
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
io << payload
|
|
131
|
+
payload = base_payload.dup
|
|
132
|
+
|
|
133
|
+
hsh = @server.stats
|
|
134
|
+
hsh.each do |k, v|
|
|
135
|
+
payload << %Q! "#{k}":#{v || 0},!
|
|
136
|
+
end
|
|
137
|
+
# sub call properly adds 'closing' string
|
|
138
|
+
io << payload.sub(/,\z/, " }\n")
|
|
139
|
+
@server.reset_max
|
|
139
140
|
rescue IOError
|
|
140
|
-
Puma::Util.purge_interrupt_queue
|
|
141
141
|
break
|
|
142
142
|
end
|
|
143
143
|
sleep @options[:worker_check_interval]
|
|
@@ -165,7 +165,7 @@ module Puma
|
|
|
165
165
|
launcher: @launcher,
|
|
166
166
|
pipes: { check_pipe: @check_pipe,
|
|
167
167
|
worker_write: @worker_write },
|
|
168
|
-
|
|
168
|
+
app: @app
|
|
169
169
|
new_worker.run
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -4,13 +4,15 @@ module Puma
|
|
|
4
4
|
class Cluster < Runner
|
|
5
5
|
#—————————————————————— DO NOT USE — this class is for internal use only ———
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
# This class represents a worker process from the perspective of the puma
|
|
9
8
|
# master process. It contains information about the process and its health
|
|
10
9
|
# and it exposes methods to control the process via IPC. It does not
|
|
11
10
|
# include the actual logic executed by the worker process itself. For that,
|
|
12
11
|
# see Puma::Cluster::Worker.
|
|
13
12
|
class WorkerHandle # :nodoc:
|
|
13
|
+
# array of stat 'max' keys
|
|
14
|
+
WORKER_MAX_KEYS = [:backlog_max, :reactor_max]
|
|
15
|
+
|
|
14
16
|
def initialize(idx, pid, phase, options)
|
|
15
17
|
@index = idx
|
|
16
18
|
@pid = pid
|
|
@@ -23,12 +25,13 @@ module Puma
|
|
|
23
25
|
@last_checkin = Time.now
|
|
24
26
|
@last_status = {}
|
|
25
27
|
@term = false
|
|
28
|
+
@worker_max = Array.new WORKER_MAX_KEYS.length, 0
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
|
31
|
+
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at, :process_status
|
|
29
32
|
|
|
30
33
|
# @version 5.0.0
|
|
31
|
-
attr_writer :pid, :phase
|
|
34
|
+
attr_writer :pid, :phase, :process_status
|
|
32
35
|
|
|
33
36
|
def booted?
|
|
34
37
|
@stage == :booted
|
|
@@ -51,12 +54,40 @@ module Puma
|
|
|
51
54
|
@term
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
STATUS_PATTERN = /{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads":(?<max_threads>\d*), "requests_count":(?<requests_count>\d*), "busy_threads":(?<busy_threads>\d*) }/
|
|
55
|
-
private_constant :STATUS_PATTERN
|
|
56
|
-
|
|
57
57
|
def ping!(status)
|
|
58
|
+
hsh = {}
|
|
59
|
+
k, v = nil, nil
|
|
60
|
+
status.tr('}{"', '').strip.split(", ") do |kv|
|
|
61
|
+
cntr = 0
|
|
62
|
+
kv.split(':') do |t|
|
|
63
|
+
if cntr == 0
|
|
64
|
+
k = t
|
|
65
|
+
cntr = 1
|
|
66
|
+
else
|
|
67
|
+
v = t
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
hsh[k.to_sym] = v.to_i
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# check stat max values, we can't signal workers to reset the max values,
|
|
74
|
+
# so we do so here
|
|
75
|
+
WORKER_MAX_KEYS.each_with_index do |key, idx|
|
|
76
|
+
next unless hsh[key]
|
|
77
|
+
|
|
78
|
+
if hsh[key] < @worker_max[idx]
|
|
79
|
+
hsh[key] = @worker_max[idx]
|
|
80
|
+
else
|
|
81
|
+
@worker_max[idx] = hsh[key]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
58
84
|
@last_checkin = Time.now
|
|
59
|
-
@last_status =
|
|
85
|
+
@last_status = hsh
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resets max values to zero. Called whenever `Cluster#stats` is called
|
|
89
|
+
def reset_max
|
|
90
|
+
WORKER_MAX_KEYS.length.times { |idx| @worker_max[idx] = 0 }
|
|
60
91
|
end
|
|
61
92
|
|
|
62
93
|
# @see Puma::Cluster#check_workers
|
data/lib/puma/cluster.rb
CHANGED
|
@@ -22,7 +22,8 @@ module Puma
|
|
|
22
22
|
@workers = []
|
|
23
23
|
@next_check = Time.now
|
|
24
24
|
|
|
25
|
-
@
|
|
25
|
+
@worker_max = [] # keeps track of 'max' stat values
|
|
26
|
+
@pending_phased_restart = false
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
# Returns the list of cluster worker handles.
|
|
@@ -44,10 +45,14 @@ module Puma
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def start_phased_restart
|
|
48
|
-
@events.
|
|
48
|
+
def start_phased_restart(refork = false)
|
|
49
|
+
@events.fire_before_restart!
|
|
49
50
|
@phase += 1
|
|
50
|
-
|
|
51
|
+
if refork
|
|
52
|
+
log "- Starting worker refork, phase: #{@phase}"
|
|
53
|
+
else
|
|
54
|
+
log "- Starting phased worker restart, phase: #{@phase}"
|
|
55
|
+
end
|
|
51
56
|
|
|
52
57
|
# Be sure to change the directory again before loading
|
|
53
58
|
# the app. This way we can pick up new code.
|
|
@@ -82,7 +87,7 @@ module Puma
|
|
|
82
87
|
end
|
|
83
88
|
|
|
84
89
|
debug "Spawned worker: #{pid}"
|
|
85
|
-
@workers
|
|
90
|
+
@workers.insert(idx, WorkerHandle.new(idx, pid, @phase, @options))
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
if @options[:fork_worker] && all_workers_in_phase?
|
|
@@ -166,7 +171,7 @@ module Puma
|
|
|
166
171
|
(@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
|
|
167
172
|
end
|
|
168
173
|
|
|
169
|
-
def check_workers
|
|
174
|
+
def check_workers(refork = false)
|
|
170
175
|
return if @next_check >= Time.now
|
|
171
176
|
|
|
172
177
|
@next_check = Time.now + @options[:worker_check_interval]
|
|
@@ -181,10 +186,15 @@ module Puma
|
|
|
181
186
|
# we need to phase any workers out (which will restart
|
|
182
187
|
# in the right phase).
|
|
183
188
|
#
|
|
184
|
-
w = @workers.find { |x| x.phase
|
|
189
|
+
w = @workers.find { |x| x.phase < @phase }
|
|
185
190
|
|
|
186
191
|
if w
|
|
187
|
-
|
|
192
|
+
if refork
|
|
193
|
+
log "- Stopping #{w.pid} for refork..."
|
|
194
|
+
else
|
|
195
|
+
log "- Stopping #{w.pid} for phased upgrade..."
|
|
196
|
+
end
|
|
197
|
+
|
|
188
198
|
unless w.term?
|
|
189
199
|
w.term
|
|
190
200
|
log "- #{w.signal} sent to #{w.pid}..."
|
|
@@ -211,12 +221,11 @@ module Puma
|
|
|
211
221
|
pipes[:wakeup] = @wakeup
|
|
212
222
|
end
|
|
213
223
|
|
|
214
|
-
server = start_server if preload?
|
|
215
224
|
new_worker = Worker.new index: index,
|
|
216
225
|
master: master,
|
|
217
226
|
launcher: @launcher,
|
|
218
227
|
pipes: pipes,
|
|
219
|
-
|
|
228
|
+
app: (app if preload?)
|
|
220
229
|
new_worker.run
|
|
221
230
|
end
|
|
222
231
|
|
|
@@ -228,7 +237,7 @@ module Puma
|
|
|
228
237
|
def phased_restart(refork = false)
|
|
229
238
|
return false if @options[:preload_app] && !refork
|
|
230
239
|
|
|
231
|
-
@
|
|
240
|
+
@pending_phased_restart = refork ? :refork : true
|
|
232
241
|
wakeup!
|
|
233
242
|
|
|
234
243
|
true
|
|
@@ -258,11 +267,14 @@ module Puma
|
|
|
258
267
|
end
|
|
259
268
|
|
|
260
269
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
|
261
|
-
# the master process.
|
|
270
|
+
# the master process. Calling this also resets stat 'max' values to zero.
|
|
262
271
|
# @!attribute [r] stats
|
|
272
|
+
# @return [Hash]
|
|
273
|
+
|
|
263
274
|
def stats
|
|
264
275
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
|
265
276
|
worker_status = @workers.map do |w|
|
|
277
|
+
w.reset_max
|
|
266
278
|
{
|
|
267
279
|
started_at: utc_iso8601(w.started_at),
|
|
268
280
|
pid: w.pid,
|
|
@@ -273,7 +285,6 @@ module Puma
|
|
|
273
285
|
last_status: w.last_status,
|
|
274
286
|
}
|
|
275
287
|
end
|
|
276
|
-
|
|
277
288
|
{
|
|
278
289
|
started_at: utc_iso8601(@started_at),
|
|
279
290
|
workers: @workers.size,
|
|
@@ -342,7 +353,7 @@ module Puma
|
|
|
342
353
|
|
|
343
354
|
stop_workers
|
|
344
355
|
stop
|
|
345
|
-
@events.
|
|
356
|
+
@events.fire_after_stopped!
|
|
346
357
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
|
347
358
|
exit 0 # Clean exit, workers were stopped
|
|
348
359
|
end
|
|
@@ -359,16 +370,12 @@ module Puma
|
|
|
359
370
|
|
|
360
371
|
if preload?
|
|
361
372
|
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
|
362
|
-
# but may be used by anyone.
|
|
363
|
-
|
|
364
|
-
# where calling thread_variable_get on a Process::Waiter will segfault.
|
|
365
|
-
# We can drop that clause once those versions of Ruby are no longer
|
|
366
|
-
# supported.
|
|
367
|
-
fork_safe = ->(t) { !t.is_a?(Process::Waiter) && t.thread_variable_get(:fork_safe) }
|
|
373
|
+
# but may be used by anyone.
|
|
374
|
+
fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
|
|
368
375
|
|
|
369
376
|
before = Thread.list.reject(&fork_safe)
|
|
370
377
|
|
|
371
|
-
log "* Restarts: (\u2714) hot (\u2716) phased"
|
|
378
|
+
log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
372
379
|
log "* Preloading application"
|
|
373
380
|
load_and_bind
|
|
374
381
|
|
|
@@ -386,7 +393,7 @@ module Puma
|
|
|
386
393
|
end
|
|
387
394
|
end
|
|
388
395
|
else
|
|
389
|
-
log "* Restarts: (\u2714) hot (\u2714) phased"
|
|
396
|
+
log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
390
397
|
|
|
391
398
|
unless @config.app_configured?
|
|
392
399
|
error "No application configured, nothing to run"
|
|
@@ -413,6 +420,7 @@ module Puma
|
|
|
413
420
|
|
|
414
421
|
log "Use Ctrl-C to stop"
|
|
415
422
|
|
|
423
|
+
warn_ruby_mn_threads
|
|
416
424
|
single_worker_warning
|
|
417
425
|
|
|
418
426
|
redirect_io
|
|
@@ -447,14 +455,18 @@ module Puma
|
|
|
447
455
|
break
|
|
448
456
|
end
|
|
449
457
|
|
|
450
|
-
if @
|
|
451
|
-
start_phased_restart
|
|
452
|
-
|
|
453
|
-
in_phased_restart =
|
|
458
|
+
if @pending_phased_restart
|
|
459
|
+
start_phased_restart(@pending_phased_restart == :refork)
|
|
460
|
+
|
|
461
|
+
in_phased_restart = @pending_phased_restart
|
|
462
|
+
@pending_phased_restart = false
|
|
463
|
+
|
|
454
464
|
workers_not_booted = @options[:workers]
|
|
465
|
+
# worker 0 is not restarted on refork
|
|
466
|
+
workers_not_booted -= 1 if in_phased_restart == :refork
|
|
455
467
|
end
|
|
456
468
|
|
|
457
|
-
check_workers
|
|
469
|
+
check_workers(in_phased_restart == :refork)
|
|
458
470
|
|
|
459
471
|
if read.wait_readable([0, @next_check - Time.now].max)
|
|
460
472
|
req = read.read_nonblock(1)
|
|
@@ -497,7 +509,7 @@ module Puma
|
|
|
497
509
|
end
|
|
498
510
|
|
|
499
511
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
|
500
|
-
@events.
|
|
512
|
+
@events.fire_after_booted!
|
|
501
513
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
502
514
|
booted = true
|
|
503
515
|
end
|
|
@@ -514,7 +526,7 @@ module Puma
|
|
|
514
526
|
end
|
|
515
527
|
|
|
516
528
|
if in_phased_restart && workers_not_booted.zero?
|
|
517
|
-
@events.
|
|
529
|
+
@events.fire_after_booted!
|
|
518
530
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
519
531
|
in_phased_restart = false
|
|
520
532
|
end
|
|
@@ -570,7 +582,9 @@ module Puma
|
|
|
570
582
|
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
|
571
583
|
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
|
572
584
|
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
|
573
|
-
if reaped_children.delete(w.pid) || Process.
|
|
585
|
+
if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
|
|
586
|
+
w.process_status = status
|
|
587
|
+
@config.run_hooks(:after_worker_shutdown, w, @log_writer)
|
|
574
588
|
true
|
|
575
589
|
else
|
|
576
590
|
w.term if w.term?
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puma
|
|
4
|
+
# Calculate a delay value for sleeping when running in clustered mode
|
|
5
|
+
#
|
|
6
|
+
# The main reason this is a class is so it can be unit tested independently.
|
|
7
|
+
# This makes modification easier in the future if we can encode properties of the
|
|
8
|
+
# delay into a test instead of relying on end-to-end testing only.
|
|
9
|
+
#
|
|
10
|
+
# This is an imprecise mechanism to address specific goals:
|
|
11
|
+
#
|
|
12
|
+
# - Evenly distribute requests across all workers at start
|
|
13
|
+
# - Evenly distribute CPU resources across all workers
|
|
14
|
+
#
|
|
15
|
+
# ## Goal: Distribute requests across workers at start
|
|
16
|
+
#
|
|
17
|
+
# There was a perf bug in Puma where one worker would wake up slightly before the rest and accept
|
|
18
|
+
# all the requests on the socket even though it didn't have enough resources to process all of them.
|
|
19
|
+
# This was originally fixed by never calling accept when a worker had more requests than threads
|
|
20
|
+
# already https://github.com/puma/puma/pull/3678/files/2736ebddb3fc8528e5150b5913fba251c37a8bf7#diff-a95f46e7ce116caddc9b9a9aa81004246d5210d5da5f4df90a818c780630166bL251-L291
|
|
21
|
+
#
|
|
22
|
+
# With the introduction of true keepalive support, there are two ways a request can come in:
|
|
23
|
+
# - A new request from a new client comes into the socket and it must be "accept"-ed
|
|
24
|
+
# - A keepalive request is served and the connection is retained. Another request is then accepted
|
|
25
|
+
#
|
|
26
|
+
# Ideally the server handles requests in the order they come in, and ideally it doesn't accept more requests than it can handle.
|
|
27
|
+
# These goals are contradictory, because when the server is at maximum capacity due to keepalive connections, it could mean we
|
|
28
|
+
# block all new requests, even if those came in before the new request on the older keepalive connection.
|
|
29
|
+
#
|
|
30
|
+
# ## Goal: Distribute CPU resources across all workers
|
|
31
|
+
#
|
|
32
|
+
# - This issue was opened https://github.com/puma/puma/issues/2078
|
|
33
|
+
#
|
|
34
|
+
# There are several entangled issues and it's not exactly clear what the root cause is, but the observable outcome
|
|
35
|
+
# was that performance was better with a small sleep, and that eventually became the default.
|
|
36
|
+
#
|
|
37
|
+
# An attempt to describe why this works is here: https://github.com/puma/puma/issues/2078#issuecomment-3287032470.
|
|
38
|
+
#
|
|
39
|
+
# Summarizing: The delay is for tuning the rate at which "accept" is called on the socket.
|
|
40
|
+
# Puma works by calling "accept" nonblock on the socket in a loop. When there are multiple workers
|
|
41
|
+
# (processes), they will "race" to accept a request at roughly the same rate. However, if one
|
|
42
|
+
# worker has all threads busy processing requests, then accepting a new request might "steal" it from
|
|
43
|
+
# a less busy worker. If a worker has no work to do, it should loop as fast as possible.
|
|
44
|
+
#
|
|
45
|
+
# ## Solution: Distribute requests across workers at start
|
|
46
|
+
#
|
|
47
|
+
# For now, both goals are framed as "load balancing" across workers (processes) and achieved through
|
|
48
|
+
# the same mechanism of sleeping longer to delay busier workers. Rather than the prior Puma 6.x
|
|
49
|
+
# and earlier behavior of using a binary on/off sleep value, we increase it an amount proportional
|
|
50
|
+
# to the load the server is under, capping the maximum delay to the scenario where all threads are busy
|
|
51
|
+
# and the todo list has reached a multiplier of the maximum number of threads.
|
|
52
|
+
#
|
|
53
|
+
# Private: API may change unexpectedly
|
|
54
|
+
class ClusterAcceptLoopDelay
|
|
55
|
+
attr_reader :max_delay
|
|
56
|
+
|
|
57
|
+
# Initialize happens once, `call` happens often. Perform global calculations here.
|
|
58
|
+
def initialize(
|
|
59
|
+
# Number of workers in the cluster
|
|
60
|
+
workers: ,
|
|
61
|
+
# Maximum delay in seconds i.e. 0.005 is 5 milliseconds
|
|
62
|
+
max_delay:
|
|
63
|
+
)
|
|
64
|
+
@on = max_delay > 0 && workers >= 2
|
|
65
|
+
@max_delay = max_delay.to_f
|
|
66
|
+
|
|
67
|
+
# Reach maximum delay when `max_threads * overload_multiplier` is reached in the system
|
|
68
|
+
@overload_multiplier = 25.0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on?
|
|
72
|
+
@on
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# We want the extreme values of this delay to be known (minimum and maximum) as well as
|
|
76
|
+
# a predictable curve between the two. i.e. no step functions or hard cliffs.
|
|
77
|
+
#
|
|
78
|
+
# Return value is always numeric. Returns 0 if there should be no delay.
|
|
79
|
+
def calculate(
|
|
80
|
+
# Number of threads working right now, plus number of requests in the todo list
|
|
81
|
+
busy_threads_plus_todo:,
|
|
82
|
+
# Maximum number of threads in the pool, note that the busy threads (alone) may go over this value at times
|
|
83
|
+
# if the pool needs to be reaped. The busy thread plus todo count may go over this value by a large amount.
|
|
84
|
+
max_threads:
|
|
85
|
+
)
|
|
86
|
+
max_value = @overload_multiplier * max_threads
|
|
87
|
+
# Approaches max delay when `busy_threads_plus_todo` approaches `max_value`
|
|
88
|
+
return max_delay * busy_threads_plus_todo.clamp(0, max_value) / max_value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/puma/commonlogger.rb
CHANGED
|
@@ -29,13 +29,13 @@ module Puma
|
|
|
29
29
|
|
|
30
30
|
CONTENT_LENGTH = 'Content-Length' # should be lower case from app,
|
|
31
31
|
# Util::HeaderHash allows mixed
|
|
32
|
-
HTTP_VERSION = Const::HTTP_VERSION
|
|
33
32
|
HTTP_X_FORWARDED_FOR = Const::HTTP_X_FORWARDED_FOR
|
|
34
33
|
PATH_INFO = Const::PATH_INFO
|
|
35
34
|
QUERY_STRING = Const::QUERY_STRING
|
|
36
35
|
REMOTE_ADDR = Const::REMOTE_ADDR
|
|
37
36
|
REMOTE_USER = 'REMOTE_USER'
|
|
38
37
|
REQUEST_METHOD = Const::REQUEST_METHOD
|
|
38
|
+
SERVER_PROTOCOL = Const::SERVER_PROTOCOL
|
|
39
39
|
|
|
40
40
|
def initialize(app, logger=nil)
|
|
41
41
|
@app = app
|
|
@@ -70,7 +70,7 @@ module Puma
|
|
|
70
70
|
env[REQUEST_METHOD],
|
|
71
71
|
env[PATH_INFO],
|
|
72
72
|
env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
|
|
73
|
-
env[
|
|
73
|
+
env[SERVER_PROTOCOL],
|
|
74
74
|
now - began_at ]
|
|
75
75
|
|
|
76
76
|
write(msg)
|
|
@@ -87,7 +87,7 @@ module Puma
|
|
|
87
87
|
env[REQUEST_METHOD],
|
|
88
88
|
env[PATH_INFO],
|
|
89
89
|
env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
|
|
90
|
-
env[
|
|
90
|
+
env[SERVER_PROTOCOL],
|
|
91
91
|
status.to_s[0..3],
|
|
92
92
|
length,
|
|
93
93
|
now - began_at ]
|