puma 4.1.1 → 5.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.

Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +149 -10
  3. data/LICENSE +23 -20
  4. data/README.md +30 -46
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/plugins.md +20 -10
  16. data/docs/signals.md +7 -6
  17. data/docs/systemd.md +1 -63
  18. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  19. data/ext/puma_http11/extconf.rb +6 -0
  20. data/ext/puma_http11/http11_parser.c +40 -63
  21. data/ext/puma_http11/http11_parser.java.rl +21 -37
  22. data/ext/puma_http11/http11_parser.rl +3 -1
  23. data/ext/puma_http11/http11_parser_common.rl +3 -3
  24. data/ext/puma_http11/mini_ssl.c +15 -2
  25. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  28. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  29. data/ext/puma_http11/puma_http11.c +9 -38
  30. data/lib/puma.rb +23 -0
  31. data/lib/puma/app/status.rb +46 -30
  32. data/lib/puma/binder.rb +112 -124
  33. data/lib/puma/cli.rb +11 -15
  34. data/lib/puma/client.rb +250 -209
  35. data/lib/puma/cluster.rb +203 -85
  36. data/lib/puma/commonlogger.rb +2 -2
  37. data/lib/puma/configuration.rb +31 -42
  38. data/lib/puma/const.rb +24 -19
  39. data/lib/puma/control_cli.rb +46 -17
  40. data/lib/puma/detect.rb +17 -0
  41. data/lib/puma/dsl.rb +162 -70
  42. data/lib/puma/error_logger.rb +97 -0
  43. data/lib/puma/events.rb +35 -31
  44. data/lib/puma/io_buffer.rb +9 -2
  45. data/lib/puma/jruby_restart.rb +0 -58
  46. data/lib/puma/launcher.rb +117 -58
  47. data/lib/puma/minissl.rb +60 -18
  48. data/lib/puma/minissl/context_builder.rb +73 -0
  49. data/lib/puma/null_io.rb +1 -1
  50. data/lib/puma/plugin.rb +6 -12
  51. data/lib/puma/rack/builder.rb +0 -4
  52. data/lib/puma/reactor.rb +16 -9
  53. data/lib/puma/runner.rb +11 -32
  54. data/lib/puma/server.rb +173 -193
  55. data/lib/puma/single.rb +7 -64
  56. data/lib/puma/state_file.rb +6 -3
  57. data/lib/puma/thread_pool.rb +104 -81
  58. data/lib/rack/handler/puma.rb +1 -5
  59. data/tools/Dockerfile +16 -0
  60. data/tools/trickletest.rb +0 -1
  61. metadata +23 -24
  62. data/ext/puma_http11/io_buffer.c +0 -155
  63. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  64. data/lib/puma/convenient.rb +0 -25
  65. data/lib/puma/daemon_ext.rb +0 -33
  66. data/lib/puma/delegation.rb +0 -13
  67. data/lib/puma/tcp_logger.rb +0 -41
  68. data/tools/jungle/README.md +0 -19
  69. data/tools/jungle/init.d/README.md +0 -61
  70. data/tools/jungle/init.d/puma +0 -421
  71. data/tools/jungle/init.d/run-puma +0 -18
@@ -80,7 +80,7 @@ module Puma
80
80
  @launcher.run
81
81
  end
82
82
 
83
- private
83
+ private
84
84
  def unsupported(str)
85
85
  @events.error(str)
86
86
  raise UnsupportedOption
@@ -112,21 +112,11 @@ module Puma
112
112
  configure_control_url(arg)
113
113
  end
114
114
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
- configure_control_url(arg)
118
- end
119
-
120
115
  o.on "--control-token TOKEN",
121
116
  "The token to use as authentication for the control server" do |arg|
122
117
  @control_options[:auth_token] = arg
123
118
  end
124
119
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
120
  o.on "--debug", "Log lowlevel debugging information" do
131
121
  user_config.debug
132
122
  end
@@ -140,6 +130,12 @@ module Puma
140
130
  user_config.environment arg
141
131
  end
142
132
 
133
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
134
+ "Fork new workers from existing worker. Cluster mode only",
135
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
136
+ user_config.fork_worker(*args.compact)
137
+ end
138
+
143
139
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
140
  $LOAD_PATH.unshift(*arg.split(':'))
145
141
  end
@@ -161,6 +157,10 @@ module Puma
161
157
  user_config.prune_bundler
162
158
  end
163
159
 
160
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
161
+ user_config.extra_runtime_dependencies arg.split(',')
162
+ end
163
+
164
164
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
165
165
  user_config.quiet
166
166
  end
@@ -188,10 +188,6 @@ module Puma
188
188
  end
189
189
  end
190
190
 
191
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
192
- user_config.tcp_mode!
193
- end
194
-
195
191
  o.on "--early-hints", "Enable early hints support" do
196
192
  user_config.early_hints
197
193
  end
@@ -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,11 +24,11 @@ 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
- # 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
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
32
  # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
33
  # registered.
34
34
  #
@@ -36,8 +36,12 @@ module Puma
36
36
  # the header and body are fully buffered via the `try_to_finish` method.
37
37
  # They can be used to "time out" a response via the `timeout_at` reader.
38
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
+
39
43
  include Puma::Const
40
- extend Puma::Delegation
44
+ extend Forwardable
41
45
 
42
46
  def initialize(io, env=nil)
43
47
  @io = io
@@ -79,7 +83,7 @@ module Puma
79
83
 
80
84
  attr_accessor :remote_addr_header
81
85
 
82
- forward :closed?, :@io
86
+ def_delegators :@io, :closed?
83
87
 
84
88
  def inspect
85
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -144,185 +148,12 @@ module Puma
144
148
  end
145
149
  end
146
150
 
147
- # The object used for a request with no body. All requests with
148
- # no body share this one object since it has no state.
149
- EmptyBody = NullIO.new
150
-
151
- def setup_chunked_body(body)
152
- @chunked_body = true
153
- @partial_part_left = 0
154
- @prev_chunk = ""
155
-
156
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
157
- @body.binmode
158
- @tempfile = @body
159
-
160
- return decode_chunk(body)
161
- end
162
-
163
- def decode_chunk(chunk)
164
- if @partial_part_left > 0
165
- if @partial_part_left <= chunk.size
166
- if @partial_part_left > 2
167
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
168
- end
169
- chunk = chunk[@partial_part_left..-1]
170
- @partial_part_left = 0
171
- else
172
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
173
- @partial_part_left -= chunk.size
174
- return false
175
- end
176
- end
177
-
178
- if @prev_chunk.empty?
179
- io = StringIO.new(chunk)
180
- else
181
- io = StringIO.new(@prev_chunk+chunk)
182
- @prev_chunk = ""
183
- end
184
-
185
- while !io.eof?
186
- line = io.gets
187
- if line.end_with?("\r\n")
188
- len = line.strip.to_i(16)
189
- if len == 0
190
- @in_last_chunk = true
191
- @body.rewind
192
- rest = io.read
193
- last_crlf_size = "\r\n".bytesize
194
- if rest.bytesize < last_crlf_size
195
- @buffer = nil
196
- @partial_part_left = last_crlf_size - rest.bytesize
197
- return false
198
- else
199
- @buffer = rest[last_crlf_size..-1]
200
- @buffer = nil if @buffer.empty?
201
- set_ready
202
- return true
203
- end
204
- end
205
-
206
- len += 2
207
-
208
- part = io.read(len)
209
-
210
- unless part
211
- @partial_part_left = len
212
- next
213
- end
214
-
215
- got = part.size
216
-
217
- case
218
- when got == len
219
- @body << part[0..-3] # to skip the ending \r\n
220
- when got <= len - 2
221
- @body << part
222
- @partial_part_left = len - part.size
223
- when got == len - 1 # edge where we get just \r but not \n
224
- @body << part[0..-2]
225
- @partial_part_left = len - part.size
226
- end
227
- else
228
- @prev_chunk = line
229
- return false
230
- end
231
- end
232
-
233
- if @in_last_chunk
234
- set_ready
235
- true
236
- else
237
- false
238
- end
239
- end
240
-
241
- def read_chunked_body
242
- while true
243
- begin
244
- chunk = @io.read_nonblock(4096)
245
- rescue IO::WaitReadable
246
- return false
247
- rescue SystemCallError, IOError
248
- raise ConnectionError, "Connection error detected during read"
249
- end
250
-
251
- # No chunk means a closed socket
252
- unless chunk
253
- @body.close
254
- @buffer = nil
255
- set_ready
256
- raise EOFError
257
- end
258
-
259
- return true if decode_chunk(chunk)
260
- end
261
- end
262
-
263
- def setup_body
264
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
265
-
266
- if @env[HTTP_EXPECT] == CONTINUE
267
- # TODO allow a hook here to check the headers before
268
- # going forward
269
- @io << HTTP_11_100
270
- @io.flush
271
- end
272
-
273
- @read_header = false
274
-
275
- body = @parser.body
276
-
277
- te = @env[TRANSFER_ENCODING2]
278
-
279
- if te && CHUNKED.casecmp(te) == 0
280
- return setup_chunked_body(body)
281
- end
282
-
283
- @chunked_body = false
284
-
285
- cl = @env[CONTENT_LENGTH]
286
-
287
- unless cl
288
- @buffer = body.empty? ? nil : body
289
- @body = EmptyBody
290
- set_ready
291
- return true
292
- end
293
-
294
- remain = cl.to_i - body.bytesize
295
-
296
- if remain <= 0
297
- @body = StringIO.new(body)
298
- @buffer = nil
299
- set_ready
300
- return true
301
- end
302
-
303
- if remain > MAX_BODY
304
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
305
- @body.binmode
306
- @tempfile = @body
307
- else
308
- # The body[0,0] trick is to get an empty string in the same
309
- # encoding as body.
310
- @body = StringIO.new body[0,0]
311
- end
312
-
313
- @body.write body
314
-
315
- @body_remain = remain
316
-
317
- return false
318
- end
319
-
320
151
  def try_to_finish
321
152
  return read_body unless @read_header
322
153
 
323
154
  begin
324
155
  data = @io.read_nonblock(CHUNK_SIZE)
325
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
326
157
  return false
327
158
  rescue SystemCallError, IOError, EOFError
328
159
  raise ConnectionError, "Connection error detected during read"
@@ -407,16 +238,127 @@ module Puma
407
238
  return false unless IO.select([@to_io], nil, nil, 0)
408
239
  try_to_finish
409
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
410
244
  end # IS_JRUBY
411
245
 
412
- def finish
246
+ def finish(timeout)
413
247
  return true if @ready
414
248
  until try_to_finish
415
- IO.select([@to_io], nil, nil)
249
+ can_read = begin
250
+ IO.select([@to_io], nil, nil, timeout)
251
+ rescue ThreadPool::ForceShutdown
252
+ nil
253
+ end
254
+ unless can_read
255
+ write_error(408) if in_data_phase
256
+ raise ConnectionError
257
+ end
416
258
  end
417
259
  true
418
260
  end
419
261
 
262
+ def write_error(status_code)
263
+ begin
264
+ @io << ERROR_RESPONSE[status_code]
265
+ rescue StandardError
266
+ end
267
+ end
268
+
269
+ def peerip
270
+ return @peerip if @peerip
271
+
272
+ if @remote_addr_header
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
274
+ @peerip = hdr
275
+ return hdr
276
+ end
277
+
278
+ @peerip ||= @io.peeraddr.last
279
+ end
280
+
281
+ # Returns true if the persistent connection can be closed immediately
282
+ # without waiting for the configured idle/shutdown timeout.
283
+ # @version 5.0.0
284
+ #
285
+ def can_close?
286
+ # Allow connection to close if it's received at least one full request
287
+ # and hasn't received any data for a future request.
288
+ #
289
+ # From RFC 2616 section 8.1.4:
290
+ # Servers SHOULD always respond to at least one request per connection,
291
+ # if at all possible.
292
+ @requests_served > 0 && @parsed_bytes == 0
293
+ end
294
+
295
+ private
296
+
297
+ def setup_body
298
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
299
+
300
+ if @env[HTTP_EXPECT] == CONTINUE
301
+ # TODO allow a hook here to check the headers before
302
+ # going forward
303
+ @io << HTTP_11_100
304
+ @io.flush
305
+ end
306
+
307
+ @read_header = false
308
+
309
+ body = @parser.body
310
+
311
+ te = @env[TRANSFER_ENCODING2]
312
+
313
+ if te
314
+ if te.include?(",")
315
+ te.split(",").each do |part|
316
+ if CHUNKED.casecmp(part.strip) == 0
317
+ return setup_chunked_body(body)
318
+ end
319
+ end
320
+ elsif CHUNKED.casecmp(te) == 0
321
+ return setup_chunked_body(body)
322
+ end
323
+ end
324
+
325
+ @chunked_body = false
326
+
327
+ cl = @env[CONTENT_LENGTH]
328
+
329
+ unless cl
330
+ @buffer = body.empty? ? nil : body
331
+ @body = EmptyBody
332
+ set_ready
333
+ return true
334
+ end
335
+
336
+ remain = cl.to_i - body.bytesize
337
+
338
+ if remain <= 0
339
+ @body = StringIO.new(body)
340
+ @buffer = nil
341
+ set_ready
342
+ return true
343
+ end
344
+
345
+ if remain > MAX_BODY
346
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
347
+ @body.binmode
348
+ @tempfile = @body
349
+ else
350
+ # The body[0,0] trick is to get an empty string in the same
351
+ # encoding as body.
352
+ @body = StringIO.new body[0,0]
353
+ end
354
+
355
+ @body.write body
356
+
357
+ @body_remain = remain
358
+
359
+ return false
360
+ end
361
+
420
362
  def read_body
421
363
  if @chunked_body
422
364
  return read_chunked_body
@@ -434,7 +376,7 @@ module Puma
434
376
 
435
377
  begin
436
378
  chunk = @io.read_nonblock(want)
437
- rescue Errno::EAGAIN
379
+ rescue IO::WaitReadable
438
380
  return false
439
381
  rescue SystemCallError, IOError
440
382
  raise ConnectionError, "Connection error detected during read"
@@ -462,45 +404,144 @@ module Puma
462
404
  false
463
405
  end
464
406
 
465
- def set_ready
466
- if @body_read_start
467
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
407
+ def read_chunked_body
408
+ while true
409
+ begin
410
+ chunk = @io.read_nonblock(4096)
411
+ rescue IO::WaitReadable
412
+ return false
413
+ rescue SystemCallError, IOError
414
+ raise ConnectionError, "Connection error detected during read"
415
+ end
416
+
417
+ # No chunk means a closed socket
418
+ unless chunk
419
+ @body.close
420
+ @buffer = nil
421
+ set_ready
422
+ raise EOFError
423
+ end
424
+
425
+ if decode_chunk(chunk)
426
+ @env[CONTENT_LENGTH] = @chunked_content_length
427
+ return true
428
+ end
468
429
  end
469
- @requests_served += 1
470
- @ready = true
471
430
  end
472
431
 
473
- def write_400
474
- begin
475
- @io << ERROR_400_RESPONSE
476
- rescue StandardError
432
+ def setup_chunked_body(body)
433
+ @chunked_body = true
434
+ @partial_part_left = 0
435
+ @prev_chunk = ""
436
+
437
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
438
+ @body.binmode
439
+ @tempfile = @body
440
+ @chunked_content_length = 0
441
+
442
+ if decode_chunk(body)
443
+ @env[CONTENT_LENGTH] = @chunked_content_length
444
+ return true
477
445
  end
478
446
  end
479
447
 
480
- def write_408
481
- begin
482
- @io << ERROR_408_RESPONSE
483
- rescue StandardError
484
- end
448
+ # @version 5.0.0
449
+ def write_chunk(str)
450
+ @chunked_content_length += @body.write(str)
485
451
  end
486
452
 
487
- def write_500
488
- begin
489
- @io << ERROR_500_RESPONSE
490
- rescue StandardError
453
+ def decode_chunk(chunk)
454
+ if @partial_part_left > 0
455
+ if @partial_part_left <= chunk.size
456
+ if @partial_part_left > 2
457
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
458
+ end
459
+ chunk = chunk[@partial_part_left..-1]
460
+ @partial_part_left = 0
461
+ else
462
+ if @partial_part_left > 2
463
+ if @partial_part_left == chunk.size + 1
464
+ # Don't include the last \r
465
+ write_chunk(chunk[0..(@partial_part_left-3)])
466
+ else
467
+ # don't include the last \r\n
468
+ write_chunk(chunk)
469
+ end
470
+ end
471
+ @partial_part_left -= chunk.size
472
+ return false
473
+ end
491
474
  end
492
- end
493
475
 
494
- def peerip
495
- return @peerip if @peerip
476
+ if @prev_chunk.empty?
477
+ io = StringIO.new(chunk)
478
+ else
479
+ io = StringIO.new(@prev_chunk+chunk)
480
+ @prev_chunk = ""
481
+ end
496
482
 
497
- if @remote_addr_header
498
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
499
- @peerip = hdr
500
- return hdr
483
+ while !io.eof?
484
+ line = io.gets
485
+ if line.end_with?("\r\n")
486
+ len = line.strip.to_i(16)
487
+ if len == 0
488
+ @in_last_chunk = true
489
+ @body.rewind
490
+ rest = io.read
491
+ last_crlf_size = "\r\n".bytesize
492
+ if rest.bytesize < last_crlf_size
493
+ @buffer = nil
494
+ @partial_part_left = last_crlf_size - rest.bytesize
495
+ return false
496
+ else
497
+ @buffer = rest[last_crlf_size..-1]
498
+ @buffer = nil if @buffer.empty?
499
+ set_ready
500
+ return true
501
+ end
502
+ end
503
+
504
+ len += 2
505
+
506
+ part = io.read(len)
507
+
508
+ unless part
509
+ @partial_part_left = len
510
+ next
511
+ end
512
+
513
+ got = part.size
514
+
515
+ case
516
+ when got == len
517
+ write_chunk(part[0..-3]) # to skip the ending \r\n
518
+ when got <= len - 2
519
+ write_chunk(part)
520
+ @partial_part_left = len - part.size
521
+ when got == len - 1 # edge where we get just \r but not \n
522
+ write_chunk(part[0..-2])
523
+ @partial_part_left = len - part.size
524
+ end
525
+ else
526
+ @prev_chunk = line
527
+ return false
528
+ end
501
529
  end
502
530
 
503
- @peerip ||= @io.peeraddr.last
531
+ if @in_last_chunk
532
+ set_ready
533
+ true
534
+ else
535
+ false
536
+ end
537
+ end
538
+
539
+ def set_ready
540
+ if @body_read_start
541
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
542
+ end
543
+ @requests_served += 1
544
+ @ready = true
504
545
  end
505
546
  end
506
547
  end