puma 3.12.6 → 4.3.10

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.

Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +145 -3
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +20 -10
  10. data/docs/restart.md +4 -2
  11. data/docs/systemd.md +27 -9
  12. data/docs/tcp_mode.md +96 -0
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +13 -0
  15. data/ext/puma_http11/http11_parser.c +58 -70
  16. data/ext/puma_http11/http11_parser.java.rl +21 -37
  17. data/ext/puma_http11/http11_parser_common.rl +4 -4
  18. data/ext/puma_http11/mini_ssl.c +78 -8
  19. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  20. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +86 -99
  21. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  23. data/ext/puma_http11/puma_http11.c +3 -0
  24. data/lib/puma/accept_nonblock.rb +7 -1
  25. data/lib/puma/app/status.rb +37 -29
  26. data/lib/puma/binder.rb +38 -60
  27. data/lib/puma/cli.rb +4 -0
  28. data/lib/puma/client.rb +242 -208
  29. data/lib/puma/cluster.rb +53 -30
  30. data/lib/puma/configuration.rb +4 -3
  31. data/lib/puma/const.rb +22 -18
  32. data/lib/puma/control_cli.rb +30 -5
  33. data/lib/puma/dsl.rb +299 -75
  34. data/lib/puma/events.rb +4 -1
  35. data/lib/puma/io_buffer.rb +1 -6
  36. data/lib/puma/launcher.rb +95 -53
  37. data/lib/puma/minissl/context_builder.rb +76 -0
  38. data/lib/puma/minissl.rb +35 -17
  39. data/lib/puma/plugin/tmp_restart.rb +2 -0
  40. data/lib/puma/plugin.rb +5 -2
  41. data/lib/puma/rack/builder.rb +2 -0
  42. data/lib/puma/rack/urlmap.rb +2 -0
  43. data/lib/puma/rack_default.rb +2 -0
  44. data/lib/puma/reactor.rb +110 -57
  45. data/lib/puma/runner.rb +11 -3
  46. data/lib/puma/server.rb +73 -57
  47. data/lib/puma/single.rb +3 -3
  48. data/lib/puma/thread_pool.rb +15 -33
  49. data/lib/puma/util.rb +1 -6
  50. data/lib/puma.rb +8 -0
  51. data/lib/rack/handler/puma.rb +3 -3
  52. data/tools/docker/Dockerfile +16 -0
  53. data/tools/jungle/init.d/puma +6 -6
  54. data/tools/trickletest.rb +0 -1
  55. metadata +26 -13
  56. data/lib/puma/compat.rb +0 -14
  57. data/lib/puma/convenient.rb +0 -25
  58. data/lib/puma/daemon_ext.rb +0 -33
  59. data/lib/puma/delegation.rb +0 -13
  60. data/lib/puma/java_io_buffer.rb +0 -47
  61. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/client.rb CHANGED
@@ -9,8 +9,8 @@ class IO
9
9
  end
10
10
 
11
11
  require 'puma/detect'
12
- require 'puma/delegation'
13
12
  require 'tempfile'
13
+ require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -24,19 +24,24 @@ module Puma
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
26
  # An instance of this class represents a unique request from a client.
27
- # For example a web request from a browser or from CURL. This
27
+ # For example, this could be a web request from a browser or from CURL.
28
28
  #
29
29
  # An instance of `Puma::Client` can be used as if it were an IO object
30
- # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
31
- # This is accomplished by the `to_io` method which gets called on any
32
- # non-IO objects being used with the IO api such as `IO.select.
30
+ # by the reactor. The reactor 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.
36
37
  # They can be used to "time out" a response via the `timeout_at` reader.
37
38
  class Client
39
+ # The object used for a request with no body. All requests with
40
+ # no body share this one object since it has no state.
41
+ EmptyBody = NullIO.new
42
+
38
43
  include Puma::Const
39
- extend Puma::Delegation
44
+ extend Forwardable
40
45
 
41
46
  def initialize(io, env=nil)
42
47
  @io = io
@@ -54,6 +59,7 @@ module Puma
54
59
  @ready = false
55
60
 
56
61
  @body = nil
62
+ @body_read_start = nil
57
63
  @buffer = nil
58
64
  @tempfile = nil
59
65
 
@@ -64,6 +70,10 @@ module Puma
64
70
 
65
71
  @peerip = nil
66
72
  @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
67
77
  end
68
78
 
69
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -73,7 +83,7 @@ module Puma
73
83
 
74
84
  attr_accessor :remote_addr_header
75
85
 
76
- forward :closed?, :@io
86
+ def_delegators :@io, :closed?
77
87
 
78
88
  def inspect
79
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -102,6 +112,9 @@ module Puma
102
112
  @tempfile = nil
103
113
  @parsed_bytes = 0
104
114
  @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
105
118
 
106
119
  if @buffer
107
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -114,9 +127,16 @@ module Puma
114
127
  end
115
128
 
116
129
  return false
117
- elsif fast_check &&
118
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
119
- return try_to_finish
130
+ else
131
+ begin
132
+ if fast_check &&
133
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
134
+ return try_to_finish
135
+ end
136
+ rescue IOError
137
+ # swallow it
138
+ end
139
+
120
140
  end
121
141
  end
122
142
 
@@ -128,189 +148,21 @@ module Puma
128
148
  end
129
149
  end
130
150
 
131
- # The object used for a request with no body. All requests with
132
- # no body share this one object since it has no state.
133
- EmptyBody = NullIO.new
134
-
135
- def setup_chunked_body(body)
136
- @chunked_body = true
137
- @partial_part_left = 0
138
- @prev_chunk = ""
139
-
140
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
141
- @body.binmode
142
- @tempfile = @body
143
-
144
- return decode_chunk(body)
145
- end
146
-
147
- def decode_chunk(chunk)
148
- if @partial_part_left > 0
149
- if @partial_part_left <= chunk.size
150
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
151
- chunk = chunk[@partial_part_left..-1]
152
- else
153
- @body << chunk
154
- @partial_part_left -= chunk.size
155
- return false
156
- end
157
- end
158
-
159
- if @prev_chunk.empty?
160
- io = StringIO.new(chunk)
161
- else
162
- io = StringIO.new(@prev_chunk+chunk)
163
- @prev_chunk = ""
164
- end
165
-
166
- while !io.eof?
167
- line = io.gets
168
- if line.end_with?("\r\n")
169
- len = line.strip.to_i(16)
170
- if len == 0
171
- @body.rewind
172
- rest = io.read
173
- rest = rest[2..-1] if rest.start_with?("\r\n")
174
- @buffer = rest.empty? ? nil : rest
175
- @requests_served += 1
176
- @ready = true
177
- return true
178
- end
179
-
180
- len += 2
181
-
182
- part = io.read(len)
183
-
184
- unless part
185
- @partial_part_left = len
186
- next
187
- end
188
-
189
- got = part.size
190
-
191
- case
192
- when got == len
193
- @body << part[0..-3] # to skip the ending \r\n
194
- when got <= len - 2
195
- @body << part
196
- @partial_part_left = len - part.size
197
- when got == len - 1 # edge where we get just \r but not \n
198
- @body << part[0..-2]
199
- @partial_part_left = len - part.size
200
- end
201
- else
202
- @prev_chunk = line
203
- return false
204
- end
205
- end
206
-
207
- return false
208
- end
209
-
210
- def read_chunked_body
211
- while true
212
- begin
213
- chunk = @io.read_nonblock(4096)
214
- rescue Errno::EAGAIN
215
- return false
216
- rescue SystemCallError, IOError
217
- raise ConnectionError, "Connection error detected during read"
218
- end
219
-
220
- # No chunk means a closed socket
221
- unless chunk
222
- @body.close
223
- @buffer = nil
224
- @requests_served += 1
225
- @ready = true
226
- raise EOFError
227
- end
228
-
229
- return true if decode_chunk(chunk)
230
- end
231
- end
232
-
233
- def setup_body
234
- if @env[HTTP_EXPECT] == CONTINUE
235
- # TODO allow a hook here to check the headers before
236
- # going forward
237
- @io << HTTP_11_100
238
- @io.flush
239
- end
240
-
241
- @read_header = false
242
-
243
- body = @parser.body
244
-
245
- te = @env[TRANSFER_ENCODING2]
246
-
247
- if te
248
- if te.include?(",")
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
257
- end
258
-
259
- @chunked_body = false
260
-
261
- cl = @env[CONTENT_LENGTH]
262
-
263
- unless cl
264
- @buffer = body.empty? ? nil : body
265
- @body = EmptyBody
266
- @requests_served += 1
267
- @ready = true
268
- return true
269
- end
270
-
271
- remain = cl.to_i - body.bytesize
272
-
273
- if remain <= 0
274
- @body = StringIO.new(body)
275
- @buffer = nil
276
- @requests_served += 1
277
- @ready = true
278
- return true
279
- end
280
-
281
- if remain > MAX_BODY
282
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
283
- @body.binmode
284
- @tempfile = @body
285
- else
286
- # The body[0,0] trick is to get an empty string in the same
287
- # encoding as body.
288
- @body = StringIO.new body[0,0]
289
- end
290
-
291
- @body.write body
292
-
293
- @body_remain = remain
294
-
295
- return false
296
- end
297
-
298
151
  def try_to_finish
299
152
  return read_body unless @read_header
300
153
 
301
154
  begin
302
155
  data = @io.read_nonblock(CHUNK_SIZE)
303
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
304
157
  return false
305
- rescue SystemCallError, IOError
158
+ rescue SystemCallError, IOError, EOFError
306
159
  raise ConnectionError, "Connection error detected during read"
307
160
  end
308
161
 
309
162
  # No data means a closed socket
310
163
  unless data
311
164
  @buffer = nil
312
- @requests_served += 1
313
- @ready = true
165
+ set_ready
314
166
  raise EOFError
315
167
  end
316
168
 
@@ -346,8 +198,7 @@ module Puma
346
198
  # No data means a closed socket
347
199
  unless data
348
200
  @buffer = nil
349
- @requests_served += 1
350
- @ready = true
201
+ set_ready
351
202
  raise EOFError
352
203
  end
353
204
 
@@ -397,6 +248,92 @@ module Puma
397
248
  true
398
249
  end
399
250
 
251
+ def write_error(status_code)
252
+ begin
253
+ @io << ERROR_RESPONSE[status_code]
254
+ rescue StandardError
255
+ end
256
+ end
257
+
258
+ def peerip
259
+ return @peerip if @peerip
260
+
261
+ if @remote_addr_header
262
+ hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
263
+ @peerip = hdr
264
+ return hdr
265
+ end
266
+
267
+ @peerip ||= @io.peeraddr.last
268
+ end
269
+
270
+ private
271
+
272
+ def setup_body
273
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
274
+
275
+ if @env[HTTP_EXPECT] == CONTINUE
276
+ # TODO allow a hook here to check the headers before
277
+ # going forward
278
+ @io << HTTP_11_100
279
+ @io.flush
280
+ end
281
+
282
+ @read_header = false
283
+
284
+ body = @parser.body
285
+
286
+ te = @env[TRANSFER_ENCODING2]
287
+
288
+ if te
289
+ if te.include?(",")
290
+ te.split(",").each do |part|
291
+ if CHUNKED.casecmp(part.strip) == 0
292
+ return setup_chunked_body(body)
293
+ end
294
+ end
295
+ elsif CHUNKED.casecmp(te) == 0
296
+ return setup_chunked_body(body)
297
+ end
298
+ end
299
+
300
+ @chunked_body = false
301
+
302
+ cl = @env[CONTENT_LENGTH]
303
+
304
+ unless cl
305
+ @buffer = body.empty? ? nil : body
306
+ @body = EmptyBody
307
+ set_ready
308
+ return true
309
+ end
310
+
311
+ remain = cl.to_i - body.bytesize
312
+
313
+ if remain <= 0
314
+ @body = StringIO.new(body)
315
+ @buffer = nil
316
+ set_ready
317
+ return true
318
+ end
319
+
320
+ if remain > MAX_BODY
321
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
322
+ @body.binmode
323
+ @tempfile = @body
324
+ else
325
+ # The body[0,0] trick is to get an empty string in the same
326
+ # encoding as body.
327
+ @body = StringIO.new body[0,0]
328
+ end
329
+
330
+ @body.write body
331
+
332
+ @body_remain = remain
333
+
334
+ return false
335
+ end
336
+
400
337
  def read_body
401
338
  if @chunked_body
402
339
  return read_chunked_body
@@ -414,7 +351,7 @@ module Puma
414
351
 
415
352
  begin
416
353
  chunk = @io.read_nonblock(want)
417
- rescue Errno::EAGAIN
354
+ rescue IO::WaitReadable
418
355
  return false
419
356
  rescue SystemCallError, IOError
420
357
  raise ConnectionError, "Connection error detected during read"
@@ -424,8 +361,7 @@ module Puma
424
361
  unless chunk
425
362
  @body.close
426
363
  @buffer = nil
427
- @requests_served += 1
428
- @ready = true
364
+ set_ready
429
365
  raise EOFError
430
366
  end
431
367
 
@@ -434,8 +370,7 @@ module Puma
434
370
  if remain <= 0
435
371
  @body.rewind
436
372
  @buffer = nil
437
- @requests_served += 1
438
- @ready = true
373
+ set_ready
439
374
  return true
440
375
  end
441
376
 
@@ -444,37 +379,136 @@ module Puma
444
379
  false
445
380
  end
446
381
 
447
- def write_400
448
- begin
449
- @io << ERROR_400_RESPONSE
450
- rescue StandardError
382
+ def read_chunked_body
383
+ while true
384
+ begin
385
+ chunk = @io.read_nonblock(4096)
386
+ rescue IO::WaitReadable
387
+ return false
388
+ rescue SystemCallError, IOError
389
+ raise ConnectionError, "Connection error detected during read"
390
+ end
391
+
392
+ # No chunk means a closed socket
393
+ unless chunk
394
+ @body.close
395
+ @buffer = nil
396
+ set_ready
397
+ raise EOFError
398
+ end
399
+
400
+ if decode_chunk(chunk)
401
+ @env[CONTENT_LENGTH] = @chunked_content_length
402
+ return true
403
+ end
451
404
  end
452
405
  end
453
406
 
454
- def write_408
455
- begin
456
- @io << ERROR_408_RESPONSE
457
- rescue StandardError
407
+ def setup_chunked_body(body)
408
+ @chunked_body = true
409
+ @partial_part_left = 0
410
+ @prev_chunk = ""
411
+
412
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
413
+ @body.binmode
414
+ @tempfile = @body
415
+
416
+ @chunked_content_length = 0
417
+
418
+ if decode_chunk(body)
419
+ @env[CONTENT_LENGTH] = @chunked_content_length
420
+ return true
458
421
  end
459
422
  end
460
423
 
461
- def write_500
462
- begin
463
- @io << ERROR_500_RESPONSE
464
- rescue StandardError
465
- end
424
+ def write_chunk(str)
425
+ @chunked_content_length += @body.write(str)
466
426
  end
467
427
 
468
- def peerip
469
- return @peerip if @peerip
428
+ def decode_chunk(chunk)
429
+ if @partial_part_left > 0
430
+ if @partial_part_left <= chunk.size
431
+ if @partial_part_left > 2
432
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
433
+ end
434
+ chunk = chunk[@partial_part_left..-1]
435
+ @partial_part_left = 0
436
+ else
437
+ write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
438
+ @partial_part_left -= chunk.size
439
+ return false
440
+ end
441
+ end
470
442
 
471
- if @remote_addr_header
472
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
473
- @peerip = hdr
474
- return hdr
443
+ if @prev_chunk.empty?
444
+ io = StringIO.new(chunk)
445
+ else
446
+ io = StringIO.new(@prev_chunk+chunk)
447
+ @prev_chunk = ""
475
448
  end
476
449
 
477
- @peerip ||= @io.peeraddr.last
450
+ while !io.eof?
451
+ line = io.gets
452
+ if line.end_with?("\r\n")
453
+ len = line.strip.to_i(16)
454
+ if len == 0
455
+ @in_last_chunk = true
456
+ @body.rewind
457
+ rest = io.read
458
+ last_crlf_size = "\r\n".bytesize
459
+ if rest.bytesize < last_crlf_size
460
+ @buffer = nil
461
+ @partial_part_left = last_crlf_size - rest.bytesize
462
+ return false
463
+ else
464
+ @buffer = rest[last_crlf_size..-1]
465
+ @buffer = nil if @buffer.empty?
466
+ set_ready
467
+ return true
468
+ end
469
+ end
470
+
471
+ len += 2
472
+
473
+ part = io.read(len)
474
+
475
+ unless part
476
+ @partial_part_left = len
477
+ next
478
+ end
479
+
480
+ got = part.size
481
+
482
+ case
483
+ when got == len
484
+ write_chunk(part[0..-3]) # to skip the ending \r\n
485
+ when got <= len - 2
486
+ write_chunk(part)
487
+ @partial_part_left = len - part.size
488
+ when got == len - 1 # edge where we get just \r but not \n
489
+ write_chunk(part[0..-2])
490
+ @partial_part_left = len - part.size
491
+ end
492
+ else
493
+ @prev_chunk = line
494
+ return false
495
+ end
496
+ end
497
+
498
+ if @in_last_chunk
499
+ set_ready
500
+ true
501
+ else
502
+ false
503
+ end
504
+ end
505
+
506
+ def set_ready
507
+ if @body_read_start
508
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
509
+ end
510
+ @requests_served += 1
511
+ @ready = true
478
512
  end
479
513
  end
480
514
  end