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