puma 6.4.3 → 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 +448 -8
- data/README.md +110 -51
- 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 +11 -1
- 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/java_options.md +54 -0
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +11 -16
- data/docs/plugins.md +6 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +11 -5
- data/docs/systemd.md +14 -5
- data/ext/puma_http11/extconf.rb +20 -32
- data/ext/puma_http11/http11_parser.java.rl +51 -65
- data/ext/puma_http11/mini_ssl.c +29 -9
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
- data/ext/puma_http11/puma_http11.c +125 -118
- data/lib/puma/app/status.rb +11 -3
- data/lib/puma/binder.rb +22 -12
- data/lib/puma/cli.rb +11 -9
- data/lib/puma/client.rb +233 -136
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +24 -21
- data/lib/puma/cluster/worker_handle.rb +38 -8
- data/lib/puma/cluster.rb +74 -48
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +197 -64
- data/lib/puma/const.rb +23 -12
- data/lib/puma/control_cli.rb +11 -7
- data/lib/puma/detect.rb +13 -0
- data/lib/puma/dsl.rb +483 -127
- data/lib/puma/error_logger.rb +7 -5
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/jruby_restart.rb +0 -16
- data/lib/puma/launcher/bundle_pruner.rb +3 -5
- data/lib/puma/launcher.rb +76 -59
- data/lib/puma/log_writer.rb +17 -11
- data/lib/puma/minissl/context_builder.rb +1 -0
- data/lib/puma/minissl.rb +1 -1
- data/lib/puma/null_io.rb +26 -0
- 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} +57 -209
- data/lib/puma/runner.rb +15 -17
- data/lib/puma/sd_notify.rb +1 -4
- data/lib/puma/server.rb +200 -104
- 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 +179 -96
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +11 -8
- data/tools/Dockerfile +15 -5
- metadata +26 -16
- data/ext/puma_http11/ext_help.h +0 -15
|
@@ -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 ]
|
data/lib/puma/configuration.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
4
6
|
require_relative 'plugin'
|
|
5
7
|
require_relative 'const'
|
|
6
8
|
require_relative 'dsl'
|
|
9
|
+
require_relative 'events'
|
|
7
10
|
|
|
8
11
|
module Puma
|
|
9
12
|
# A class used for storing "leveled" configuration options.
|
|
@@ -113,7 +116,7 @@ module Puma
|
|
|
113
116
|
# config = Configuration.new({}) do |user_config, file_config, default_config|
|
|
114
117
|
# user_config.port 3003
|
|
115
118
|
# end
|
|
116
|
-
# config.
|
|
119
|
+
# config.clamp
|
|
117
120
|
# puts config.options[:port]
|
|
118
121
|
# # => 3003
|
|
119
122
|
#
|
|
@@ -126,42 +129,48 @@ module Puma
|
|
|
126
129
|
# is done because an environment variable may have been modified while loading
|
|
127
130
|
# configuration files.
|
|
128
131
|
class Configuration
|
|
132
|
+
class NotLoadedError < StandardError; end
|
|
133
|
+
class NotClampedError < StandardError; end
|
|
134
|
+
|
|
129
135
|
DEFAULTS = {
|
|
130
136
|
auto_trim_time: 30,
|
|
131
|
-
binds: ['tcp://
|
|
132
|
-
clean_thread_locals: false,
|
|
137
|
+
binds: ['tcp://[::]:9292'.freeze],
|
|
133
138
|
debug: false,
|
|
134
139
|
early_hints: nil,
|
|
140
|
+
enable_keep_alives: true,
|
|
135
141
|
environment: 'development'.freeze,
|
|
142
|
+
fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
|
|
136
143
|
# Number of seconds to wait until we get the first data for the request.
|
|
137
144
|
first_data_timeout: 30,
|
|
145
|
+
force_shutdown_after: -1,
|
|
146
|
+
http_content_length_limit: nil,
|
|
138
147
|
# Number of seconds to wait until the next request before shutting down.
|
|
139
148
|
idle_timeout: nil,
|
|
140
149
|
io_selector_backend: :auto,
|
|
141
150
|
log_requests: false,
|
|
142
151
|
logger: STDOUT,
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
max_fast_inline: 10,
|
|
152
|
+
# Limits how many requests a keep alive connection can make.
|
|
153
|
+
# The connection will be closed after it reaches `max_keep_alive`
|
|
154
|
+
# requests.
|
|
155
|
+
max_io_threads: 0,
|
|
156
|
+
max_keep_alive: 999,
|
|
149
157
|
max_threads: Puma.mri? ? 5 : 16,
|
|
150
158
|
min_threads: 0,
|
|
151
159
|
mode: :http,
|
|
152
160
|
mutate_stdout_and_stderr_to_sync_on_write: true,
|
|
153
161
|
out_of_band: [],
|
|
154
162
|
# Number of seconds for another request within a persistent session.
|
|
155
|
-
persistent_timeout:
|
|
163
|
+
persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
|
|
164
|
+
prune_bundler: false,
|
|
156
165
|
queue_requests: true,
|
|
157
166
|
rackup: 'config.ru'.freeze,
|
|
158
167
|
raise_exception_on_sigterm: true,
|
|
159
168
|
reaping_time: 1,
|
|
160
169
|
remote_address: :socket,
|
|
161
|
-
silence_single_worker_warning: false,
|
|
162
170
|
silence_fork_callback_warning: false,
|
|
171
|
+
silence_single_worker_warning: false,
|
|
163
172
|
tag: File.basename(Dir.getwd),
|
|
164
|
-
tcp_host: '
|
|
173
|
+
tcp_host: '::'.freeze,
|
|
165
174
|
tcp_port: 9292,
|
|
166
175
|
wait_for_less_busy_worker: 0.005,
|
|
167
176
|
worker_boot_timeout: 60,
|
|
@@ -170,28 +179,36 @@ module Puma
|
|
|
170
179
|
worker_shutdown_timeout: 30,
|
|
171
180
|
worker_timeout: 60,
|
|
172
181
|
workers: 0,
|
|
173
|
-
http_content_length_limit: nil
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
def initialize(user_options={}, default_options = {}, &block)
|
|
177
|
-
default_options = self.puma_default_options.merge(default_options)
|
|
184
|
+
def initialize(user_options={}, default_options = {}, env = ENV, &block)
|
|
185
|
+
default_options = self.puma_default_options(env).merge(events: Events.new).merge(default_options)
|
|
178
186
|
|
|
179
|
-
@
|
|
187
|
+
@_options = UserFileDefaultOptions.new(user_options, default_options)
|
|
180
188
|
@plugins = PluginLoader.new
|
|
181
|
-
@
|
|
182
|
-
@
|
|
183
|
-
@
|
|
189
|
+
@events = @_options[:events] || Events.new
|
|
190
|
+
@hooks = {}
|
|
191
|
+
@user_dsl = DSL.new(@_options.user_options, self)
|
|
192
|
+
@file_dsl = DSL.new(@_options.file_options, self)
|
|
193
|
+
@default_dsl = DSL.new(@_options.default_options, self)
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
|
|
187
|
-
end
|
|
195
|
+
@puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
|
|
188
196
|
|
|
189
197
|
if block
|
|
190
198
|
configure(&block)
|
|
191
199
|
end
|
|
200
|
+
|
|
201
|
+
@loaded = false
|
|
202
|
+
@clamped = false
|
|
192
203
|
end
|
|
193
204
|
|
|
194
|
-
attr_reader :
|
|
205
|
+
attr_reader :plugins, :events, :hooks, :_options
|
|
206
|
+
|
|
207
|
+
def options
|
|
208
|
+
raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
|
|
209
|
+
|
|
210
|
+
@_options
|
|
211
|
+
end
|
|
195
212
|
|
|
196
213
|
def configure
|
|
197
214
|
yield @user_dsl, @file_dsl, @default_dsl
|
|
@@ -204,7 +221,7 @@ module Puma
|
|
|
204
221
|
def initialize_copy(other)
|
|
205
222
|
@conf = nil
|
|
206
223
|
@cli_options = nil
|
|
207
|
-
@
|
|
224
|
+
@_options = @_options.dup
|
|
208
225
|
end
|
|
209
226
|
|
|
210
227
|
def flatten
|
|
@@ -212,42 +229,49 @@ module Puma
|
|
|
212
229
|
end
|
|
213
230
|
|
|
214
231
|
def flatten!
|
|
215
|
-
@
|
|
232
|
+
@_options = @_options.flatten
|
|
216
233
|
self
|
|
217
234
|
end
|
|
218
235
|
|
|
219
|
-
def puma_default_options
|
|
236
|
+
def puma_default_options(env = ENV)
|
|
220
237
|
defaults = DEFAULTS.dup
|
|
221
|
-
|
|
238
|
+
defaults[:tcp_host] = self.class.default_tcp_host
|
|
239
|
+
defaults[:binds] = [self.class.default_tcp_bind]
|
|
240
|
+
puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
|
|
222
241
|
defaults
|
|
223
242
|
end
|
|
224
243
|
|
|
225
|
-
def puma_options_from_env
|
|
226
|
-
min =
|
|
227
|
-
max =
|
|
228
|
-
|
|
244
|
+
def puma_options_from_env(env = ENV)
|
|
245
|
+
min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
|
|
246
|
+
max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
|
|
247
|
+
persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
|
|
248
|
+
workers_env = env['WEB_CONCURRENCY']
|
|
249
|
+
workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
|
|
229
250
|
|
|
230
251
|
{
|
|
231
|
-
min_threads: min && Integer(min),
|
|
232
|
-
max_threads: max && Integer(max),
|
|
233
|
-
|
|
234
|
-
|
|
252
|
+
min_threads: min && min != "" && Integer(min),
|
|
253
|
+
max_threads: max && max != "" && Integer(max),
|
|
254
|
+
persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
|
|
255
|
+
workers: workers,
|
|
256
|
+
environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
|
|
235
257
|
}
|
|
236
258
|
end
|
|
237
259
|
|
|
238
260
|
def load
|
|
261
|
+
@loaded = true
|
|
239
262
|
config_files.each { |config_file| @file_dsl._load_from(config_file) }
|
|
240
|
-
|
|
241
|
-
@options
|
|
263
|
+
@_options
|
|
242
264
|
end
|
|
243
265
|
|
|
244
266
|
def config_files
|
|
245
|
-
|
|
267
|
+
raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
|
|
268
|
+
|
|
269
|
+
files = @_options.all_of(:config_files)
|
|
246
270
|
|
|
247
271
|
return [] if files == ['-']
|
|
248
272
|
return files if files.any?
|
|
249
273
|
|
|
250
|
-
first_default_file = %W(config/puma/#{@
|
|
274
|
+
first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
|
|
251
275
|
File.exist?(f)
|
|
252
276
|
end
|
|
253
277
|
|
|
@@ -255,9 +279,18 @@ module Puma
|
|
|
255
279
|
end
|
|
256
280
|
|
|
257
281
|
# Call once all configuration (included from rackup files)
|
|
258
|
-
# is loaded to
|
|
282
|
+
# is loaded to finalize defaults and lock in the configuration.
|
|
283
|
+
#
|
|
284
|
+
# This also calls load if it hasn't been called yet.
|
|
259
285
|
def clamp
|
|
260
|
-
@
|
|
286
|
+
load unless @loaded
|
|
287
|
+
run_mode_hooks
|
|
288
|
+
set_conditional_default_options
|
|
289
|
+
@_options.finalize_values
|
|
290
|
+
rewrite_unavailable_ipv6_binds!
|
|
291
|
+
@clamped = true
|
|
292
|
+
warn_hooks
|
|
293
|
+
options
|
|
261
294
|
end
|
|
262
295
|
|
|
263
296
|
# Injects the Configuration object into the env
|
|
@@ -276,11 +309,11 @@ module Puma
|
|
|
276
309
|
# Indicate if there is a properly configured app
|
|
277
310
|
#
|
|
278
311
|
def app_configured?
|
|
279
|
-
|
|
312
|
+
options[:app] || File.exist?(rackup)
|
|
280
313
|
end
|
|
281
314
|
|
|
282
315
|
def rackup
|
|
283
|
-
|
|
316
|
+
options[:rackup]
|
|
284
317
|
end
|
|
285
318
|
|
|
286
319
|
# Load the specified rackup file, pull options from
|
|
@@ -289,9 +322,9 @@ module Puma
|
|
|
289
322
|
def app
|
|
290
323
|
found = options[:app] || load_rackup
|
|
291
324
|
|
|
292
|
-
if
|
|
325
|
+
if options[:log_requests]
|
|
293
326
|
require_relative 'commonlogger'
|
|
294
|
-
logger =
|
|
327
|
+
logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
|
|
295
328
|
found = CommonLogger.new(found, logger)
|
|
296
329
|
end
|
|
297
330
|
|
|
@@ -300,7 +333,7 @@ module Puma
|
|
|
300
333
|
|
|
301
334
|
# Return which environment we're running in
|
|
302
335
|
def environment
|
|
303
|
-
|
|
336
|
+
options[:environment]
|
|
304
337
|
end
|
|
305
338
|
|
|
306
339
|
def load_plugin(name)
|
|
@@ -308,16 +341,19 @@ module Puma
|
|
|
308
341
|
end
|
|
309
342
|
|
|
310
343
|
# @param key [:Symbol] hook to run
|
|
311
|
-
# @param arg [Launcher, Int] `:
|
|
344
|
+
# @param arg [Launcher, Int] `:before_restart` passes Launcher
|
|
312
345
|
#
|
|
313
346
|
def run_hooks(key, arg, log_writer, hook_data = nil)
|
|
314
|
-
|
|
347
|
+
log_writer.debug { "Running #{key} hooks" }
|
|
348
|
+
|
|
349
|
+
options.all_of(key).each do |hook_options|
|
|
315
350
|
begin
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
351
|
+
block = hook_options[:block]
|
|
352
|
+
if id = hook_options[:id]
|
|
353
|
+
hook_data[id] ||= Hash.new
|
|
354
|
+
block.call arg, hook_data[id]
|
|
319
355
|
else
|
|
320
|
-
|
|
356
|
+
block.call arg
|
|
321
357
|
end
|
|
322
358
|
rescue => e
|
|
323
359
|
log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
|
|
@@ -327,7 +363,24 @@ module Puma
|
|
|
327
363
|
end
|
|
328
364
|
|
|
329
365
|
def final_options
|
|
330
|
-
|
|
366
|
+
options.final_options
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def self.default_tcp_host
|
|
370
|
+
ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def self.default_tcp_bind(port = DEFAULTS[:tcp_port])
|
|
374
|
+
URI::Generic.build(scheme: 'tcp', host: default_tcp_host, port: Integer(port)).to_s
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def self.ipv6_interface_available?
|
|
378
|
+
Socket.getifaddrs.any? do |ifaddr|
|
|
379
|
+
addr = ifaddr.addr
|
|
380
|
+
addr&.ipv6? && !addr&.ipv6_loopback?
|
|
381
|
+
end
|
|
382
|
+
rescue StandardError
|
|
383
|
+
false
|
|
331
384
|
end
|
|
332
385
|
|
|
333
386
|
def self.temp_path
|
|
@@ -337,14 +390,66 @@ module Puma
|
|
|
337
390
|
"#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
|
|
338
391
|
end
|
|
339
392
|
|
|
393
|
+
def self.random_token
|
|
394
|
+
require 'securerandom' unless defined?(SecureRandom)
|
|
395
|
+
|
|
396
|
+
SecureRandom.hex(16)
|
|
397
|
+
end
|
|
398
|
+
|
|
340
399
|
private
|
|
341
400
|
|
|
401
|
+
def rewrite_unavailable_ipv6_binds!
|
|
402
|
+
return if self.class.ipv6_interface_available?
|
|
403
|
+
|
|
404
|
+
tried_ipv6_bind = false
|
|
405
|
+
bind_schemes = ['tcp', 'ssl']
|
|
406
|
+
|
|
407
|
+
@_options[:binds] = Array(@_options[:binds]).map do |bind|
|
|
408
|
+
uri = URI.parse(bind)
|
|
409
|
+
next bind unless bind_schemes.include?(uri.scheme)
|
|
410
|
+
|
|
411
|
+
host = uri.host&.delete_prefix('[')&.delete_suffix(']')
|
|
412
|
+
next bind unless host&.include?(':')
|
|
413
|
+
|
|
414
|
+
tried_ipv6_bind = true
|
|
415
|
+
next bind unless host == Const::UNSPECIFIED_IPV6
|
|
416
|
+
|
|
417
|
+
uri.host = Const::UNSPECIFIED_IPV4
|
|
418
|
+
uri.to_s
|
|
419
|
+
rescue URI::InvalidURIError
|
|
420
|
+
bind
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
warn "WARNING: IPv6 bind requested but no non-loopback IPv6 interface was detected" if tried_ipv6_bind
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def require_processor_counter
|
|
427
|
+
require 'concurrent/utility/processor_counter'
|
|
428
|
+
rescue LoadError
|
|
429
|
+
warn <<~MESSAGE
|
|
430
|
+
WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
|
|
431
|
+
Please add "concurrent-ruby" to your Gemfile.
|
|
432
|
+
MESSAGE
|
|
433
|
+
raise
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def parse_workers(value)
|
|
437
|
+
if value == :auto || value == 'auto'
|
|
438
|
+
require_processor_counter
|
|
439
|
+
Integer(::Concurrent.available_processor_count)
|
|
440
|
+
else
|
|
441
|
+
Integer(value)
|
|
442
|
+
end
|
|
443
|
+
rescue ArgumentError, TypeError
|
|
444
|
+
raise ArgumentError, "workers must be an Integer or :auto"
|
|
445
|
+
end
|
|
446
|
+
|
|
342
447
|
# Load and use the normal Rack builder if we can, otherwise
|
|
343
448
|
# fallback to our minimal version.
|
|
344
449
|
def rack_builder
|
|
345
450
|
# Load bundler now if we can so that we can pickup rack from
|
|
346
451
|
# a Gemfile
|
|
347
|
-
if
|
|
452
|
+
if @puma_bundler_pruned
|
|
348
453
|
begin
|
|
349
454
|
require 'bundler/setup'
|
|
350
455
|
rescue LoadError
|
|
@@ -354,11 +459,10 @@ module Puma
|
|
|
354
459
|
begin
|
|
355
460
|
require 'rack'
|
|
356
461
|
require 'rack/builder'
|
|
462
|
+
::Rack::Builder
|
|
357
463
|
rescue LoadError
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
else
|
|
361
|
-
return ::Rack::Builder
|
|
464
|
+
require_relative 'rack/builder'
|
|
465
|
+
Puma::Rack::Builder
|
|
362
466
|
end
|
|
363
467
|
end
|
|
364
468
|
|
|
@@ -368,22 +472,51 @@ module Puma
|
|
|
368
472
|
rack_app, rack_options = rack_builder.parse_file(rackup)
|
|
369
473
|
rack_options = rack_options || {}
|
|
370
474
|
|
|
371
|
-
|
|
475
|
+
options.file_options.merge!(rack_options)
|
|
372
476
|
|
|
373
477
|
config_ru_binds = []
|
|
374
478
|
rack_options.each do |k, v|
|
|
375
479
|
config_ru_binds << v if k.to_s.start_with?("bind")
|
|
376
480
|
end
|
|
377
481
|
|
|
378
|
-
|
|
482
|
+
options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
|
|
379
483
|
|
|
380
484
|
rack_app
|
|
381
485
|
end
|
|
382
486
|
|
|
383
|
-
def
|
|
384
|
-
|
|
487
|
+
def run_mode_hooks
|
|
488
|
+
workers_before = @_options[:workers]
|
|
489
|
+
key = workers_before > 0 ? :cluster : :single
|
|
385
490
|
|
|
386
|
-
|
|
491
|
+
@_options.all_of(key).each(&:call)
|
|
492
|
+
|
|
493
|
+
unless @_options[:workers] == workers_before
|
|
494
|
+
raise "cannot change the number of workers inside a #{key} configuration hook"
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def set_conditional_default_options
|
|
499
|
+
@_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
|
|
500
|
+
(@_options[:workers] > 1) && Puma.forkable?
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def warn_hooks
|
|
504
|
+
return if options[:workers] > 0
|
|
505
|
+
return if options[:silence_fork_callback_warning]
|
|
506
|
+
|
|
507
|
+
log_writer = LogWriter.stdio
|
|
508
|
+
@hooks.each_key do |hook|
|
|
509
|
+
options.all_of(hook).each do |hook_options|
|
|
510
|
+
next unless hook_options[:cluster_only]
|
|
511
|
+
|
|
512
|
+
log_writer.log(<<~MSG.tr("\n", " "))
|
|
513
|
+
Warning: The code in the `#{hook}` block will not execute
|
|
514
|
+
in the current Puma configuration. The `#{hook}` block only
|
|
515
|
+
executes in Puma's cluster mode. To fix this, either remove the
|
|
516
|
+
`#{hook}` call or increase Puma's worker count above zero.
|
|
517
|
+
MSG
|
|
518
|
+
end
|
|
519
|
+
end
|
|
387
520
|
end
|
|
388
521
|
end
|
|
389
522
|
end
|
data/lib/puma/const.rb
CHANGED
|
@@ -100,13 +100,11 @@ module Puma
|
|
|
100
100
|
# too taxing on performance.
|
|
101
101
|
module Const
|
|
102
102
|
|
|
103
|
-
PUMA_VERSION = VERSION = "
|
|
104
|
-
CODE_NAME = "
|
|
103
|
+
PUMA_VERSION = VERSION = "8.0.2"
|
|
104
|
+
CODE_NAME = "Into the Arena"
|
|
105
105
|
|
|
106
106
|
PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
|
|
107
107
|
|
|
108
|
-
FAST_TRACK_KA_TIMEOUT = 0.2
|
|
109
|
-
|
|
110
108
|
# How long to wait when getting some write blocking on the socket when
|
|
111
109
|
# sending data back
|
|
112
110
|
WRITE_TIMEOUT = 10
|
|
@@ -125,9 +123,9 @@ module Puma
|
|
|
125
123
|
# Indicate that we couldn't parse the request
|
|
126
124
|
400 => "HTTP/1.1 400 Bad Request\r\n\r\n",
|
|
127
125
|
# The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
|
|
128
|
-
404 => "HTTP/1.1 404 Not Found\r\
|
|
126
|
+
404 => "HTTP/1.1 404 Not Found\r\nconnection: close\r\n\r\n",
|
|
129
127
|
# The standard empty 408 response for requests that timed out.
|
|
130
|
-
408 => "HTTP/1.1 408 Request Timeout\r\
|
|
128
|
+
408 => "HTTP/1.1 408 Request Timeout\r\nconnection: close\r\n\r\n",
|
|
131
129
|
# Indicate that there was an internal error, obviously.
|
|
132
130
|
500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n",
|
|
133
131
|
# Incorrect or invalid header value
|
|
@@ -137,7 +135,7 @@ module Puma
|
|
|
137
135
|
}.freeze
|
|
138
136
|
|
|
139
137
|
# The basic max request size we'll try to read.
|
|
140
|
-
CHUNK_SIZE =
|
|
138
|
+
CHUNK_SIZE = 64 * 1024
|
|
141
139
|
|
|
142
140
|
# This is the maximum header that is allowed before a client is booted. The parser detects
|
|
143
141
|
# this, but we'd also like to do this as well.
|
|
@@ -230,6 +228,7 @@ module Puma
|
|
|
230
228
|
RACK_INPUT = "rack.input"
|
|
231
229
|
RACK_URL_SCHEME = "rack.url_scheme"
|
|
232
230
|
RACK_AFTER_REPLY = "rack.after_reply"
|
|
231
|
+
RACK_RESPONSE_FINISHED = "rack.response_finished"
|
|
233
232
|
PUMA_SOCKET = "puma.socket"
|
|
234
233
|
PUMA_CONFIG = "puma.config"
|
|
235
234
|
PUMA_PEERCERT = "puma.peercert"
|
|
@@ -252,14 +251,14 @@ module Puma
|
|
|
252
251
|
KEEP_ALIVE = "keep-alive"
|
|
253
252
|
|
|
254
253
|
CONTENT_LENGTH2 = "content-length"
|
|
255
|
-
CONTENT_LENGTH_S = "
|
|
254
|
+
CONTENT_LENGTH_S = "content-length: "
|
|
256
255
|
TRANSFER_ENCODING = "transfer-encoding"
|
|
257
256
|
TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING"
|
|
258
257
|
|
|
259
|
-
CONNECTION_CLOSE = "
|
|
260
|
-
CONNECTION_KEEP_ALIVE = "
|
|
258
|
+
CONNECTION_CLOSE = "connection: close\r\n"
|
|
259
|
+
CONNECTION_KEEP_ALIVE = "connection: keep-alive\r\n"
|
|
261
260
|
|
|
262
|
-
TRANSFER_ENCODING_CHUNKED = "
|
|
261
|
+
TRANSFER_ENCODING_CHUNKED = "transfer-encoding: chunked\r\n"
|
|
263
262
|
CLOSE_CHUNKED = "0\r\n\r\n"
|
|
264
263
|
|
|
265
264
|
CHUNKED = "chunked"
|
|
@@ -292,6 +291,18 @@ module Puma
|
|
|
292
291
|
# Banned keys of response header
|
|
293
292
|
BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
|
|
294
293
|
|
|
295
|
-
PROXY_PROTOCOL_V1_REGEX =
|
|
294
|
+
PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
|
|
295
|
+
PROXY_PROTOCOL_V1_MAX_LENGTH = 107
|
|
296
|
+
|
|
297
|
+
# All constants are prefixed with `PIPE_` to avoid name collisions.
|
|
298
|
+
module PipeRequest
|
|
299
|
+
PIPE_WAKEUP = "!"
|
|
300
|
+
PIPE_BOOT = "b"
|
|
301
|
+
PIPE_FORK = "f"
|
|
302
|
+
PIPE_EXTERNAL_TERM = "e"
|
|
303
|
+
PIPE_TERM = "t"
|
|
304
|
+
PIPE_PING = "p"
|
|
305
|
+
PIPE_IDLE = "i"
|
|
306
|
+
end
|
|
296
307
|
end
|
|
297
308
|
end
|