puma 6.6.1 → 7.2.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 +224 -4
- data/README.md +34 -34
- data/docs/deployment.md +58 -23
- data/docs/fork_worker.md +5 -5
- 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 +19 -19
- data/docs/stats.md +4 -3
- data/docs/systemd.md +3 -3
- 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 +9 -1
- data/ext/puma_http11/puma_http11.c +122 -118
- data/lib/puma/app/status.rb +10 -2
- data/lib/puma/binder.rb +10 -8
- data/lib/puma/cli.rb +3 -5
- data/lib/puma/client.rb +52 -56
- data/lib/puma/cluster/worker.rb +17 -17
- data/lib/puma/cluster/worker_handle.rb +38 -7
- data/lib/puma/cluster.rb +23 -23
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +104 -51
- 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 +149 -91
- 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 +54 -49
- 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 +42 -31
- data/lib/puma/runner.rb +9 -18
- data/lib/puma/server.rb +114 -64
- data/lib/puma/single.rb +6 -3
- 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 +13 -5
- metadata +6 -5
- data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class IO
|
|
4
|
-
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
|
5
|
-
# So this either creates the constant (on 1.8), or harmlessly
|
|
6
|
-
# reopens it (on 1.9).
|
|
7
|
-
module WaitReadable
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
|
|
11
3
|
require_relative 'detect'
|
|
12
4
|
require_relative 'io_buffer'
|
|
13
5
|
require 'tempfile'
|
|
@@ -64,6 +56,11 @@ module Puma
|
|
|
64
56
|
|
|
65
57
|
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
|
66
58
|
|
|
59
|
+
# See:
|
|
60
|
+
# https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
|
|
61
|
+
# https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
|
|
62
|
+
STRIP_OWS = /\A[ \t]+|[ \t]+\z/
|
|
63
|
+
|
|
67
64
|
# The object used for a request with no body. All requests with
|
|
68
65
|
# no body share this one object since it has no state.
|
|
69
66
|
EmptyBody = NullIO.new
|
|
@@ -111,7 +108,8 @@ module Puma
|
|
|
111
108
|
end
|
|
112
109
|
|
|
113
110
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
114
|
-
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
|
111
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
112
|
+
:requests_served
|
|
115
113
|
|
|
116
114
|
attr_writer :peerip, :http_content_length_limit
|
|
117
115
|
|
|
@@ -133,9 +131,9 @@ module Puma
|
|
|
133
131
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
134
132
|
end
|
|
135
133
|
|
|
136
|
-
# For the hijack protocol
|
|
137
|
-
#
|
|
138
|
-
def
|
|
134
|
+
# For the full hijack protocol, `env['rack.hijack']` is set to
|
|
135
|
+
# `client.method :full_hijack`
|
|
136
|
+
def full_hijack
|
|
139
137
|
@hijacked = true
|
|
140
138
|
env[HIJACK_IO] ||= @io
|
|
141
139
|
end
|
|
@@ -150,11 +148,12 @@ module Puma
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
# Number of seconds until the timeout elapses.
|
|
151
|
+
# @!attribute [r] timeout
|
|
153
152
|
def timeout
|
|
154
153
|
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
155
154
|
end
|
|
156
155
|
|
|
157
|
-
def reset
|
|
156
|
+
def reset
|
|
158
157
|
@parser.reset
|
|
159
158
|
@io_buffer.reset
|
|
160
159
|
@read_header = true
|
|
@@ -166,7 +165,10 @@ module Puma
|
|
|
166
165
|
@peerip = nil if @remote_addr_header
|
|
167
166
|
@in_last_chunk = false
|
|
168
167
|
@http_content_length_limit_exceeded = false
|
|
168
|
+
end
|
|
169
169
|
|
|
170
|
+
# only used with back-to-back requests contained in the buffer
|
|
171
|
+
def process_back_to_back_requests
|
|
170
172
|
if @buffer
|
|
171
173
|
return false unless try_to_parse_proxy_protocol
|
|
172
174
|
|
|
@@ -178,25 +180,20 @@ module Puma
|
|
|
178
180
|
raise HttpParserError,
|
|
179
181
|
"HEADER is longer than allowed, aborting client early."
|
|
180
182
|
end
|
|
181
|
-
|
|
182
|
-
return false
|
|
183
|
-
else
|
|
184
|
-
begin
|
|
185
|
-
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
|
186
|
-
return try_to_finish
|
|
187
|
-
end
|
|
188
|
-
rescue IOError
|
|
189
|
-
# swallow it
|
|
190
|
-
end
|
|
191
183
|
end
|
|
192
184
|
end
|
|
193
185
|
|
|
186
|
+
# if a client sends back-to-back requests, the buffer may contain one or more
|
|
187
|
+
# of them.
|
|
188
|
+
def has_back_to_back_requests?
|
|
189
|
+
!(@buffer.nil? || @buffer.empty?)
|
|
190
|
+
end
|
|
191
|
+
|
|
194
192
|
def close
|
|
195
193
|
tempfile_close
|
|
196
194
|
begin
|
|
197
195
|
@io.close
|
|
198
196
|
rescue IOError, Errno::EBADF
|
|
199
|
-
Puma::Util.purge_interrupt_queue
|
|
200
197
|
end
|
|
201
198
|
end
|
|
202
199
|
|
|
@@ -291,8 +288,10 @@ module Puma
|
|
|
291
288
|
|
|
292
289
|
def eagerly_finish
|
|
293
290
|
return true if @ready
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
292
|
+
return true if try_to_finish
|
|
293
|
+
end
|
|
294
|
+
false
|
|
296
295
|
end
|
|
297
296
|
|
|
298
297
|
def finish(timeout)
|
|
@@ -412,17 +411,18 @@ module Puma
|
|
|
412
411
|
if te
|
|
413
412
|
te_lwr = te.downcase
|
|
414
413
|
if te.include? ','
|
|
415
|
-
te_ary = te_lwr.split
|
|
414
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
416
415
|
te_count = te_ary.count CHUNKED
|
|
417
416
|
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
418
|
-
if
|
|
419
|
-
@env.delete TRANSFER_ENCODING2
|
|
420
|
-
return setup_chunked_body body
|
|
421
|
-
elsif te_count >= 1
|
|
417
|
+
if te_count > 1
|
|
422
418
|
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
|
419
|
+
elsif te_ary.last != CHUNKED
|
|
420
|
+
raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
|
|
423
421
|
elsif !te_valid
|
|
424
422
|
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
425
423
|
end
|
|
424
|
+
@env.delete TRANSFER_ENCODING2
|
|
425
|
+
return setup_chunked_body body
|
|
426
426
|
elsif te_lwr == CHUNKED
|
|
427
427
|
@env.delete TRANSFER_ENCODING2
|
|
428
428
|
return setup_chunked_body body
|
|
@@ -500,40 +500,36 @@ module Puma
|
|
|
500
500
|
# after this
|
|
501
501
|
remain = @body_remain
|
|
502
502
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
# don't bother with reading zero bytes
|
|
504
|
+
unless remain.zero?
|
|
505
|
+
begin
|
|
506
|
+
chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
|
|
507
|
+
rescue IO::WaitReadable
|
|
508
|
+
return false
|
|
509
|
+
rescue SystemCallError, IOError
|
|
510
|
+
raise ConnectionError, "Connection error detected during read"
|
|
511
|
+
end
|
|
508
512
|
|
|
509
|
-
|
|
510
|
-
chunk
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
513
|
+
# No chunk means a closed socket
|
|
514
|
+
unless chunk
|
|
515
|
+
@body.close
|
|
516
|
+
@buffer = nil
|
|
517
|
+
set_ready
|
|
518
|
+
raise EOFError
|
|
519
|
+
end
|
|
516
520
|
|
|
517
|
-
|
|
518
|
-
unless chunk
|
|
519
|
-
@body.close
|
|
520
|
-
@buffer = nil
|
|
521
|
-
set_ready
|
|
522
|
-
raise EOFError
|
|
521
|
+
remain -= @body.write(chunk)
|
|
523
522
|
end
|
|
524
523
|
|
|
525
|
-
remain -= @body.write(chunk)
|
|
526
|
-
|
|
527
524
|
if remain <= 0
|
|
528
525
|
@body.rewind
|
|
529
526
|
@buffer = nil
|
|
530
527
|
set_ready
|
|
531
|
-
|
|
528
|
+
true
|
|
529
|
+
else
|
|
530
|
+
@body_remain = remain
|
|
531
|
+
false
|
|
532
532
|
end
|
|
533
|
-
|
|
534
|
-
@body_remain = remain
|
|
535
|
-
|
|
536
|
-
false
|
|
537
533
|
end
|
|
538
534
|
|
|
539
535
|
def read_chunked_body
|
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.
|
|
@@ -45,8 +46,7 @@ module Puma
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def start_phased_restart(refork = false)
|
|
48
|
-
@events.
|
|
49
|
-
|
|
49
|
+
@events.fire_before_restart!
|
|
50
50
|
@phase += 1
|
|
51
51
|
if refork
|
|
52
52
|
log "- Starting worker refork, phase: #{@phase}"
|
|
@@ -186,7 +186,7 @@ module Puma
|
|
|
186
186
|
# we need to phase any workers out (which will restart
|
|
187
187
|
# in the right phase).
|
|
188
188
|
#
|
|
189
|
-
w = @workers.find { |x| x.phase
|
|
189
|
+
w = @workers.find { |x| x.phase < @phase }
|
|
190
190
|
|
|
191
191
|
if w
|
|
192
192
|
if refork
|
|
@@ -221,12 +221,11 @@ module Puma
|
|
|
221
221
|
pipes[:wakeup] = @wakeup
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
-
server = start_server if preload?
|
|
225
224
|
new_worker = Worker.new index: index,
|
|
226
225
|
master: master,
|
|
227
226
|
launcher: @launcher,
|
|
228
227
|
pipes: pipes,
|
|
229
|
-
|
|
228
|
+
app: (app if preload?)
|
|
230
229
|
new_worker.run
|
|
231
230
|
end
|
|
232
231
|
|
|
@@ -238,7 +237,7 @@ module Puma
|
|
|
238
237
|
def phased_restart(refork = false)
|
|
239
238
|
return false if @options[:preload_app] && !refork
|
|
240
239
|
|
|
241
|
-
@
|
|
240
|
+
@pending_phased_restart = refork ? :refork : true
|
|
242
241
|
wakeup!
|
|
243
242
|
|
|
244
243
|
true
|
|
@@ -268,11 +267,14 @@ module Puma
|
|
|
268
267
|
end
|
|
269
268
|
|
|
270
269
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
|
271
|
-
# the master process.
|
|
270
|
+
# the master process. Calling this also resets stat 'max' values to zero.
|
|
272
271
|
# @!attribute [r] stats
|
|
272
|
+
# @return [Hash]
|
|
273
|
+
|
|
273
274
|
def stats
|
|
274
275
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
|
275
276
|
worker_status = @workers.map do |w|
|
|
277
|
+
w.reset_max
|
|
276
278
|
{
|
|
277
279
|
started_at: utc_iso8601(w.started_at),
|
|
278
280
|
pid: w.pid,
|
|
@@ -283,7 +285,6 @@ module Puma
|
|
|
283
285
|
last_status: w.last_status,
|
|
284
286
|
}
|
|
285
287
|
end
|
|
286
|
-
|
|
287
288
|
{
|
|
288
289
|
started_at: utc_iso8601(@started_at),
|
|
289
290
|
workers: @workers.size,
|
|
@@ -352,7 +353,7 @@ module Puma
|
|
|
352
353
|
|
|
353
354
|
stop_workers
|
|
354
355
|
stop
|
|
355
|
-
@events.
|
|
356
|
+
@events.fire_after_stopped!
|
|
356
357
|
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
|
357
358
|
exit 0 # Clean exit, workers were stopped
|
|
358
359
|
end
|
|
@@ -369,12 +370,8 @@ module Puma
|
|
|
369
370
|
|
|
370
371
|
if preload?
|
|
371
372
|
# Threads explicitly marked as fork safe will be ignored. Used in Rails,
|
|
372
|
-
# but may be used by anyone.
|
|
373
|
-
|
|
374
|
-
# where calling thread_variable_get on a Process::Waiter will segfault.
|
|
375
|
-
# We can drop that clause once those versions of Ruby are no longer
|
|
376
|
-
# supported.
|
|
377
|
-
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) }
|
|
378
375
|
|
|
379
376
|
before = Thread.list.reject(&fork_safe)
|
|
380
377
|
|
|
@@ -423,6 +420,7 @@ module Puma
|
|
|
423
420
|
|
|
424
421
|
log "Use Ctrl-C to stop"
|
|
425
422
|
|
|
423
|
+
warn_ruby_mn_threads
|
|
426
424
|
single_worker_warning
|
|
427
425
|
|
|
428
426
|
redirect_io
|
|
@@ -457,11 +455,11 @@ module Puma
|
|
|
457
455
|
break
|
|
458
456
|
end
|
|
459
457
|
|
|
460
|
-
if @
|
|
461
|
-
start_phased_restart(@
|
|
458
|
+
if @pending_phased_restart
|
|
459
|
+
start_phased_restart(@pending_phased_restart == :refork)
|
|
462
460
|
|
|
463
|
-
in_phased_restart = @
|
|
464
|
-
@
|
|
461
|
+
in_phased_restart = @pending_phased_restart
|
|
462
|
+
@pending_phased_restart = false
|
|
465
463
|
|
|
466
464
|
workers_not_booted = @options[:workers]
|
|
467
465
|
# worker 0 is not restarted on refork
|
|
@@ -511,7 +509,7 @@ module Puma
|
|
|
511
509
|
end
|
|
512
510
|
|
|
513
511
|
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
|
514
|
-
@events.
|
|
512
|
+
@events.fire_after_booted!
|
|
515
513
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
516
514
|
booted = true
|
|
517
515
|
end
|
|
@@ -528,7 +526,7 @@ module Puma
|
|
|
528
526
|
end
|
|
529
527
|
|
|
530
528
|
if in_phased_restart && workers_not_booted.zero?
|
|
531
|
-
@events.
|
|
529
|
+
@events.fire_after_booted!
|
|
532
530
|
debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
|
|
533
531
|
in_phased_restart = false
|
|
534
532
|
end
|
|
@@ -584,7 +582,9 @@ module Puma
|
|
|
584
582
|
# `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
|
|
585
583
|
# 2. When `fork_worker` is enabled, some worker may not be direct children,
|
|
586
584
|
# but grand children. Because of this they won't be reaped by `Process.wait2(-1)`.
|
|
587
|
-
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)
|
|
588
588
|
true
|
|
589
589
|
else
|
|
590
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 ]
|