puma 3.9.1 → 4.3.1

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