puma 3.12.1 → 5.6.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +1553 -447
- data/LICENSE +23 -20
- data/README.md +175 -63
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +59 -21
- data/docs/compile_options.md +21 -0
- data/docs/deployment.md +69 -58
- data/docs/fork_worker.md +33 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/jungle/README.md +9 -0
- data/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +22 -12
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +47 -22
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +95 -120
- data/ext/puma_http11/PumaHttp11Service.java +2 -2
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +51 -1
- data/ext/puma_http11/http11_parser.c +105 -117
- data/ext/puma_http11/http11_parser.h +1 -1
- data/ext/puma_http11/http11_parser.java.rl +22 -38
- data/ext/puma_http11/http11_parser.rl +4 -2
- data/ext/puma_http11/http11_parser_common.rl +4 -4
- data/ext/puma_http11/mini_ssl.c +319 -96
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +120 -65
- data/ext/puma_http11/puma_http11.c +35 -51
- data/lib/puma/app/status.rb +68 -49
- data/lib/puma/binder.rb +234 -137
- data/lib/puma/cli.rb +28 -18
- data/lib/puma/client.rb +343 -230
- data/lib/puma/cluster/worker.rb +173 -0
- data/lib/puma/cluster/worker_handle.rb +94 -0
- data/lib/puma/cluster.rb +247 -232
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +61 -51
- data/lib/puma/const.rb +42 -21
- data/lib/puma/control_cli.rb +109 -67
- data/lib/puma/detect.rb +29 -2
- data/lib/puma/dsl.rb +615 -123
- data/lib/puma/error_logger.rb +104 -0
- data/lib/puma/events.rb +55 -31
- data/lib/puma/io_buffer.rb +7 -5
- data/lib/puma/jruby_restart.rb +0 -58
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher.rb +182 -69
- data/lib/puma/minissl/context_builder.rb +81 -0
- data/lib/puma/minissl.rb +161 -61
- data/lib/puma/null_io.rb +13 -1
- data/lib/puma/plugin/tmp_restart.rb +2 -0
- data/lib/puma/plugin.rb +7 -13
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/rack/builder.rb +3 -5
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +2 -0
- data/lib/puma/reactor.rb +85 -316
- data/lib/puma/request.rb +472 -0
- data/lib/puma/runner.rb +48 -55
- data/lib/puma/server.rb +303 -695
- data/lib/puma/single.rb +11 -67
- data/lib/puma/state_file.rb +47 -8
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +132 -82
- data/lib/puma/util.rb +21 -7
- data/lib/puma.rb +54 -0
- data/lib/rack/handler/puma.rb +5 -6
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +45 -29
- data/ext/puma_http11/io_buffer.c +0 -155
- data/lib/puma/accept_nonblock.rb +0 -23
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/convenient.rb +0 -25
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
- data/lib/puma/java_io_buffer.rb +0 -47
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/server.rb
CHANGED
@@ -6,21 +6,16 @@ require 'puma/thread_pool'
|
|
6
6
|
require 'puma/const'
|
7
7
|
require 'puma/events'
|
8
8
|
require 'puma/null_io'
|
9
|
-
require 'puma/compat'
|
10
9
|
require 'puma/reactor'
|
11
10
|
require 'puma/client'
|
12
11
|
require 'puma/binder'
|
13
|
-
require 'puma/delegation'
|
14
|
-
require 'puma/accept_nonblock'
|
15
12
|
require 'puma/util'
|
16
|
-
|
17
|
-
require 'puma/
|
18
|
-
|
19
|
-
unless Puma.const_defined? "IOBuffer"
|
20
|
-
require 'puma/io_buffer'
|
21
|
-
end
|
13
|
+
require 'puma/io_buffer'
|
14
|
+
require 'puma/request'
|
22
15
|
|
23
16
|
require 'socket'
|
17
|
+
require 'io/wait'
|
18
|
+
require 'forwardable'
|
24
19
|
|
25
20
|
module Puma
|
26
21
|
|
@@ -28,7 +23,7 @@ module Puma
|
|
28
23
|
#
|
29
24
|
# This class is used by the `Puma::Single` and `Puma::Cluster` classes
|
30
25
|
# to generate one or more `Puma::Server` instances capable of handling requests.
|
31
|
-
# Each Puma process will contain one `Puma::Server`
|
26
|
+
# Each Puma process will contain one `Puma::Server` instance.
|
32
27
|
#
|
33
28
|
# The `Puma::Server` instance pulls requests from the socket, adds them to a
|
34
29
|
# `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
|
@@ -37,18 +32,31 @@ module Puma
|
|
37
32
|
class Server
|
38
33
|
|
39
34
|
include Puma::Const
|
40
|
-
|
35
|
+
include Request
|
36
|
+
extend Forwardable
|
41
37
|
|
42
38
|
attr_reader :thread
|
43
39
|
attr_reader :events
|
40
|
+
attr_reader :min_threads, :max_threads # for #stats
|
41
|
+
attr_reader :requests_count # @version 5.0.0
|
42
|
+
|
43
|
+
# @todo the following may be deprecated in the future
|
44
|
+
attr_reader :auto_trim_time, :early_hints, :first_data_timeout,
|
45
|
+
:leak_stack_on_error,
|
46
|
+
:persistent_timeout, :reaping_time
|
47
|
+
|
48
|
+
# @deprecated v6.0.0
|
49
|
+
attr_writer :auto_trim_time, :early_hints, :first_data_timeout,
|
50
|
+
:leak_stack_on_error, :min_threads, :max_threads,
|
51
|
+
:persistent_timeout, :reaping_time
|
52
|
+
|
44
53
|
attr_accessor :app
|
54
|
+
attr_accessor :binder
|
55
|
+
|
56
|
+
def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
|
57
|
+
:add_unix_listener, :connected_ports
|
45
58
|
|
46
|
-
|
47
|
-
attr_accessor :max_threads
|
48
|
-
attr_accessor :persistent_timeout
|
49
|
-
attr_accessor :auto_trim_time
|
50
|
-
attr_accessor :reaping_time
|
51
|
-
attr_accessor :first_data_timeout
|
59
|
+
ThreadLocalKey = :puma_server
|
52
60
|
|
53
61
|
# Create a server for the rack app +app+.
|
54
62
|
#
|
@@ -58,90 +66,117 @@ module Puma
|
|
58
66
|
# Server#run returns a thread that you can join on to wait for the server
|
59
67
|
# to do its work.
|
60
68
|
#
|
69
|
+
# @note Several instance variables exist so they are available for testing,
|
70
|
+
# and have default values set via +fetch+. Normally the values are set via
|
71
|
+
# `::Puma::Configuration.puma_default_options`.
|
72
|
+
#
|
61
73
|
def initialize(app, events=Events.stdio, options={})
|
62
74
|
@app = app
|
63
75
|
@events = events
|
64
76
|
|
65
|
-
@check, @notify =
|
66
|
-
|
77
|
+
@check, @notify = nil
|
67
78
|
@status = :stop
|
68
79
|
|
69
|
-
@min_threads = 0
|
70
|
-
@max_threads = 16
|
71
80
|
@auto_trim_time = 30
|
72
81
|
@reaping_time = 1
|
73
82
|
|
74
83
|
@thread = nil
|
75
84
|
@thread_pool = nil
|
76
|
-
@early_hints = nil
|
77
85
|
|
78
|
-
@
|
79
|
-
@first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
|
86
|
+
@options = options
|
80
87
|
|
81
|
-
@
|
82
|
-
@
|
88
|
+
@early_hints = options.fetch :early_hints, nil
|
89
|
+
@first_data_timeout = options.fetch :first_data_timeout, FIRST_DATA_TIMEOUT
|
90
|
+
@min_threads = options.fetch :min_threads, 0
|
91
|
+
@max_threads = options.fetch :max_threads , (Puma.mri? ? 5 : 16)
|
92
|
+
@persistent_timeout = options.fetch :persistent_timeout, PERSISTENT_TIMEOUT
|
93
|
+
@queue_requests = options.fetch :queue_requests, true
|
94
|
+
@max_fast_inline = options.fetch :max_fast_inline, MAX_FAST_INLINE
|
95
|
+
@io_selector_backend = options.fetch :io_selector_backend, :auto
|
83
96
|
|
84
|
-
|
97
|
+
temp = !!(@options[:environment] =~ /\A(development|test)\z/)
|
98
|
+
@leak_stack_on_error = @options[:environment] ? temp : true
|
85
99
|
|
86
|
-
@
|
87
|
-
@queue_requests = options[:queue_requests].nil? ? true : options[:queue_requests]
|
100
|
+
@binder = Binder.new(events)
|
88
101
|
|
89
102
|
ENV['RACK_ENV'] ||= "development"
|
90
103
|
|
91
104
|
@mode = :http
|
92
105
|
|
93
106
|
@precheck_closing = true
|
94
|
-
end
|
95
107
|
|
96
|
-
|
97
|
-
|
98
|
-
forward :add_tcp_listener, :@binder
|
99
|
-
forward :add_ssl_listener, :@binder
|
100
|
-
forward :add_unix_listener, :@binder
|
101
|
-
forward :connected_port, :@binder
|
108
|
+
@requests_count = 0
|
109
|
+
end
|
102
110
|
|
103
111
|
def inherit_binder(bind)
|
104
112
|
@binder = bind
|
105
|
-
@own_binder = false
|
106
113
|
end
|
107
114
|
|
108
|
-
|
109
|
-
|
115
|
+
class << self
|
116
|
+
# @!attribute [r] current
|
117
|
+
def current
|
118
|
+
Thread.current[ThreadLocalKey]
|
119
|
+
end
|
120
|
+
|
121
|
+
# :nodoc:
|
122
|
+
# @version 5.0.0
|
123
|
+
def tcp_cork_supported?
|
124
|
+
Socket.const_defined?(:TCP_CORK) && Socket.const_defined?(:IPPROTO_TCP)
|
125
|
+
end
|
126
|
+
|
127
|
+
# :nodoc:
|
128
|
+
# @version 5.0.0
|
129
|
+
def closed_socket_supported?
|
130
|
+
Socket.const_defined?(:TCP_INFO) && Socket.const_defined?(:IPPROTO_TCP)
|
131
|
+
end
|
132
|
+
private :tcp_cork_supported?
|
133
|
+
private :closed_socket_supported?
|
110
134
|
end
|
111
135
|
|
112
136
|
# On Linux, use TCP_CORK to better control how the TCP stack
|
113
137
|
# packetizes our stream. This improves both latency and throughput.
|
138
|
+
# socket parameter may be an MiniSSL::Socket, so use to_io
|
114
139
|
#
|
115
|
-
if
|
116
|
-
UNPACK_TCP_STATE_FROM_TCP_INFO = "C".freeze
|
117
|
-
|
140
|
+
if tcp_cork_supported?
|
118
141
|
# 6 == Socket::IPPROTO_TCP
|
119
142
|
# 3 == TCP_CORK
|
120
143
|
# 1/0 == turn on/off
|
121
144
|
def cork_socket(socket)
|
145
|
+
skt = socket.to_io
|
122
146
|
begin
|
123
|
-
|
147
|
+
skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 1) if skt.kind_of? TCPSocket
|
124
148
|
rescue IOError, SystemCallError
|
125
|
-
|
149
|
+
Puma::Util.purge_interrupt_queue
|
126
150
|
end
|
127
151
|
end
|
128
152
|
|
129
153
|
def uncork_socket(socket)
|
154
|
+
skt = socket.to_io
|
130
155
|
begin
|
131
|
-
|
156
|
+
skt.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_CORK, 0) if skt.kind_of? TCPSocket
|
132
157
|
rescue IOError, SystemCallError
|
133
|
-
|
158
|
+
Puma::Util.purge_interrupt_queue
|
134
159
|
end
|
135
160
|
end
|
161
|
+
else
|
162
|
+
def cork_socket(socket)
|
163
|
+
end
|
164
|
+
|
165
|
+
def uncork_socket(socket)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
if closed_socket_supported?
|
170
|
+
UNPACK_TCP_STATE_FROM_TCP_INFO = "C".freeze
|
136
171
|
|
137
172
|
def closed_socket?(socket)
|
138
|
-
|
139
|
-
return false unless @precheck_closing
|
173
|
+
skt = socket.to_io
|
174
|
+
return false unless skt.kind_of?(TCPSocket) && @precheck_closing
|
140
175
|
|
141
176
|
begin
|
142
|
-
tcp_info =
|
177
|
+
tcp_info = skt.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
|
143
178
|
rescue IOError, SystemCallError
|
144
|
-
|
179
|
+
Puma::Util.purge_interrupt_queue
|
145
180
|
@precheck_closing = false
|
146
181
|
false
|
147
182
|
else
|
@@ -151,21 +186,17 @@ module Puma
|
|
151
186
|
end
|
152
187
|
end
|
153
188
|
else
|
154
|
-
def cork_socket(socket)
|
155
|
-
end
|
156
|
-
|
157
|
-
def uncork_socket(socket)
|
158
|
-
end
|
159
|
-
|
160
189
|
def closed_socket?(socket)
|
161
190
|
false
|
162
191
|
end
|
163
192
|
end
|
164
193
|
|
194
|
+
# @!attribute [r] backlog
|
165
195
|
def backlog
|
166
196
|
@thread_pool and @thread_pool.backlog
|
167
197
|
end
|
168
198
|
|
199
|
+
# @!attribute [r] running
|
169
200
|
def running
|
170
201
|
@thread_pool and @thread_pool.spawned
|
171
202
|
end
|
@@ -178,172 +209,38 @@ module Puma
|
|
178
209
|
# there are 5 threads sitting idle ready to take
|
179
210
|
# a request. If one request comes in, then the
|
180
211
|
# value would be 4 until it finishes processing.
|
212
|
+
# @!attribute [r] pool_capacity
|
181
213
|
def pool_capacity
|
182
214
|
@thread_pool and @thread_pool.pool_capacity
|
183
215
|
end
|
184
216
|
|
185
|
-
# Lopez Mode == raw tcp apps
|
186
|
-
|
187
|
-
def run_lopez_mode(background=true)
|
188
|
-
@thread_pool = ThreadPool.new(@min_threads,
|
189
|
-
@max_threads,
|
190
|
-
Hash) do |client, tl|
|
191
|
-
|
192
|
-
io = client.to_io
|
193
|
-
addr = io.peeraddr.last
|
194
|
-
|
195
|
-
if addr.empty?
|
196
|
-
# Set unix socket addrs to localhost
|
197
|
-
addr = "127.0.0.1:0"
|
198
|
-
else
|
199
|
-
addr = "#{addr}:#{io.peeraddr[1]}"
|
200
|
-
end
|
201
|
-
|
202
|
-
env = { 'thread' => tl, REMOTE_ADDR => addr }
|
203
|
-
|
204
|
-
begin
|
205
|
-
@app.call env, client.to_io
|
206
|
-
rescue Object => e
|
207
|
-
STDERR.puts "! Detected exception at toplevel: #{e.message} (#{e.class})"
|
208
|
-
STDERR.puts e.backtrace
|
209
|
-
end
|
210
|
-
|
211
|
-
client.close unless env['detach']
|
212
|
-
end
|
213
|
-
|
214
|
-
@events.fire :state, :running
|
215
|
-
|
216
|
-
if background
|
217
|
-
@thread = Thread.new { handle_servers_lopez_mode }
|
218
|
-
return @thread
|
219
|
-
else
|
220
|
-
handle_servers_lopez_mode
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
def handle_servers_lopez_mode
|
225
|
-
begin
|
226
|
-
check = @check
|
227
|
-
sockets = [check] + @binder.ios
|
228
|
-
pool = @thread_pool
|
229
|
-
|
230
|
-
while @status == :run
|
231
|
-
begin
|
232
|
-
ios = IO.select sockets
|
233
|
-
ios.first.each do |sock|
|
234
|
-
if sock == check
|
235
|
-
break if handle_check
|
236
|
-
else
|
237
|
-
begin
|
238
|
-
if io = sock.accept_nonblock
|
239
|
-
client = Client.new io, nil
|
240
|
-
pool << client
|
241
|
-
end
|
242
|
-
rescue SystemCallError
|
243
|
-
# nothing
|
244
|
-
rescue Errno::ECONNABORTED
|
245
|
-
# client closed the socket even before accept
|
246
|
-
begin
|
247
|
-
io.close
|
248
|
-
rescue
|
249
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
254
|
-
rescue Object => e
|
255
|
-
@events.unknown_error self, e, "Listen loop"
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
@events.fire :state, @status
|
260
|
-
|
261
|
-
graceful_shutdown if @status == :stop || @status == :restart
|
262
|
-
|
263
|
-
rescue Exception => e
|
264
|
-
STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
|
265
|
-
STDERR.puts e.backtrace
|
266
|
-
ensure
|
267
|
-
begin
|
268
|
-
@check.close
|
269
|
-
rescue
|
270
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
271
|
-
end
|
272
|
-
|
273
|
-
@notify.close
|
274
|
-
|
275
|
-
if @status != :restart and @own_binder
|
276
|
-
@binder.close
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
@events.fire :state, :done
|
281
|
-
end
|
282
217
|
# Runs the server.
|
283
218
|
#
|
284
219
|
# If +background+ is true (the default) then a thread is spun
|
285
220
|
# up in the background to handle requests. Otherwise requests
|
286
221
|
# are handled synchronously.
|
287
222
|
#
|
288
|
-
def run(background=true)
|
223
|
+
def run(background=true, thread_name: 'srv')
|
289
224
|
BasicSocket.do_not_reverse_lookup = true
|
290
225
|
|
291
226
|
@events.fire :state, :booting
|
292
227
|
|
293
228
|
@status = :run
|
294
229
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
@max_threads,
|
303
|
-
IOBuffer) do |client, buffer|
|
304
|
-
|
305
|
-
# Advertise this server into the thread
|
306
|
-
Thread.current[ThreadLocalKey] = self
|
307
|
-
|
308
|
-
process_now = false
|
309
|
-
|
310
|
-
begin
|
311
|
-
if queue_requests
|
312
|
-
process_now = client.eagerly_finish
|
313
|
-
else
|
314
|
-
client.finish
|
315
|
-
process_now = true
|
316
|
-
end
|
317
|
-
rescue MiniSSL::SSLError => e
|
318
|
-
ssl_socket = client.io
|
319
|
-
addr = ssl_socket.peeraddr.last
|
320
|
-
cert = ssl_socket.peercert
|
321
|
-
|
322
|
-
client.close
|
323
|
-
|
324
|
-
@events.ssl_error self, addr, cert, e
|
325
|
-
rescue HttpParserError => e
|
326
|
-
client.write_400
|
327
|
-
client.close
|
328
|
-
|
329
|
-
@events.parse_error self, client.env, e
|
330
|
-
rescue ConnectionError, EOFError
|
331
|
-
client.close
|
332
|
-
else
|
333
|
-
if process_now
|
334
|
-
process_client client, buffer
|
335
|
-
else
|
336
|
-
client.set_timeout @first_data_timeout
|
337
|
-
@reactor.add client
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
230
|
+
@thread_pool = ThreadPool.new(
|
231
|
+
thread_name,
|
232
|
+
@min_threads,
|
233
|
+
@max_threads,
|
234
|
+
::Puma::IOBuffer,
|
235
|
+
&method(:process_client)
|
236
|
+
)
|
341
237
|
|
238
|
+
@thread_pool.out_of_band_hook = @options[:out_of_band]
|
342
239
|
@thread_pool.clean_thread_locals = @options[:clean_thread_locals]
|
343
240
|
|
344
|
-
if queue_requests
|
345
|
-
@reactor = Reactor.new
|
346
|
-
@reactor.
|
241
|
+
if @queue_requests
|
242
|
+
@reactor = Reactor.new(@io_selector_backend, &method(:reactor_wakeup))
|
243
|
+
@reactor.run
|
347
244
|
end
|
348
245
|
|
349
246
|
if @reaping_time
|
@@ -354,86 +251,133 @@ module Puma
|
|
354
251
|
@thread_pool.auto_trim!(@auto_trim_time)
|
355
252
|
end
|
356
253
|
|
254
|
+
@check, @notify = Puma::Util.pipe unless @notify
|
255
|
+
|
357
256
|
@events.fire :state, :running
|
358
257
|
|
359
258
|
if background
|
360
|
-
@thread = Thread.new
|
259
|
+
@thread = Thread.new do
|
260
|
+
Puma.set_thread_name thread_name
|
261
|
+
handle_servers
|
262
|
+
end
|
361
263
|
return @thread
|
362
264
|
else
|
363
265
|
handle_servers
|
364
266
|
end
|
365
267
|
end
|
366
268
|
|
269
|
+
# This method is called from the Reactor thread when a queued Client receives data,
|
270
|
+
# times out, or when the Reactor is shutting down.
|
271
|
+
#
|
272
|
+
# It is responsible for ensuring that a request has been completely received
|
273
|
+
# before it starts to be processed by the ThreadPool. This may be known as read buffering.
|
274
|
+
# If read buffering is not done, and no other read buffering is performed (such as by an application server
|
275
|
+
# such as nginx) then the application would be subject to a slow client attack.
|
276
|
+
#
|
277
|
+
# For a graphical representation of how the request buffer works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
|
278
|
+
#
|
279
|
+
# The method checks to see if it has the full header and body with
|
280
|
+
# the `Puma::Client#try_to_finish` method. If the full request has been sent,
|
281
|
+
# then the request is passed to the ThreadPool (`@thread_pool << client`)
|
282
|
+
# so that a "worker thread" can pick up the request and begin to execute application logic.
|
283
|
+
# The Client is then removed from the reactor (return `true`).
|
284
|
+
#
|
285
|
+
# If a client object times out, a 408 response is written, its connection is closed,
|
286
|
+
# and the object is removed from the reactor (return `true`).
|
287
|
+
#
|
288
|
+
# If the Reactor is shutting down, all Clients are either timed out or passed to the
|
289
|
+
# ThreadPool, depending on their current state (#can_close?).
|
290
|
+
#
|
291
|
+
# Otherwise, if the full request is not ready then the client will remain in the reactor
|
292
|
+
# (return `false`). When the client sends more data to the socket the `Puma::Client` object
|
293
|
+
# will wake up and again be checked to see if it's ready to be passed to the thread pool.
|
294
|
+
def reactor_wakeup(client)
|
295
|
+
shutdown = !@queue_requests
|
296
|
+
if client.try_to_finish || (shutdown && !client.can_close?)
|
297
|
+
@thread_pool << client
|
298
|
+
elsif shutdown || client.timeout == 0
|
299
|
+
client.timeout!
|
300
|
+
else
|
301
|
+
client.set_timeout(@first_data_timeout)
|
302
|
+
false
|
303
|
+
end
|
304
|
+
rescue StandardError => e
|
305
|
+
client_error(e, client)
|
306
|
+
client.close
|
307
|
+
true
|
308
|
+
end
|
309
|
+
|
367
310
|
def handle_servers
|
368
311
|
begin
|
369
312
|
check = @check
|
370
313
|
sockets = [check] + @binder.ios
|
371
314
|
pool = @thread_pool
|
372
315
|
queue_requests = @queue_requests
|
316
|
+
drain = @options[:drain_on_shutdown] ? 0 : nil
|
373
317
|
|
374
|
-
|
375
|
-
remote_addr_header = nil
|
376
|
-
|
377
|
-
case @options[:remote_address]
|
318
|
+
addr_send_name, addr_value = case @options[:remote_address]
|
378
319
|
when :value
|
379
|
-
|
320
|
+
[:peerip=, @options[:remote_address_value]]
|
380
321
|
when :header
|
381
|
-
remote_addr_header
|
322
|
+
[:remote_addr_header=, @options[:remote_address_header]]
|
323
|
+
when :proxy_protocol
|
324
|
+
[:expect_proxy_proto=, @options[:remote_address_proxy_protocol]]
|
325
|
+
else
|
326
|
+
[nil, nil]
|
382
327
|
end
|
383
328
|
|
384
|
-
while @status == :run
|
329
|
+
while @status == :run || (drain && shutting_down?)
|
385
330
|
begin
|
386
|
-
ios = IO.select sockets
|
331
|
+
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
|
332
|
+
break unless ios
|
387
333
|
ios.first.each do |sock|
|
388
334
|
if sock == check
|
389
335
|
break if handle_check
|
390
336
|
else
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
end
|
399
|
-
|
400
|
-
pool << client
|
401
|
-
pool.wait_until_not_full
|
402
|
-
end
|
403
|
-
rescue SystemCallError
|
404
|
-
# nothing
|
405
|
-
rescue Errno::ECONNABORTED
|
406
|
-
# client closed the socket even before accept
|
407
|
-
begin
|
408
|
-
io.close
|
409
|
-
rescue
|
410
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
411
|
-
end
|
337
|
+
pool.wait_until_not_full
|
338
|
+
pool.wait_for_less_busy_worker(@options[:wait_for_less_busy_worker])
|
339
|
+
|
340
|
+
io = begin
|
341
|
+
sock.accept_nonblock
|
342
|
+
rescue IO::WaitReadable
|
343
|
+
next
|
412
344
|
end
|
345
|
+
drain += 1 if shutting_down?
|
346
|
+
pool << Client.new(io, @binder.env(sock)).tap { |c|
|
347
|
+
c.listener = sock
|
348
|
+
c.send(addr_send_name, addr_value) if addr_value
|
349
|
+
}
|
413
350
|
end
|
414
351
|
end
|
415
|
-
rescue
|
416
|
-
|
352
|
+
rescue IOError, Errno::EBADF
|
353
|
+
# In the case that any of the sockets are unexpectedly close.
|
354
|
+
raise
|
355
|
+
rescue StandardError => e
|
356
|
+
@events.unknown_error e, nil, "Listen loop"
|
417
357
|
end
|
418
358
|
end
|
419
359
|
|
360
|
+
@events.debug "Drained #{drain} additional connections." if drain
|
420
361
|
@events.fire :state, @status
|
421
362
|
|
422
|
-
graceful_shutdown if @status == :stop || @status == :restart
|
423
363
|
if queue_requests
|
424
|
-
@
|
364
|
+
@queue_requests = false
|
425
365
|
@reactor.shutdown
|
426
366
|
end
|
367
|
+
graceful_shutdown if @status == :stop || @status == :restart
|
427
368
|
rescue Exception => e
|
428
|
-
|
429
|
-
STDERR.puts e.backtrace
|
369
|
+
@events.unknown_error e, nil, "Exception handling servers"
|
430
370
|
ensure
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
371
|
+
# RuntimeError is Ruby 2.2 issue, can't modify frozen IOError
|
372
|
+
# Errno::EBADF is infrequently raised
|
373
|
+
[@check, @notify].each do |io|
|
374
|
+
begin
|
375
|
+
io.close unless io.closed?
|
376
|
+
rescue Errno::EBADF, RuntimeError
|
377
|
+
end
|
436
378
|
end
|
379
|
+
@notify = nil
|
380
|
+
@check = nil
|
437
381
|
end
|
438
382
|
|
439
383
|
@events.fire :state, :done
|
@@ -455,450 +399,149 @@ module Puma
|
|
455
399
|
return true
|
456
400
|
end
|
457
401
|
|
458
|
-
|
402
|
+
false
|
459
403
|
end
|
460
404
|
|
461
|
-
# Given a connection on +client+, handle the incoming requests
|
405
|
+
# Given a connection on +client+, handle the incoming requests,
|
406
|
+
# or queue the connection in the Reactor if no request is available.
|
462
407
|
#
|
463
|
-
# This method
|
408
|
+
# This method is called from a ThreadPool worker thread.
|
409
|
+
#
|
410
|
+
# This method supports HTTP Keep-Alive so it may, depending on if the client
|
464
411
|
# indicates that it supports keep alive, wait for another request before
|
465
412
|
# returning.
|
466
413
|
#
|
414
|
+
# Return true if one or more requests were processed.
|
467
415
|
def process_client(client, buffer)
|
416
|
+
# Advertise this server into the thread
|
417
|
+
Thread.current[ThreadLocalKey] = self
|
418
|
+
|
419
|
+
clean_thread_locals = @options[:clean_thread_locals]
|
420
|
+
close_socket = true
|
421
|
+
|
422
|
+
requests = 0
|
423
|
+
|
468
424
|
begin
|
425
|
+
if @queue_requests &&
|
426
|
+
!client.eagerly_finish
|
469
427
|
|
470
|
-
|
471
|
-
|
428
|
+
client.set_timeout(@first_data_timeout)
|
429
|
+
if @reactor.add client
|
430
|
+
close_socket = false
|
431
|
+
return false
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
with_force_shutdown(client) do
|
436
|
+
client.finish(@first_data_timeout)
|
437
|
+
end
|
472
438
|
|
473
439
|
while true
|
474
|
-
|
440
|
+
@requests_count += 1
|
441
|
+
case handle_request(client, buffer, requests + 1)
|
475
442
|
when false
|
476
|
-
|
443
|
+
break
|
477
444
|
when :async
|
478
445
|
close_socket = false
|
479
|
-
|
446
|
+
break
|
480
447
|
when true
|
481
|
-
return unless @queue_requests
|
482
448
|
buffer.reset
|
483
449
|
|
484
450
|
ThreadPool.clean_thread_locals if clean_thread_locals
|
485
451
|
|
486
|
-
|
487
|
-
close_socket = false
|
488
|
-
client.set_timeout @persistent_timeout
|
489
|
-
@reactor.add client
|
490
|
-
return
|
491
|
-
end
|
492
|
-
end
|
493
|
-
end
|
494
|
-
|
495
|
-
# The client disconnected while we were reading data
|
496
|
-
rescue ConnectionError
|
497
|
-
# Swallow them. The ensure tries to close +client+ down
|
498
|
-
|
499
|
-
# SSL handshake error
|
500
|
-
rescue MiniSSL::SSLError => e
|
501
|
-
lowlevel_error(e, client.env)
|
502
|
-
|
503
|
-
ssl_socket = client.io
|
504
|
-
addr = ssl_socket.peeraddr.last
|
505
|
-
cert = ssl_socket.peercert
|
506
|
-
|
507
|
-
close_socket = true
|
508
|
-
|
509
|
-
@events.ssl_error self, addr, cert, e
|
452
|
+
requests += 1
|
510
453
|
|
511
|
-
|
512
|
-
|
513
|
-
|
454
|
+
# As an optimization, try to read the next request from the
|
455
|
+
# socket for a short time before returning to the reactor.
|
456
|
+
fast_check = @status == :run
|
514
457
|
|
515
|
-
|
458
|
+
# Always pass the client back to the reactor after a reasonable
|
459
|
+
# number of inline requests if there are other requests pending.
|
460
|
+
fast_check = false if requests >= @max_fast_inline &&
|
461
|
+
@thread_pool.backlog > 0
|
516
462
|
|
517
|
-
|
463
|
+
next_request_ready = with_force_shutdown(client) do
|
464
|
+
client.reset(fast_check)
|
465
|
+
end
|
518
466
|
|
519
|
-
|
467
|
+
unless next_request_ready
|
468
|
+
break unless @queue_requests
|
469
|
+
client.set_timeout @persistent_timeout
|
470
|
+
if @reactor.add client
|
471
|
+
close_socket = false
|
472
|
+
break
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
true
|
520
478
|
rescue StandardError => e
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
@events.unknown_error self, e, "Read"
|
526
|
-
|
479
|
+
client_error(e, client)
|
480
|
+
# The ensure tries to close +client+ down
|
481
|
+
requests > 0
|
527
482
|
ensure
|
528
483
|
buffer.reset
|
529
484
|
|
530
485
|
begin
|
531
486
|
client.close if close_socket
|
532
487
|
rescue IOError, SystemCallError
|
533
|
-
|
488
|
+
Puma::Util.purge_interrupt_queue
|
534
489
|
# Already closed
|
535
490
|
rescue StandardError => e
|
536
|
-
@events.unknown_error
|
537
|
-
end
|
538
|
-
end
|
539
|
-
end
|
540
|
-
|
541
|
-
# Given a Hash +env+ for the request read from +client+, add
|
542
|
-
# and fixup keys to comply with Rack's env guidelines.
|
543
|
-
#
|
544
|
-
def normalize_env(env, client)
|
545
|
-
if host = env[HTTP_HOST]
|
546
|
-
if colon = host.index(":")
|
547
|
-
env[SERVER_NAME] = host[0, colon]
|
548
|
-
env[SERVER_PORT] = host[colon+1, host.bytesize]
|
549
|
-
else
|
550
|
-
env[SERVER_NAME] = host
|
551
|
-
env[SERVER_PORT] = default_server_port(env)
|
552
|
-
end
|
553
|
-
else
|
554
|
-
env[SERVER_NAME] = LOCALHOST
|
555
|
-
env[SERVER_PORT] = default_server_port(env)
|
556
|
-
end
|
557
|
-
|
558
|
-
unless env[REQUEST_PATH]
|
559
|
-
# it might be a dumbass full host request header
|
560
|
-
uri = URI.parse(env[REQUEST_URI])
|
561
|
-
env[REQUEST_PATH] = uri.path
|
562
|
-
|
563
|
-
raise "No REQUEST PATH" unless env[REQUEST_PATH]
|
564
|
-
|
565
|
-
# A nil env value will cause a LintError (and fatal errors elsewhere),
|
566
|
-
# so only set the env value if there actually is a value.
|
567
|
-
env[QUERY_STRING] = uri.query if uri.query
|
568
|
-
end
|
569
|
-
|
570
|
-
env[PATH_INFO] = env[REQUEST_PATH]
|
571
|
-
|
572
|
-
# From http://www.ietf.org/rfc/rfc3875 :
|
573
|
-
# "Script authors should be aware that the REMOTE_ADDR and
|
574
|
-
# REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
|
575
|
-
# may not identify the ultimate source of the request.
|
576
|
-
# They identify the client for the immediate request to the
|
577
|
-
# server; that client may be a proxy, gateway, or other
|
578
|
-
# intermediary acting on behalf of the actual source client."
|
579
|
-
#
|
580
|
-
|
581
|
-
unless env.key?(REMOTE_ADDR)
|
582
|
-
begin
|
583
|
-
addr = client.peerip
|
584
|
-
rescue Errno::ENOTCONN
|
585
|
-
# Client disconnects can result in an inability to get the
|
586
|
-
# peeraddr from the socket; default to localhost.
|
587
|
-
addr = LOCALHOST_IP
|
588
|
-
end
|
589
|
-
|
590
|
-
# Set unix socket addrs to localhost
|
591
|
-
addr = LOCALHOST_IP if addr.empty?
|
592
|
-
|
593
|
-
env[REMOTE_ADDR] = addr
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
|
-
def default_server_port(env)
|
598
|
-
return PORT_443 if env[HTTPS_KEY] == 'on' || env[HTTPS_KEY] == 'https'
|
599
|
-
env['HTTP_X_FORWARDED_PROTO'] == 'https' ? PORT_443 : PORT_80
|
600
|
-
end
|
601
|
-
|
602
|
-
# Given the request +env+ from +client+ and a partial request body
|
603
|
-
# in +body+, finish reading the body if there is one and invoke
|
604
|
-
# the rack app. Then construct the response and write it back to
|
605
|
-
# +client+
|
606
|
-
#
|
607
|
-
# +cl+ is the previously fetched Content-Length header if there
|
608
|
-
# was one. This is an optimization to keep from having to look
|
609
|
-
# it up again.
|
610
|
-
#
|
611
|
-
def handle_request(req, lines)
|
612
|
-
env = req.env
|
613
|
-
client = req.io
|
614
|
-
|
615
|
-
return false if closed_socket?(client)
|
616
|
-
|
617
|
-
normalize_env env, req
|
618
|
-
|
619
|
-
env[PUMA_SOCKET] = client
|
620
|
-
|
621
|
-
if env[HTTPS_KEY] && client.peercert
|
622
|
-
env[PUMA_PEERCERT] = client.peercert
|
623
|
-
end
|
624
|
-
|
625
|
-
env[HIJACK_P] = true
|
626
|
-
env[HIJACK] = req
|
627
|
-
|
628
|
-
body = req.body
|
629
|
-
|
630
|
-
head = env[REQUEST_METHOD] == HEAD
|
631
|
-
|
632
|
-
env[RACK_INPUT] = body
|
633
|
-
env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
|
634
|
-
|
635
|
-
if @early_hints
|
636
|
-
env[EARLY_HINTS] = lambda { |headers|
|
637
|
-
fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
|
638
|
-
|
639
|
-
headers.each_pair do |k, vs|
|
640
|
-
if vs.respond_to?(:to_s) && !vs.to_s.empty?
|
641
|
-
vs.to_s.split(NEWLINE).each do |v|
|
642
|
-
fast_write client, "#{k}: #{v}\r\n"
|
643
|
-
end
|
644
|
-
else
|
645
|
-
fast_write client, "#{k}: #{vs}\r\n"
|
646
|
-
end
|
647
|
-
end
|
648
|
-
|
649
|
-
fast_write client, "\r\n".freeze
|
650
|
-
}
|
651
|
-
end
|
652
|
-
|
653
|
-
# A rack extension. If the app writes #call'ables to this
|
654
|
-
# array, we will invoke them when the request is done.
|
655
|
-
#
|
656
|
-
after_reply = env[RACK_AFTER_REPLY] = []
|
657
|
-
|
658
|
-
begin
|
659
|
-
begin
|
660
|
-
status, headers, res_body = @app.call(env)
|
661
|
-
|
662
|
-
return :async if req.hijacked
|
663
|
-
|
664
|
-
status = status.to_i
|
665
|
-
|
666
|
-
if status == -1
|
667
|
-
unless headers.empty? and res_body == []
|
668
|
-
raise "async response must have empty headers and body"
|
669
|
-
end
|
670
|
-
|
671
|
-
return :async
|
672
|
-
end
|
673
|
-
rescue ThreadPool::ForceShutdown => e
|
674
|
-
@events.log "Detected force shutdown of a thread, returning 503"
|
675
|
-
@events.unknown_error self, e, "Rack app"
|
676
|
-
|
677
|
-
status = 503
|
678
|
-
headers = {}
|
679
|
-
res_body = ["Request was internally terminated early\n"]
|
680
|
-
|
681
|
-
rescue Exception => e
|
682
|
-
@events.unknown_error self, e, "Rack app", env
|
683
|
-
|
684
|
-
status, headers, res_body = lowlevel_error(e, env)
|
685
|
-
end
|
686
|
-
|
687
|
-
content_length = nil
|
688
|
-
no_body = head
|
689
|
-
|
690
|
-
if res_body.kind_of? Array and res_body.size == 1
|
691
|
-
content_length = res_body[0].bytesize
|
692
|
-
end
|
693
|
-
|
694
|
-
cork_socket client
|
695
|
-
|
696
|
-
line_ending = LINE_END
|
697
|
-
colon = COLON
|
698
|
-
|
699
|
-
http_11 = if env[HTTP_VERSION] == HTTP_11
|
700
|
-
allow_chunked = true
|
701
|
-
keep_alive = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
|
702
|
-
include_keepalive_header = false
|
703
|
-
|
704
|
-
# An optimization. The most common response is 200, so we can
|
705
|
-
# reply with the proper 200 status without having to compute
|
706
|
-
# the response header.
|
707
|
-
#
|
708
|
-
if status == 200
|
709
|
-
lines << HTTP_11_200
|
710
|
-
else
|
711
|
-
lines.append "HTTP/1.1 ", status.to_s, " ",
|
712
|
-
fetch_status_code(status), line_ending
|
713
|
-
|
714
|
-
no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
|
715
|
-
end
|
716
|
-
true
|
717
|
-
else
|
718
|
-
allow_chunked = false
|
719
|
-
keep_alive = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
|
720
|
-
include_keepalive_header = keep_alive
|
721
|
-
|
722
|
-
# Same optimization as above for HTTP/1.1
|
723
|
-
#
|
724
|
-
if status == 200
|
725
|
-
lines << HTTP_10_200
|
726
|
-
else
|
727
|
-
lines.append "HTTP/1.0 ", status.to_s, " ",
|
728
|
-
fetch_status_code(status), line_ending
|
729
|
-
|
730
|
-
no_body ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
|
731
|
-
end
|
732
|
-
false
|
491
|
+
@events.unknown_error e, nil, "Client"
|
733
492
|
end
|
734
|
-
|
735
|
-
response_hijack = nil
|
736
|
-
|
737
|
-
headers.each do |k, vs|
|
738
|
-
case k.downcase
|
739
|
-
when CONTENT_LENGTH2
|
740
|
-
content_length = vs
|
741
|
-
next
|
742
|
-
when TRANSFER_ENCODING
|
743
|
-
allow_chunked = false
|
744
|
-
content_length = nil
|
745
|
-
when HIJACK
|
746
|
-
response_hijack = vs
|
747
|
-
next
|
748
|
-
end
|
749
|
-
|
750
|
-
if vs.respond_to?(:to_s) && !vs.to_s.empty?
|
751
|
-
vs.to_s.split(NEWLINE).each do |v|
|
752
|
-
lines.append k, colon, v, line_ending
|
753
|
-
end
|
754
|
-
else
|
755
|
-
lines.append k, colon, line_ending
|
756
|
-
end
|
757
|
-
end
|
758
|
-
|
759
|
-
if include_keepalive_header
|
760
|
-
lines << CONNECTION_KEEP_ALIVE
|
761
|
-
elsif http_11 && !keep_alive
|
762
|
-
lines << CONNECTION_CLOSE
|
763
|
-
end
|
764
|
-
|
765
|
-
if no_body
|
766
|
-
if content_length and status != 204
|
767
|
-
lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
|
768
|
-
end
|
769
|
-
|
770
|
-
lines << line_ending
|
771
|
-
fast_write client, lines.to_s
|
772
|
-
return keep_alive
|
773
|
-
end
|
774
|
-
|
775
|
-
if content_length
|
776
|
-
lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
|
777
|
-
chunked = false
|
778
|
-
elsif !response_hijack and allow_chunked
|
779
|
-
lines << TRANSFER_ENCODING_CHUNKED
|
780
|
-
chunked = true
|
781
|
-
end
|
782
|
-
|
783
|
-
lines << line_ending
|
784
|
-
|
785
|
-
fast_write client, lines.to_s
|
786
|
-
|
787
|
-
if response_hijack
|
788
|
-
response_hijack.call client
|
789
|
-
return :async
|
790
|
-
end
|
791
|
-
|
792
|
-
begin
|
793
|
-
res_body.each do |part|
|
794
|
-
next if part.bytesize.zero?
|
795
|
-
if chunked
|
796
|
-
fast_write client, part.bytesize.to_s(16)
|
797
|
-
fast_write client, line_ending
|
798
|
-
fast_write client, part
|
799
|
-
fast_write client, line_ending
|
800
|
-
else
|
801
|
-
fast_write client, part
|
802
|
-
end
|
803
|
-
|
804
|
-
client.flush
|
805
|
-
end
|
806
|
-
|
807
|
-
if chunked
|
808
|
-
fast_write client, CLOSE_CHUNKED
|
809
|
-
client.flush
|
810
|
-
end
|
811
|
-
rescue SystemCallError, IOError
|
812
|
-
raise ConnectionError, "Connection error detected during write"
|
813
|
-
end
|
814
|
-
|
815
|
-
ensure
|
816
|
-
uncork_socket client
|
817
|
-
|
818
|
-
body.close
|
819
|
-
req.tempfile.unlink if req.tempfile
|
820
|
-
res_body.close if res_body.respond_to? :close
|
821
|
-
|
822
|
-
after_reply.each { |o| o.call }
|
823
493
|
end
|
824
|
-
|
825
|
-
return keep_alive
|
826
494
|
end
|
827
495
|
|
828
|
-
|
829
|
-
|
496
|
+
# Triggers a client timeout if the thread-pool shuts down
|
497
|
+
# during execution of the provided block.
|
498
|
+
def with_force_shutdown(client, &block)
|
499
|
+
@thread_pool.with_force_shutdown(&block)
|
500
|
+
rescue ThreadPool::ForceShutdown
|
501
|
+
client.timeout!
|
830
502
|
end
|
831
|
-
private :fetch_status_code
|
832
503
|
|
833
|
-
#
|
834
|
-
|
835
|
-
#
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
stream.binmode
|
504
|
+
# :nocov:
|
505
|
+
|
506
|
+
# Handle various error types thrown by Client I/O operations.
|
507
|
+
def client_error(e, client)
|
508
|
+
# Swallow, do not log
|
509
|
+
return if [ConnectionError, EOFError].include?(e.class)
|
510
|
+
|
511
|
+
lowlevel_error(e, client.env)
|
512
|
+
case e
|
513
|
+
when MiniSSL::SSLError
|
514
|
+
@events.ssl_error e, client.io
|
515
|
+
when HttpParserError
|
516
|
+
client.write_error(400)
|
517
|
+
@events.parse_error e, client
|
518
|
+
when HttpParserError501
|
519
|
+
client.write_error(501)
|
520
|
+
@events.parse_error e, client
|
851
521
|
else
|
852
|
-
|
853
|
-
|
854
|
-
stream = StringIO.new body[0,0]
|
855
|
-
end
|
856
|
-
|
857
|
-
stream.write body
|
858
|
-
|
859
|
-
# Read an odd sized chunk so we can read even sized ones
|
860
|
-
# after this
|
861
|
-
chunk = client.readpartial(remain % CHUNK_SIZE)
|
862
|
-
|
863
|
-
# No chunk means a closed socket
|
864
|
-
unless chunk
|
865
|
-
stream.close
|
866
|
-
return nil
|
867
|
-
end
|
868
|
-
|
869
|
-
remain -= stream.write(chunk)
|
870
|
-
|
871
|
-
# Raed the rest of the chunks
|
872
|
-
while remain > 0
|
873
|
-
chunk = client.readpartial(CHUNK_SIZE)
|
874
|
-
unless chunk
|
875
|
-
stream.close
|
876
|
-
return nil
|
877
|
-
end
|
878
|
-
|
879
|
-
remain -= stream.write(chunk)
|
522
|
+
client.write_error(500)
|
523
|
+
@events.unknown_error e, nil, "Read"
|
880
524
|
end
|
881
|
-
|
882
|
-
stream.rewind
|
883
|
-
|
884
|
-
return stream
|
885
525
|
end
|
886
526
|
|
887
527
|
# A fallback rack response if +@app+ raises as exception.
|
888
528
|
#
|
889
|
-
def lowlevel_error(e, env)
|
529
|
+
def lowlevel_error(e, env, status=500)
|
890
530
|
if handler = @options[:lowlevel_error_handler]
|
891
531
|
if handler.arity == 1
|
892
532
|
return handler.call(e)
|
893
|
-
|
533
|
+
elsif handler.arity == 2
|
894
534
|
return handler.call(e, env)
|
535
|
+
else
|
536
|
+
return handler.call(e, env, status)
|
895
537
|
end
|
896
538
|
end
|
897
539
|
|
898
540
|
if @leak_stack_on_error
|
899
|
-
|
541
|
+
backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
|
542
|
+
[status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
|
900
543
|
else
|
901
|
-
[
|
544
|
+
[status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
|
902
545
|
end
|
903
546
|
end
|
904
547
|
|
@@ -920,31 +563,13 @@ module Puma
|
|
920
563
|
$stdout.syswrite "#{pid}: === End thread backtrace dump ===\n"
|
921
564
|
end
|
922
565
|
|
923
|
-
if @
|
924
|
-
|
925
|
-
|
926
|
-
while true
|
927
|
-
ios = IO.select @binder.ios, nil, nil, 0
|
928
|
-
break unless ios
|
929
|
-
|
930
|
-
ios.first.each do |sock|
|
931
|
-
begin
|
932
|
-
if io = sock.accept_nonblock
|
933
|
-
count += 1
|
934
|
-
client = Client.new io, @binder.env(sock)
|
935
|
-
@thread_pool << client
|
936
|
-
end
|
937
|
-
rescue SystemCallError
|
938
|
-
end
|
939
|
-
end
|
940
|
-
end
|
941
|
-
|
942
|
-
@events.debug "Drained #{count} additional connections."
|
566
|
+
if @status != :restart
|
567
|
+
@binder.close
|
943
568
|
end
|
944
569
|
|
945
570
|
if @thread_pool
|
946
571
|
if timeout = @options[:force_shutdown_after]
|
947
|
-
@thread_pool.shutdown timeout.
|
572
|
+
@thread_pool.shutdown timeout.to_f
|
948
573
|
else
|
949
574
|
@thread_pool.shutdown
|
950
575
|
end
|
@@ -952,18 +577,16 @@ module Puma
|
|
952
577
|
end
|
953
578
|
|
954
579
|
def notify_safely(message)
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
raise e
|
966
|
-
end
|
580
|
+
@notify << message
|
581
|
+
rescue IOError, NoMethodError, Errno::EPIPE
|
582
|
+
# The server, in another thread, is shutting down
|
583
|
+
Puma::Util.purge_interrupt_queue
|
584
|
+
rescue RuntimeError => e
|
585
|
+
# Temporary workaround for https://bugs.ruby-lang.org/issues/13239
|
586
|
+
if e.message.include?('IOError')
|
587
|
+
Puma::Util.purge_interrupt_queue
|
588
|
+
else
|
589
|
+
raise e
|
967
590
|
end
|
968
591
|
end
|
969
592
|
private :notify_safely
|
@@ -981,39 +604,24 @@ module Puma
|
|
981
604
|
@thread.join if @thread && sync
|
982
605
|
end
|
983
606
|
|
984
|
-
def begin_restart
|
607
|
+
def begin_restart(sync=false)
|
985
608
|
notify_safely(RESTART_COMMAND)
|
609
|
+
@thread.join if @thread && sync
|
986
610
|
end
|
987
611
|
|
988
|
-
def
|
989
|
-
|
990
|
-
while true
|
991
|
-
begin
|
992
|
-
n = io.syswrite str
|
993
|
-
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
994
|
-
if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
|
995
|
-
raise ConnectionError, "Socket timeout writing data"
|
996
|
-
end
|
997
|
-
|
998
|
-
retry
|
999
|
-
rescue Errno::EPIPE, SystemCallError, IOError
|
1000
|
-
raise ConnectionError, "Socket timeout writing data"
|
1001
|
-
end
|
1002
|
-
|
1003
|
-
return if n == str.bytesize
|
1004
|
-
str = str.byteslice(n..-1)
|
1005
|
-
end
|
612
|
+
def shutting_down?
|
613
|
+
@status == :stop || @status == :restart
|
1006
614
|
end
|
1007
|
-
private :fast_write
|
1008
615
|
|
1009
|
-
|
616
|
+
# List of methods invoked by #stats.
|
617
|
+
# @version 5.0.0
|
618
|
+
STAT_METHODS = [:backlog, :running, :pool_capacity, :max_threads, :requests_count].freeze
|
1010
619
|
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
@status == :stop || @status == :restart
|
620
|
+
# Returns a hash of stats about the running server for reporting purposes.
|
621
|
+
# @version 5.0.0
|
622
|
+
# @!attribute [r] stats
|
623
|
+
def stats
|
624
|
+
STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
|
1017
625
|
end
|
1018
626
|
end
|
1019
627
|
end
|