puma 6.6.0 → 7.1.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 +170 -5
- data/README.md +24 -32
- data/docs/fork_worker.md +5 -5
- data/docs/kubernetes.md +8 -6
- data/docs/restart.md +2 -2
- data/docs/signals.md +11 -11
- data/docs/stats.md +3 -2
- data/docs/systemd.md +1 -1
- data/ext/puma_http11/extconf.rb +2 -17
- data/ext/puma_http11/mini_ssl.c +18 -8
- data/ext/puma_http11/org/jruby/puma/Http11.java +10 -2
- data/ext/puma_http11/puma_http11.c +23 -11
- data/lib/puma/binder.rb +10 -8
- data/lib/puma/cli.rb +3 -5
- data/lib/puma/client.rb +95 -61
- data/lib/puma/cluster/worker.rb +9 -10
- data/lib/puma/cluster/worker_handle.rb +38 -7
- data/lib/puma/cluster.rb +41 -26
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +89 -43
- data/lib/puma/const.rb +9 -10
- data/lib/puma/control_cli.rb +6 -2
- data/lib/puma/detect.rb +2 -0
- data/lib/puma/dsl.rb +135 -94
- 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 +1 -1
- data/lib/puma/launcher.rb +52 -48
- 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 -4
- data/lib/puma/request.rb +45 -32
- data/lib/puma/runner.rb +8 -17
- data/lib/puma/server.rb +111 -61
- data/lib/puma/single.rb +5 -2
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +47 -82
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +2 -2
- data/tools/Dockerfile +3 -1
- metadata +6 -4
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.
|
|
@@ -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]
|
|
@@ -184,7 +189,12 @@ module Puma
|
|
|
184
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}..."
|
|
@@ -228,7 +238,7 @@ module Puma
|
|
|
228
238
|
def phased_restart(refork = false)
|
|
229
239
|
return false if @options[:preload_app] && !refork
|
|
230
240
|
|
|
231
|
-
@
|
|
241
|
+
@pending_phased_restart = refork ? :refork : true
|
|
232
242
|
wakeup!
|
|
233
243
|
|
|
234
244
|
true
|
|
@@ -258,11 +268,14 @@ module Puma
|
|
|
258
268
|
end
|
|
259
269
|
|
|
260
270
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
|
261
|
-
# the master process.
|
|
271
|
+
# the master process. Calling this also resets stat 'max' values to zero.
|
|
262
272
|
# @!attribute [r] stats
|
|
273
|
+
# @return [Hash]
|
|
274
|
+
|
|
263
275
|
def stats
|
|
264
276
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
|
265
277
|
worker_status = @workers.map do |w|
|
|
278
|
+
w.reset_max
|
|
266
279
|
{
|
|
267
280
|
started_at: utc_iso8601(w.started_at),
|
|
268
281
|
pid: w.pid,
|
|
@@ -273,7 +286,6 @@ module Puma
|
|
|
273
286
|
last_status: w.last_status,
|
|
274
287
|
}
|
|
275
288
|
end
|
|
276
|
-
|
|
277
289
|
{
|
|
278
290
|
started_at: utc_iso8601(@started_at),
|
|
279
291
|
workers: @workers.size,
|
|
@@ -342,7 +354,7 @@ module Puma
|
|
|
342
354
|
|
|
343
355
|
stop_workers
|
|
344
356
|
stop
|
|
345
|
-
@events.
|
|
357
|
+
@events.fire_after_stopped!
|
|
346
358
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
|
347
359
|
exit 0 # Clean exit, workers were stopped
|
|
348
360
|
end
|
|
@@ -359,16 +371,12 @@ module Puma
|
|
|
359
371
|
|
|
360
372
|
if preload?
|
|
361
373
|
# 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) }
|
|
374
|
+
# but may be used by anyone.
|
|
375
|
+
fork_safe = ->(t) { t.thread_variable_get(:fork_safe) }
|
|
368
376
|
|
|
369
377
|
before = Thread.list.reject(&fork_safe)
|
|
370
378
|
|
|
371
|
-
log "* Restarts: (\u2714) hot (\u2716) phased"
|
|
379
|
+
log "* Restarts: (\u2714) hot (\u2716) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
372
380
|
log "* Preloading application"
|
|
373
381
|
load_and_bind
|
|
374
382
|
|
|
@@ -386,7 +394,7 @@ module Puma
|
|
|
386
394
|
end
|
|
387
395
|
end
|
|
388
396
|
else
|
|
389
|
-
log "* Restarts: (\u2714) hot (\u2714) phased"
|
|
397
|
+
log "* Restarts: (\u2714) hot (\u2714) phased (#{@options[:fork_worker] ? "\u2714" : "\u2716"}) refork"
|
|
390
398
|
|
|
391
399
|
unless @config.app_configured?
|
|
392
400
|
error "No application configured, nothing to run"
|
|
@@ -413,6 +421,7 @@ module Puma
|
|
|
413
421
|
|
|
414
422
|
log "Use Ctrl-C to stop"
|
|
415
423
|
|
|
424
|
+
warn_ruby_mn_threads
|
|
416
425
|
single_worker_warning
|
|
417
426
|
|
|
418
427
|
redirect_io
|
|
@@ -447,14 +456,18 @@ module Puma
|
|
|
447
456
|
break
|
|
448
457
|
end
|
|
449
458
|
|
|
450
|
-
if @
|
|
451
|
-
start_phased_restart
|
|
452
|
-
|
|
453
|
-
in_phased_restart =
|
|
459
|
+
if @pending_phased_restart
|
|
460
|
+
start_phased_restart(@pending_phased_restart == :refork)
|
|
461
|
+
|
|
462
|
+
in_phased_restart = @pending_phased_restart
|
|
463
|
+
@pending_phased_restart = false
|
|
464
|
+
|
|
454
465
|
workers_not_booted = @options[:workers]
|
|
466
|
+
# worker 0 is not restarted on refork
|
|
467
|
+
workers_not_booted -= 1 if in_phased_restart == :refork
|
|
455
468
|
end
|
|
456
469
|
|
|
457
|
-
check_workers
|
|
470
|
+
check_workers(in_phased_restart == :refork)
|
|
458
471
|
|
|
459
472
|
if read.wait_readable([0, @next_check - Time.now].max)
|
|
460
473
|
req = read.read_nonblock(1)
|
|
@@ -497,7 +510,7 @@ module Puma
|
|
|
497
510
|
end
|
|
498
511
|
|
|
499
512
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
|
500
|
-
@events.
|
|
513
|
+
@events.fire_after_booted!
|
|
501
514
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
502
515
|
booted = true
|
|
503
516
|
end
|
|
@@ -514,7 +527,7 @@ module Puma
|
|
|
514
527
|
end
|
|
515
528
|
|
|
516
529
|
if in_phased_restart && workers_not_booted.zero?
|
|
517
|
-
@events.
|
|
530
|
+
@events.fire_after_booted!
|
|
518
531
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
519
532
|
in_phased_restart = false
|
|
520
533
|
end
|
|
@@ -570,7 +583,9 @@ module Puma
|
|
|
570
583
|
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
|
571
584
|
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
|
572
585
|
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
|
573
|
-
if reaped_children.delete(w.pid) || Process.
|
|
586
|
+
if (status = reaped_children.delete(w.pid) || Process.wait2(w.pid, Process::WNOHANG)&.last)
|
|
587
|
+
w.process_status = status
|
|
588
|
+
@config.run_hooks(:after_worker_shutdown, w, @log_writer)
|
|
574
589
|
true
|
|
575
590
|
else
|
|
576
591
|
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 ]
|
data/lib/puma/configuration.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'plugin'
|
|
4
4
|
require_relative 'const'
|
|
5
5
|
require_relative 'dsl'
|
|
6
|
+
require_relative 'events'
|
|
6
7
|
|
|
7
8
|
module Puma
|
|
8
9
|
# A class used for storing "leveled" configuration options.
|
|
@@ -112,7 +113,7 @@ module Puma
|
|
|
112
113
|
# config = Configuration.new({}) do |user_config, file_config, default_config|
|
|
113
114
|
# user_config.port 3003
|
|
114
115
|
# end
|
|
115
|
-
# config.
|
|
116
|
+
# config.clamp
|
|
116
117
|
# puts config.options[:port]
|
|
117
118
|
# # => 3003
|
|
118
119
|
#
|
|
@@ -125,10 +126,13 @@ module Puma
|
|
|
125
126
|
# is done because an environment variable may have been modified while loading
|
|
126
127
|
# configuration files.
|
|
127
128
|
class Configuration
|
|
129
|
+
class NotLoadedError < StandardError; end
|
|
130
|
+
class NotClampedError < StandardError; end
|
|
131
|
+
|
|
128
132
|
DEFAULTS = {
|
|
129
133
|
auto_trim_time: 30,
|
|
130
134
|
binds: ['tcp://0.0.0.0:9292'.freeze],
|
|
131
|
-
|
|
135
|
+
fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
|
|
132
136
|
debug: false,
|
|
133
137
|
enable_keep_alives: true,
|
|
134
138
|
early_hints: nil,
|
|
@@ -140,19 +144,18 @@ module Puma
|
|
|
140
144
|
io_selector_backend: :auto,
|
|
141
145
|
log_requests: false,
|
|
142
146
|
logger: STDOUT,
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
# well behaved client from monopolizing the thread forever.
|
|
148
|
-
max_fast_inline: 10,
|
|
147
|
+
# Limits how many requests a keep alive connection can make.
|
|
148
|
+
# The connection will be closed after it reaches `max_keep_alive`
|
|
149
|
+
# requests.
|
|
150
|
+
max_keep_alive: 999,
|
|
149
151
|
max_threads: Puma.mri? ? 5 : 16,
|
|
150
152
|
min_threads: 0,
|
|
151
153
|
mode: :http,
|
|
152
154
|
mutate_stdout_and_stderr_to_sync_on_write: true,
|
|
153
155
|
out_of_band: [],
|
|
154
156
|
# Number of seconds for another request within a persistent session.
|
|
155
|
-
persistent_timeout:
|
|
157
|
+
persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
|
|
158
|
+
prune_bundler: false,
|
|
156
159
|
queue_requests: true,
|
|
157
160
|
rackup: 'config.ru'.freeze,
|
|
158
161
|
raise_exception_on_sigterm: true,
|
|
@@ -176,24 +179,31 @@ module Puma
|
|
|
176
179
|
def initialize(user_options={}, default_options = {}, env = ENV, &block)
|
|
177
180
|
default_options = self.puma_default_options(env).merge(default_options)
|
|
178
181
|
|
|
179
|
-
@
|
|
182
|
+
@_options = UserFileDefaultOptions.new(user_options, default_options)
|
|
180
183
|
@plugins = PluginLoader.new
|
|
181
|
-
@
|
|
182
|
-
@
|
|
183
|
-
@
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
|
|
187
|
-
end
|
|
184
|
+
@events = @_options[:events] || Events.new
|
|
185
|
+
@hooks = {}
|
|
186
|
+
@user_dsl = DSL.new(@_options.user_options, self)
|
|
187
|
+
@file_dsl = DSL.new(@_options.file_options, self)
|
|
188
|
+
@default_dsl = DSL.new(@_options.default_options, self)
|
|
188
189
|
|
|
189
190
|
@puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
|
|
190
191
|
|
|
191
192
|
if block
|
|
192
193
|
configure(&block)
|
|
193
194
|
end
|
|
195
|
+
|
|
196
|
+
@loaded = false
|
|
197
|
+
@clamped = false
|
|
194
198
|
end
|
|
195
199
|
|
|
196
|
-
attr_reader :
|
|
200
|
+
attr_reader :plugins, :events, :hooks
|
|
201
|
+
|
|
202
|
+
def options
|
|
203
|
+
raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
|
|
204
|
+
|
|
205
|
+
@_options
|
|
206
|
+
end
|
|
197
207
|
|
|
198
208
|
def configure
|
|
199
209
|
yield @user_dsl, @file_dsl, @default_dsl
|
|
@@ -206,7 +216,7 @@ module Puma
|
|
|
206
216
|
def initialize_copy(other)
|
|
207
217
|
@conf = nil
|
|
208
218
|
@cli_options = nil
|
|
209
|
-
@
|
|
219
|
+
@_options = @_options.dup
|
|
210
220
|
end
|
|
211
221
|
|
|
212
222
|
def flatten
|
|
@@ -214,7 +224,7 @@ module Puma
|
|
|
214
224
|
end
|
|
215
225
|
|
|
216
226
|
def flatten!
|
|
217
|
-
@
|
|
227
|
+
@_options = @_options.flatten
|
|
218
228
|
self
|
|
219
229
|
end
|
|
220
230
|
|
|
@@ -227,6 +237,7 @@ module Puma
|
|
|
227
237
|
def puma_options_from_env(env = ENV)
|
|
228
238
|
min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
|
|
229
239
|
max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
|
|
240
|
+
persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
|
|
230
241
|
workers = if env['WEB_CONCURRENCY'] == 'auto'
|
|
231
242
|
require_processor_counter
|
|
232
243
|
::Concurrent.available_processor_count
|
|
@@ -237,24 +248,27 @@ module Puma
|
|
|
237
248
|
{
|
|
238
249
|
min_threads: min && min != "" && Integer(min),
|
|
239
250
|
max_threads: max && max != "" && Integer(max),
|
|
251
|
+
persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
|
|
240
252
|
workers: workers && workers != "" && Integer(workers),
|
|
241
253
|
environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
|
|
242
254
|
}
|
|
243
255
|
end
|
|
244
256
|
|
|
245
257
|
def load
|
|
258
|
+
@loaded = true
|
|
246
259
|
config_files.each { |config_file| @file_dsl._load_from(config_file) }
|
|
247
|
-
|
|
248
|
-
@options
|
|
260
|
+
@_options
|
|
249
261
|
end
|
|
250
262
|
|
|
251
263
|
def config_files
|
|
252
|
-
|
|
264
|
+
raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
|
|
265
|
+
|
|
266
|
+
files = @_options.all_of(:config_files)
|
|
253
267
|
|
|
254
268
|
return [] if files == ['-']
|
|
255
269
|
return files if files.any?
|
|
256
270
|
|
|
257
|
-
first_default_file = %W(config/puma/#{@
|
|
271
|
+
first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
|
|
258
272
|
File.exist?(f)
|
|
259
273
|
end
|
|
260
274
|
|
|
@@ -262,9 +276,16 @@ module Puma
|
|
|
262
276
|
end
|
|
263
277
|
|
|
264
278
|
# Call once all configuration (included from rackup files)
|
|
265
|
-
# is loaded to
|
|
279
|
+
# is loaded to finalize defaults and lock in the configuration.
|
|
280
|
+
#
|
|
281
|
+
# This also calls load if it hasn't been called yet.
|
|
266
282
|
def clamp
|
|
267
|
-
@
|
|
283
|
+
load unless @loaded
|
|
284
|
+
set_conditional_default_options
|
|
285
|
+
@_options.finalize_values
|
|
286
|
+
@clamped = true
|
|
287
|
+
warn_hooks
|
|
288
|
+
options
|
|
268
289
|
end
|
|
269
290
|
|
|
270
291
|
# Injects the Configuration object into the env
|
|
@@ -283,11 +304,11 @@ module Puma
|
|
|
283
304
|
# Indicate if there is a properly configured app
|
|
284
305
|
#
|
|
285
306
|
def app_configured?
|
|
286
|
-
|
|
307
|
+
options[:app] || File.exist?(rackup)
|
|
287
308
|
end
|
|
288
309
|
|
|
289
310
|
def rackup
|
|
290
|
-
|
|
311
|
+
options[:rackup]
|
|
291
312
|
end
|
|
292
313
|
|
|
293
314
|
# Load the specified rackup file, pull options from
|
|
@@ -296,9 +317,9 @@ module Puma
|
|
|
296
317
|
def app
|
|
297
318
|
found = options[:app] || load_rackup
|
|
298
319
|
|
|
299
|
-
if
|
|
320
|
+
if options[:log_requests]
|
|
300
321
|
require_relative 'commonlogger'
|
|
301
|
-
logger =
|
|
322
|
+
logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
|
|
302
323
|
found = CommonLogger.new(found, logger)
|
|
303
324
|
end
|
|
304
325
|
|
|
@@ -307,7 +328,7 @@ module Puma
|
|
|
307
328
|
|
|
308
329
|
# Return which environment we're running in
|
|
309
330
|
def environment
|
|
310
|
-
|
|
331
|
+
options[:environment]
|
|
311
332
|
end
|
|
312
333
|
|
|
313
334
|
def load_plugin(name)
|
|
@@ -315,18 +336,19 @@ module Puma
|
|
|
315
336
|
end
|
|
316
337
|
|
|
317
338
|
# @param key [:Symbol] hook to run
|
|
318
|
-
# @param arg [Launcher, Int] `:
|
|
339
|
+
# @param arg [Launcher, Int] `:before_restart` passes Launcher
|
|
319
340
|
#
|
|
320
341
|
def run_hooks(key, arg, log_writer, hook_data = nil)
|
|
321
342
|
log_writer.debug "Running #{key} hooks"
|
|
322
343
|
|
|
323
|
-
|
|
344
|
+
options.all_of(key).each do |hook_options|
|
|
324
345
|
begin
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
346
|
+
block = hook_options[:block]
|
|
347
|
+
if id = hook_options[:id]
|
|
348
|
+
hook_data[id] ||= Hash.new
|
|
349
|
+
block.call arg, hook_data[id]
|
|
328
350
|
else
|
|
329
|
-
|
|
351
|
+
block.call arg
|
|
330
352
|
end
|
|
331
353
|
rescue => e
|
|
332
354
|
log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
|
|
@@ -336,7 +358,7 @@ module Puma
|
|
|
336
358
|
end
|
|
337
359
|
|
|
338
360
|
def final_options
|
|
339
|
-
|
|
361
|
+
options.final_options
|
|
340
362
|
end
|
|
341
363
|
|
|
342
364
|
def self.temp_path
|
|
@@ -346,6 +368,12 @@ module Puma
|
|
|
346
368
|
"#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
|
|
347
369
|
end
|
|
348
370
|
|
|
371
|
+
def self.random_token
|
|
372
|
+
require 'securerandom' unless defined?(SecureRandom)
|
|
373
|
+
|
|
374
|
+
SecureRandom.hex(16)
|
|
375
|
+
end
|
|
376
|
+
|
|
349
377
|
private
|
|
350
378
|
|
|
351
379
|
def require_processor_counter
|
|
@@ -386,22 +414,40 @@ module Puma
|
|
|
386
414
|
rack_app, rack_options = rack_builder.parse_file(rackup)
|
|
387
415
|
rack_options = rack_options || {}
|
|
388
416
|
|
|
389
|
-
|
|
417
|
+
options.file_options.merge!(rack_options)
|
|
390
418
|
|
|
391
419
|
config_ru_binds = []
|
|
392
420
|
rack_options.each do |k, v|
|
|
393
421
|
config_ru_binds << v if k.to_s.start_with?("bind")
|
|
394
422
|
end
|
|
395
423
|
|
|
396
|
-
|
|
424
|
+
options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
|
|
397
425
|
|
|
398
426
|
rack_app
|
|
399
427
|
end
|
|
400
428
|
|
|
401
|
-
def
|
|
402
|
-
|
|
429
|
+
def set_conditional_default_options
|
|
430
|
+
@_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
|
|
431
|
+
(@_options[:workers] > 1) && Puma.forkable?
|
|
432
|
+
end
|
|
403
433
|
|
|
404
|
-
|
|
434
|
+
def warn_hooks
|
|
435
|
+
return if options[:workers] > 0
|
|
436
|
+
return if options[:silence_fork_callback_warning]
|
|
437
|
+
|
|
438
|
+
log_writer = LogWriter.stdio
|
|
439
|
+
@hooks.each_key do |hook|
|
|
440
|
+
options.all_of(hook).each do |hook_options|
|
|
441
|
+
next unless hook_options[:cluster_only]
|
|
442
|
+
|
|
443
|
+
log_writer.log(<<~MSG.tr("\n", " "))
|
|
444
|
+
Warning: The code in the `#{hook}` block will not execute
|
|
445
|
+
in the current Puma configuration. The `#{hook}` block only
|
|
446
|
+
executes in Puma's cluster mode. To fix this, either remove the
|
|
447
|
+
`#{hook}` call or increase Puma's worker count above zero.
|
|
448
|
+
MSG
|
|
449
|
+
end
|
|
450
|
+
end
|
|
405
451
|
end
|
|
406
452
|
end
|
|
407
453
|
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 = "7.1.0"
|
|
104
|
+
CODE_NAME = "Neon Witch"
|
|
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
|
|
@@ -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"
|
data/lib/puma/control_cli.rb
CHANGED
|
@@ -124,11 +124,15 @@ module Puma
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
if @config_file
|
|
127
|
+
# needed because neither `Puma::CLI` or `Puma::Server` are loaded
|
|
128
|
+
require_relative '../puma'
|
|
129
|
+
|
|
127
130
|
require_relative 'configuration'
|
|
128
131
|
require_relative 'log_writer'
|
|
129
132
|
|
|
130
133
|
config = Puma::Configuration.new({ config_files: [@config_file] }, {} , env)
|
|
131
|
-
config.
|
|
134
|
+
config.clamp
|
|
135
|
+
|
|
132
136
|
@state ||= config.options[:state]
|
|
133
137
|
@control_url ||= config.options[:control_url]
|
|
134
138
|
@control_auth_token ||= config.options[:control_auth_token]
|
|
@@ -248,7 +252,7 @@ module Puma
|
|
|
248
252
|
@stdout.flush unless @stdout.sync
|
|
249
253
|
return
|
|
250
254
|
elsif sig.start_with? 'SIG'
|
|
251
|
-
if Signal.list.key? sig.
|
|
255
|
+
if Signal.list.key? sig.delete_prefix('SIG')
|
|
252
256
|
Process.kill sig, @pid
|
|
253
257
|
else
|
|
254
258
|
raise "Signal '#{sig}' not available'"
|