puma 3.12.6 → 4.0.0
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 +24 -20
- data/README.md +29 -9
- data/docs/architecture.md +1 -0
- data/docs/deployment.md +24 -4
- 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/restart.md +4 -2
- data/docs/systemd.md +27 -9
- data/ext/puma_http11/PumaHttp11Service.java +2 -0
- data/ext/puma_http11/http11_parser.c +1 -3
- data/ext/puma_http11/http11_parser.rl +1 -3
- data/ext/puma_http11/mini_ssl.c +20 -4
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +11 -4
- data/lib/puma/app/status.rb +3 -2
- data/lib/puma/binder.rb +9 -1
- data/lib/puma/client.rb +45 -35
- data/lib/puma/cluster.rb +38 -14
- data/lib/puma/configuration.rb +2 -1
- data/lib/puma/const.rb +6 -10
- data/lib/puma/control_cli.rb +10 -0
- data/lib/puma/dsl.rb +45 -3
- data/lib/puma/io_buffer.rb +1 -6
- data/lib/puma/launcher.rb +10 -11
- data/lib/puma/minissl.rb +13 -1
- data/lib/puma/reactor.rb +104 -53
- data/lib/puma/runner.rb +1 -1
- data/lib/puma/server.rb +26 -78
- data/lib/puma/single.rb +2 -2
- data/lib/puma/thread_pool.rb +5 -1
- data/lib/puma/util.rb +1 -6
- data/tools/jungle/init.d/puma +5 -5
- metadata +18 -6
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/java_io_buffer.rb +0 -47
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -23,6 +23,7 @@ import javax.net.ssl.SSLPeerUnverifiedException;
|
|
23
23
|
import javax.net.ssl.SSLSession;
|
24
24
|
import java.io.FileInputStream;
|
25
25
|
import java.io.IOException;
|
26
|
+
import java.nio.Buffer;
|
26
27
|
import java.nio.ByteBuffer;
|
27
28
|
import java.security.KeyManagementException;
|
28
29
|
import java.security.KeyStore;
|
@@ -65,7 +66,7 @@ public class MiniSSL extends RubyObject {
|
|
65
66
|
|
66
67
|
public void clear() { buffer.clear(); }
|
67
68
|
public void compact() { buffer.compact(); }
|
68
|
-
public void flip() { buffer.flip(); }
|
69
|
+
public void flip() { ((Buffer) buffer).flip(); }
|
69
70
|
public boolean hasRemaining() { return buffer.hasRemaining(); }
|
70
71
|
public int position() { return buffer.position(); }
|
71
72
|
|
@@ -89,7 +90,7 @@ public class MiniSSL extends RubyObject {
|
|
89
90
|
public void resize(int newCapacity) {
|
90
91
|
if (newCapacity > buffer.capacity()) {
|
91
92
|
ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity);
|
92
|
-
|
93
|
+
flip();
|
93
94
|
dstTmp.put(buffer);
|
94
95
|
buffer = dstTmp;
|
95
96
|
} else {
|
@@ -101,7 +102,7 @@ public class MiniSSL extends RubyObject {
|
|
101
102
|
* Drains the buffer to a ByteList, or returns null for an empty buffer
|
102
103
|
*/
|
103
104
|
public ByteList asByteList() {
|
104
|
-
|
105
|
+
flip();
|
105
106
|
if (!buffer.hasRemaining()) {
|
106
107
|
buffer.clear();
|
107
108
|
return null;
|
@@ -158,7 +159,13 @@ public class MiniSSL extends RubyObject {
|
|
158
159
|
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
159
160
|
engine = sslCtx.createSSLEngine();
|
160
161
|
|
161
|
-
String[] protocols
|
162
|
+
String[] protocols;
|
163
|
+
if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) {
|
164
|
+
protocols = new String[] { "TLSv1.1", "TLSv1.2" };
|
165
|
+
} else {
|
166
|
+
protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
|
167
|
+
}
|
168
|
+
|
162
169
|
engine.setEnabledProtocols(protocols);
|
163
170
|
engine.setUseClientMode(false);
|
164
171
|
|
data/lib/puma/app/status.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
module Puma
|
2
4
|
module App
|
3
5
|
class Status
|
@@ -60,8 +62,7 @@ module Puma
|
|
60
62
|
return rack_response(200, OK_STATUS)
|
61
63
|
|
62
64
|
when /\/gc-stats$/
|
63
|
-
|
64
|
-
return rack_response(200, json)
|
65
|
+
return rack_response(200, GC.stat.to_json)
|
65
66
|
|
66
67
|
when /\/stats$/
|
67
68
|
return rack_response(200, @cli.stats)
|
data/lib/puma/binder.rb
CHANGED
@@ -50,7 +50,13 @@ module Puma
|
|
50
50
|
|
51
51
|
def close
|
52
52
|
@ios.each { |i| i.close }
|
53
|
-
@unix_paths.each
|
53
|
+
@unix_paths.each do |i|
|
54
|
+
# Errno::ENOENT is intermittently raised
|
55
|
+
begin
|
56
|
+
File.unlink i
|
57
|
+
rescue Errno::ENOENT
|
58
|
+
end
|
59
|
+
end
|
54
60
|
end
|
55
61
|
|
56
62
|
def import_from_env
|
@@ -187,6 +193,8 @@ module Puma
|
|
187
193
|
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
|
188
194
|
end
|
189
195
|
|
196
|
+
ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
|
197
|
+
|
190
198
|
if params['verify_mode']
|
191
199
|
ctx.verify_mode = case params['verify_mode']
|
192
200
|
when "peer"
|
data/lib/puma/client.rb
CHANGED
@@ -27,9 +27,10 @@ module Puma
|
|
27
27
|
# For example a web request from a browser or from CURL. This
|
28
28
|
#
|
29
29
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
30
|
+
# by the reactor, that's because the latter is expected to call `#to_io`
|
31
|
+
# on any non-IO objects it polls. For example nio4r internally calls
|
32
|
+
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
33
|
+
# registered.
|
33
34
|
#
|
34
35
|
# Instances of this class are responsible for knowing if
|
35
36
|
# the header and body are fully buffered via the `try_to_finish` method.
|
@@ -54,6 +55,7 @@ module Puma
|
|
54
55
|
@ready = false
|
55
56
|
|
56
57
|
@body = nil
|
58
|
+
@body_read_start = nil
|
57
59
|
@buffer = nil
|
58
60
|
@tempfile = nil
|
59
61
|
|
@@ -64,6 +66,8 @@ module Puma
|
|
64
66
|
|
65
67
|
@peerip = nil
|
66
68
|
@remote_addr_header = nil
|
69
|
+
|
70
|
+
@body_remain = 0
|
67
71
|
end
|
68
72
|
|
69
73
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
@@ -102,6 +106,8 @@ module Puma
|
|
102
106
|
@tempfile = nil
|
103
107
|
@parsed_bytes = 0
|
104
108
|
@ready = false
|
109
|
+
@body_remain = 0
|
110
|
+
@peerip = nil
|
105
111
|
|
106
112
|
if @buffer
|
107
113
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
@@ -114,9 +120,16 @@ module Puma
|
|
114
120
|
end
|
115
121
|
|
116
122
|
return false
|
117
|
-
|
118
|
-
|
119
|
-
|
123
|
+
else
|
124
|
+
begin
|
125
|
+
if fast_check &&
|
126
|
+
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
127
|
+
return try_to_finish
|
128
|
+
end
|
129
|
+
rescue IOError
|
130
|
+
# swallow it
|
131
|
+
end
|
132
|
+
|
120
133
|
end
|
121
134
|
end
|
122
135
|
|
@@ -147,8 +160,11 @@ module Puma
|
|
147
160
|
def decode_chunk(chunk)
|
148
161
|
if @partial_part_left > 0
|
149
162
|
if @partial_part_left <= chunk.size
|
150
|
-
|
163
|
+
if @partial_part_left > 2
|
164
|
+
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
165
|
+
end
|
151
166
|
chunk = chunk[@partial_part_left..-1]
|
167
|
+
@partial_part_left = 0
|
152
168
|
else
|
153
169
|
@body << chunk
|
154
170
|
@partial_part_left -= chunk.size
|
@@ -172,8 +188,7 @@ module Puma
|
|
172
188
|
rest = io.read
|
173
189
|
rest = rest[2..-1] if rest.start_with?("\r\n")
|
174
190
|
@buffer = rest.empty? ? nil : rest
|
175
|
-
|
176
|
-
@ready = true
|
191
|
+
set_ready
|
177
192
|
return true
|
178
193
|
end
|
179
194
|
|
@@ -211,7 +226,7 @@ module Puma
|
|
211
226
|
while true
|
212
227
|
begin
|
213
228
|
chunk = @io.read_nonblock(4096)
|
214
|
-
rescue
|
229
|
+
rescue IO::WaitReadable
|
215
230
|
return false
|
216
231
|
rescue SystemCallError, IOError
|
217
232
|
raise ConnectionError, "Connection error detected during read"
|
@@ -221,8 +236,7 @@ module Puma
|
|
221
236
|
unless chunk
|
222
237
|
@body.close
|
223
238
|
@buffer = nil
|
224
|
-
|
225
|
-
@ready = true
|
239
|
+
set_ready
|
226
240
|
raise EOFError
|
227
241
|
end
|
228
242
|
|
@@ -231,6 +245,8 @@ module Puma
|
|
231
245
|
end
|
232
246
|
|
233
247
|
def setup_body
|
248
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
249
|
+
|
234
250
|
if @env[HTTP_EXPECT] == CONTINUE
|
235
251
|
# TODO allow a hook here to check the headers before
|
236
252
|
# going forward
|
@@ -244,16 +260,8 @@ module Puma
|
|
244
260
|
|
245
261
|
te = @env[TRANSFER_ENCODING2]
|
246
262
|
|
247
|
-
if te
|
248
|
-
|
249
|
-
te.split(",").each do |part|
|
250
|
-
if CHUNKED.casecmp(part.strip) == 0
|
251
|
-
return setup_chunked_body(body)
|
252
|
-
end
|
253
|
-
end
|
254
|
-
elsif CHUNKED.casecmp(te) == 0
|
255
|
-
return setup_chunked_body(body)
|
256
|
-
end
|
263
|
+
if te && CHUNKED.casecmp(te) == 0
|
264
|
+
return setup_chunked_body(body)
|
257
265
|
end
|
258
266
|
|
259
267
|
@chunked_body = false
|
@@ -263,8 +271,7 @@ module Puma
|
|
263
271
|
unless cl
|
264
272
|
@buffer = body.empty? ? nil : body
|
265
273
|
@body = EmptyBody
|
266
|
-
|
267
|
-
@ready = true
|
274
|
+
set_ready
|
268
275
|
return true
|
269
276
|
end
|
270
277
|
|
@@ -273,8 +280,7 @@ module Puma
|
|
273
280
|
if remain <= 0
|
274
281
|
@body = StringIO.new(body)
|
275
282
|
@buffer = nil
|
276
|
-
|
277
|
-
@ready = true
|
283
|
+
set_ready
|
278
284
|
return true
|
279
285
|
end
|
280
286
|
|
@@ -302,15 +308,14 @@ module Puma
|
|
302
308
|
data = @io.read_nonblock(CHUNK_SIZE)
|
303
309
|
rescue Errno::EAGAIN
|
304
310
|
return false
|
305
|
-
rescue SystemCallError, IOError
|
311
|
+
rescue SystemCallError, IOError, EOFError
|
306
312
|
raise ConnectionError, "Connection error detected during read"
|
307
313
|
end
|
308
314
|
|
309
315
|
# No data means a closed socket
|
310
316
|
unless data
|
311
317
|
@buffer = nil
|
312
|
-
|
313
|
-
@ready = true
|
318
|
+
set_ready
|
314
319
|
raise EOFError
|
315
320
|
end
|
316
321
|
|
@@ -346,8 +351,7 @@ module Puma
|
|
346
351
|
# No data means a closed socket
|
347
352
|
unless data
|
348
353
|
@buffer = nil
|
349
|
-
|
350
|
-
@ready = true
|
354
|
+
set_ready
|
351
355
|
raise EOFError
|
352
356
|
end
|
353
357
|
|
@@ -424,8 +428,7 @@ module Puma
|
|
424
428
|
unless chunk
|
425
429
|
@body.close
|
426
430
|
@buffer = nil
|
427
|
-
|
428
|
-
@ready = true
|
431
|
+
set_ready
|
429
432
|
raise EOFError
|
430
433
|
end
|
431
434
|
|
@@ -434,8 +437,7 @@ module Puma
|
|
434
437
|
if remain <= 0
|
435
438
|
@body.rewind
|
436
439
|
@buffer = nil
|
437
|
-
|
438
|
-
@ready = true
|
440
|
+
set_ready
|
439
441
|
return true
|
440
442
|
end
|
441
443
|
|
@@ -444,6 +446,14 @@ module Puma
|
|
444
446
|
false
|
445
447
|
end
|
446
448
|
|
449
|
+
def set_ready
|
450
|
+
if @body_read_start
|
451
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
452
|
+
end
|
453
|
+
@requests_served += 1
|
454
|
+
@ready = true
|
455
|
+
end
|
456
|
+
|
447
457
|
def write_400
|
448
458
|
begin
|
449
459
|
@io << ERROR_400_RESPONSE
|
data/lib/puma/cluster.rb
CHANGED
@@ -19,8 +19,6 @@ module Puma
|
|
19
19
|
# via the `spawn_workers` method call. Each worker will have it's own
|
20
20
|
# instance of a `Puma::Server`.
|
21
21
|
class Cluster < Runner
|
22
|
-
WORKER_CHECK_INTERVAL = 5
|
23
|
-
|
24
22
|
def initialize(cli, events)
|
25
23
|
super cli, events
|
26
24
|
|
@@ -37,7 +35,35 @@ module Puma
|
|
37
35
|
@workers.each { |x| x.term }
|
38
36
|
|
39
37
|
begin
|
40
|
-
|
38
|
+
if RUBY_VERSION < '2.6'
|
39
|
+
@workers.each do |w|
|
40
|
+
begin
|
41
|
+
Process.waitpid(w.pid)
|
42
|
+
rescue Errno::ECHILD
|
43
|
+
# child is already terminated
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# below code is for a bug in Ruby 2.6+, above waitpid call hangs
|
48
|
+
t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
+
pids = @workers.map(&:pid)
|
50
|
+
loop do
|
51
|
+
pids.reject! do |w_pid|
|
52
|
+
begin
|
53
|
+
if Process.waitpid(w_pid, Process::WNOHANG)
|
54
|
+
log " worker status: #{$?}"
|
55
|
+
true
|
56
|
+
end
|
57
|
+
rescue Errno::ECHILD
|
58
|
+
true # child is already terminated
|
59
|
+
end
|
60
|
+
end
|
61
|
+
break if pids.empty?
|
62
|
+
sleep 0.5
|
63
|
+
end
|
64
|
+
t_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
65
|
+
log format(" worker shutdown time: %6.2f", t_end - t_st)
|
66
|
+
end
|
41
67
|
rescue Interrupt
|
42
68
|
log "! Cancelled waiting for workers"
|
43
69
|
end
|
@@ -183,7 +209,7 @@ module Puma
|
|
183
209
|
def check_workers(force=false)
|
184
210
|
return if !force && @next_check && @next_check >= Time.now
|
185
211
|
|
186
|
-
@next_check = Time.now + WORKER_CHECK_INTERVAL
|
212
|
+
@next_check = Time.now + Const::WORKER_CHECK_INTERVAL
|
187
213
|
|
188
214
|
any = false
|
189
215
|
|
@@ -200,14 +226,9 @@ module Puma
|
|
200
226
|
# during this loop by giving the kernel time to kill them.
|
201
227
|
sleep 1 if any
|
202
228
|
|
203
|
-
|
204
|
-
pid = Process.waitpid(-1, Process::WNOHANG)
|
205
|
-
break unless pid
|
229
|
+
@workers.reject! { |w| Process.waitpid(w.pid, Process::WNOHANG) }
|
206
230
|
|
207
|
-
|
208
|
-
end
|
209
|
-
|
210
|
-
@workers.delete_if(&:dead?)
|
231
|
+
@workers.reject!(&:dead?)
|
211
232
|
|
212
233
|
cull_workers
|
213
234
|
spawn_workers
|
@@ -290,7 +311,7 @@ module Puma
|
|
290
311
|
base_payload = "p#{Process.pid}"
|
291
312
|
|
292
313
|
while true
|
293
|
-
sleep WORKER_CHECK_INTERVAL
|
314
|
+
sleep Const::WORKER_CHECK_INTERVAL
|
294
315
|
begin
|
295
316
|
b = server.backlog || 0
|
296
317
|
r = server.running || 0
|
@@ -390,10 +411,13 @@ module Puma
|
|
390
411
|
log "Early termination of worker"
|
391
412
|
exit! 0
|
392
413
|
else
|
414
|
+
@launcher.close_binder_listeners
|
415
|
+
|
393
416
|
stop_workers
|
394
417
|
stop
|
395
418
|
|
396
|
-
raise
|
419
|
+
raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
|
420
|
+
exit 0 # Clean exit, workers were stopped
|
397
421
|
end
|
398
422
|
end
|
399
423
|
end
|
@@ -487,7 +511,7 @@ module Puma
|
|
487
511
|
|
488
512
|
force_check = false
|
489
513
|
|
490
|
-
res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
|
514
|
+
res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
|
491
515
|
|
492
516
|
if res
|
493
517
|
req = read.read_nonblock(1)
|
data/lib/puma/configuration.rb
CHANGED
@@ -186,7 +186,8 @@ module Puma
|
|
186
186
|
:rackup => DefaultRackup,
|
187
187
|
:logger => STDOUT,
|
188
188
|
:persistent_timeout => Const::PERSISTENT_TIMEOUT,
|
189
|
-
:first_data_timeout => Const::FIRST_DATA_TIMEOUT
|
189
|
+
:first_data_timeout => Const::FIRST_DATA_TIMEOUT,
|
190
|
+
:raise_exception_on_sigterm => true
|
190
191
|
}
|
191
192
|
end
|
192
193
|
|
data/lib/puma/const.rb
CHANGED
@@ -100,8 +100,8 @@ module Puma
|
|
100
100
|
# too taxing on performance.
|
101
101
|
module Const
|
102
102
|
|
103
|
-
PUMA_VERSION = VERSION = "
|
104
|
-
CODE_NAME = "
|
103
|
+
PUMA_VERSION = VERSION = "4.0.0".freeze
|
104
|
+
CODE_NAME = "4 Fast 4 Furious".freeze
|
105
105
|
PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
|
106
106
|
|
107
107
|
FAST_TRACK_KA_TIMEOUT = 0.2
|
@@ -118,13 +118,6 @@ module Puma
|
|
118
118
|
# sending data back
|
119
119
|
WRITE_TIMEOUT = 10
|
120
120
|
|
121
|
-
# How many requests to attempt inline before sending a client back to
|
122
|
-
# the reactor to be subject to normal ordering. The idea here is that
|
123
|
-
# we amortize the cost of going back to the reactor for a well behaved
|
124
|
-
# but very "greedy" client across 10 requests. This prevents a not
|
125
|
-
# well behaved client from monopolizing the thread forever.
|
126
|
-
MAX_FAST_INLINE = 10
|
127
|
-
|
128
121
|
# The original URI requested by the client.
|
129
122
|
REQUEST_URI= 'REQUEST_URI'.freeze
|
130
123
|
REQUEST_PATH = 'REQUEST_PATH'.freeze
|
@@ -228,12 +221,15 @@ module Puma
|
|
228
221
|
COLON = ": ".freeze
|
229
222
|
|
230
223
|
NEWLINE = "\n".freeze
|
231
|
-
HTTP_INJECTION_REGEX = /[\r\n]/.freeze
|
232
224
|
|
233
225
|
HIJACK_P = "rack.hijack?".freeze
|
234
226
|
HIJACK = "rack.hijack".freeze
|
235
227
|
HIJACK_IO = "rack.hijack_io".freeze
|
236
228
|
|
237
229
|
EARLY_HINTS = "rack.early_hints".freeze
|
230
|
+
|
231
|
+
# Mininum interval to checks worker health
|
232
|
+
WORKER_CHECK_INTERVAL = 5
|
233
|
+
|
238
234
|
end
|
239
235
|
end
|
data/lib/puma/control_cli.rb
CHANGED
@@ -206,6 +206,16 @@ module Puma
|
|
206
206
|
when "phased-restart"
|
207
207
|
Process.kill "SIGUSR1", @pid
|
208
208
|
|
209
|
+
when "status"
|
210
|
+
begin
|
211
|
+
Process.kill 0, @pid
|
212
|
+
puts "Puma is started"
|
213
|
+
rescue Errno::ESRCH
|
214
|
+
raise "Puma is not running"
|
215
|
+
end
|
216
|
+
|
217
|
+
return
|
218
|
+
|
209
219
|
else
|
210
220
|
return
|
211
221
|
end
|