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