puma 4.3.12 → 5.0.0.beta1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +58 -41
  3. data/LICENSE +23 -20
  4. data/README.md +17 -11
  5. data/bin/puma-wild +0 -0
  6. data/docs/architecture.md +0 -0
  7. data/docs/deployment.md +3 -1
  8. data/docs/fork_worker.md +31 -0
  9. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  10. data/docs/images/puma-connection-flow.png +0 -0
  11. data/docs/images/puma-general-arch.png +0 -0
  12. data/docs/jungle/README.md +13 -0
  13. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  14. data/{tools → docs}/jungle/rc.d/puma +0 -0
  15. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  16. data/{tools → docs}/jungle/upstart/README.md +0 -0
  17. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  18. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  19. data/docs/nginx.md +0 -0
  20. data/docs/plugins.md +0 -0
  21. data/docs/restart.md +0 -0
  22. data/docs/signals.md +1 -0
  23. data/docs/systemd.md +1 -63
  24. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  25. data/ext/puma_http11/ext_help.h +0 -0
  26. data/ext/puma_http11/extconf.rb +3 -10
  27. data/ext/puma_http11/http11_parser.c +11 -26
  28. data/ext/puma_http11/http11_parser.h +0 -0
  29. data/ext/puma_http11/http11_parser.java.rl +0 -0
  30. data/ext/puma_http11/http11_parser.rl +1 -3
  31. data/ext/puma_http11/http11_parser_common.rl +1 -1
  32. data/ext/puma_http11/mini_ssl.c +47 -82
  33. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  34. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +46 -48
  35. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +0 -0
  36. data/ext/puma_http11/puma_http11.c +2 -38
  37. data/lib/puma/accept_nonblock.rb +0 -0
  38. data/lib/puma/app/status.rb +16 -5
  39. data/lib/puma/binder.rb +62 -60
  40. data/lib/puma/cli.rb +7 -15
  41. data/lib/puma/client.rb +38 -78
  42. data/lib/puma/cluster.rb +179 -74
  43. data/lib/puma/commonlogger.rb +0 -0
  44. data/lib/puma/configuration.rb +30 -42
  45. data/lib/puma/const.rb +5 -8
  46. data/lib/puma/control_cli.rb +27 -17
  47. data/lib/puma/detect.rb +8 -0
  48. data/lib/puma/dsl.rb +70 -34
  49. data/lib/puma/events.rb +0 -0
  50. data/lib/puma/io_buffer.rb +9 -2
  51. data/lib/puma/jruby_restart.rb +0 -58
  52. data/lib/puma/launcher.rb +41 -29
  53. data/lib/puma/minissl/context_builder.rb +0 -0
  54. data/lib/puma/minissl.rb +13 -8
  55. data/lib/puma/null_io.rb +1 -1
  56. data/lib/puma/plugin/tmp_restart.rb +0 -0
  57. data/lib/puma/plugin.rb +1 -10
  58. data/lib/puma/rack/builder.rb +0 -4
  59. data/lib/puma/rack/urlmap.rb +0 -0
  60. data/lib/puma/rack_default.rb +0 -0
  61. data/lib/puma/reactor.rb +6 -1
  62. data/lib/puma/runner.rb +5 -34
  63. data/lib/puma/server.rb +74 -206
  64. data/lib/puma/single.rb +7 -64
  65. data/lib/puma/state_file.rb +5 -2
  66. data/lib/puma/thread_pool.rb +85 -47
  67. data/lib/puma/util.rb +0 -0
  68. data/lib/puma.rb +4 -0
  69. data/lib/rack/handler/puma.rb +1 -3
  70. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  71. data/tools/trickletest.rb +0 -0
  72. metadata +20 -24
  73. data/docs/tcp_mode.md +0 -96
  74. data/ext/puma_http11/io_buffer.c +0 -155
  75. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  76. data/lib/puma/tcp_logger.rb +0 -41
  77. data/tools/jungle/README.md +0 -19
  78. data/tools/jungle/init.d/README.md +0 -61
  79. data/tools/jungle/init.d/puma +0 -421
  80. data/tools/jungle/init.d/run-puma +0 -18
data/lib/puma/binder.rb CHANGED
@@ -11,7 +11,7 @@ module Puma
11
11
  class Binder
12
12
  include Puma::Const
13
13
 
14
- RACK_VERSION = [1,3].freeze
14
+ RACK_VERSION = [1,6].freeze
15
15
 
16
16
  def initialize(events)
17
17
  @events = events
@@ -43,7 +43,8 @@ module Puma
43
43
  @ios = []
44
44
  end
45
45
 
46
- attr_reader :ios
46
+ attr_reader :ios, :listeners, :unix_paths, :proto_env, :envs, :activated_sockets, :inherited_fds
47
+ attr_writer :ios, :listeners
47
48
 
48
49
  def env(sock)
49
50
  @envs.fetch(sock, @proto_env)
@@ -53,40 +54,39 @@ module Puma
53
54
  @ios.each { |i| i.close }
54
55
  end
55
56
 
56
- def import_from_env
57
- remove = []
58
-
59
- ENV.each do |k,v|
60
- if k =~ /PUMA_INHERIT_\d+/
61
- fd, url = v.split(":", 2)
62
- @inherited_fds[url] = fd.to_i
63
- remove << k
64
- elsif k == 'LISTEN_FDS' && ENV['LISTEN_PID'].to_i == $$
65
- v.to_i.times do |num|
66
- fd = num + 3
67
- sock = TCPServer.for_fd(fd)
68
- begin
69
- key = [ :unix, Socket.unpack_sockaddr_un(sock.getsockname) ]
70
- rescue ArgumentError
71
- port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
72
- if addr =~ /\:/
73
- addr = "[#{addr}]"
74
- end
75
- key = [ :tcp, addr, port ]
76
- end
77
- @activated_sockets[key] = sock
78
- @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
79
- end
80
- remove << k << 'LISTEN_PID'
81
- end
82
- end
57
+ def connected_ports
58
+ ios.map { |io| io.addr[1] }.uniq
59
+ end
60
+
61
+ def create_inherited_fds(env_hash)
62
+ env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v|
63
+ fd, url = v.split(":", 2)
64
+ @inherited_fds[url] = fd.to_i
65
+ end.keys # pass keys back for removal
66
+ end
83
67
 
84
- remove.each do |k|
85
- ENV.delete k
68
+ # systemd socket activation.
69
+ # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4.
70
+ # LISTEN_PID = PID of the service process, aka us
71
+ # see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html
72
+ def create_activated_fds(env_hash)
73
+ return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
74
+ env_hash['LISTEN_FDS'].to_i.times do |index|
75
+ sock = TCPServer.for_fd(socket_activation_fd(index))
76
+ key = begin # Try to parse as a path
77
+ [:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
78
+ rescue ArgumentError # Try to parse as a port/ip
79
+ port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
80
+ addr = "[#{addr}]" if addr =~ /\:/
81
+ [:tcp, addr, port]
82
+ end
83
+ @activated_sockets[key] = sock
84
+ @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
86
85
  end
86
+ ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
87
87
  end
88
88
 
89
- def parse(binds, logger)
89
+ def parse(binds, logger, log_msg = 'Listening')
90
90
  binds.each do |str|
91
91
  uri = URI.parse str
92
92
  case uri.scheme
@@ -113,7 +113,7 @@ module Puma
113
113
  i.local_address.ip_unpack.join(':')
114
114
  end
115
115
 
116
- logger.log "* Listening on tcp://#{addr}"
116
+ logger.log "* #{log_msg} on tcp://#{addr}"
117
117
  end
118
118
  end
119
119
 
@@ -149,7 +149,7 @@ module Puma
149
149
  end
150
150
 
151
151
  io = add_unix_listener path, umask, mode, backlog
152
- logger.log "* Listening on #{str}"
152
+ logger.log "* #{log_msg} on #{str}"
153
153
  end
154
154
 
155
155
  @listeners << [str, io]
@@ -204,12 +204,6 @@ module Puma
204
204
  end
205
205
  end
206
206
 
207
- def loopback_addresses
208
- Socket.ip_address_list.select do |addrinfo|
209
- addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
210
- end.map { |addrinfo| addrinfo.ip_address }.uniq
211
- end
212
-
213
207
  # Tell the server to listen on host +host+, port +port+.
214
208
  # If +optimize_for_latency+ is true (the default) then clients connecting
215
209
  # will be optimized for latency over throughput.
@@ -226,20 +220,17 @@ module Puma
226
220
  end
227
221
 
228
222
  host = host[1..-2] if host and host[0..0] == '['
229
- s = TCPServer.new(host, port)
223
+ tcp_server = TCPServer.new(host, port)
230
224
  if optimize_for_latency
231
- s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
225
+ tcp_server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
232
226
  end
233
- s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
234
- s.listen backlog
235
- @connected_port = s.addr[1]
227
+ tcp_server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
228
+ tcp_server.listen backlog
236
229
 
237
- @ios << s
238
- s
230
+ @ios << tcp_server
231
+ tcp_server
239
232
  end
240
233
 
241
- attr_reader :connected_port
242
-
243
234
  def inherit_tcp_listener(host, port, fd)
244
235
  if fd.kind_of? TCPServer
245
236
  s = fd
@@ -360,26 +351,37 @@ module Puma
360
351
  end
361
352
 
362
353
  def close_listeners
363
- @listeners.each do |l, io|
364
- io.close
354
+ listeners.each do |l, io|
355
+ io.close unless io.closed? # Ruby 2.2 issue
365
356
  uri = URI.parse(l)
366
357
  next unless uri.scheme == 'unix'
367
358
  unix_path = "#{uri.host}#{uri.path}"
368
- File.unlink unix_path if @unix_paths.include? unix_path
359
+ File.unlink unix_path if unix_paths.include? unix_path
369
360
  end
370
361
  end
371
362
 
372
- def close_unix_paths
373
- @unix_paths.each { |up| File.unlink(up) if File.exist? up }
363
+ def redirects_for_restart
364
+ redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h
365
+ redirects[:close_others] = true
366
+ redirects
374
367
  end
375
368
 
376
- def redirects_for_restart
377
- redirects = {:close_others => true}
378
- @listeners.each_with_index do |(l, io), i|
379
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
380
- redirects[io.to_i] = io.to_i
369
+ def redirects_for_restart_env
370
+ listeners.each_with_object({}).with_index do |(listen, memo), i|
371
+ memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}"
381
372
  end
382
- redirects
373
+ end
374
+
375
+ private
376
+
377
+ def loopback_addresses
378
+ Socket.ip_address_list.select do |addrinfo|
379
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
380
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
381
+ end
382
+
383
+ def socket_activation_fd(int)
384
+ int + 3 # 3 is the magic number you add to follow the SA protocol
383
385
  end
384
386
  end
385
387
  end
data/lib/puma/cli.rb CHANGED
@@ -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
@@ -192,10 +188,6 @@ module Puma
192
188
  end
193
189
  end
194
190
 
195
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
196
- user_config.tcp_mode!
197
- end
198
-
199
191
  o.on "--early-hints", "Enable early hints support" do
200
192
  user_config.early_hints
201
193
  end
data/lib/puma/client.rb CHANGED
@@ -23,8 +23,6 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
- class HttpParserError501 < IOError; end
27
-
28
26
  # An instance of this class represents a unique request from a client.
29
27
  # For example, this could be a web request from a browser or from CURL.
30
28
  #
@@ -37,21 +35,7 @@ module Puma
37
35
  # Instances of this class are responsible for knowing if
38
36
  # the header and body are fully buffered via the `try_to_finish` method.
39
37
  # They can be used to "time out" a response via the `timeout_at` reader.
40
- #
41
38
  class Client
42
-
43
- # this tests all values but the last, which must be chunked
44
- ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
-
46
- # chunked body validation
47
- CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
- CHUNK_VALID_ENDING = "\r\n".freeze
49
-
50
- # Content-Length header value validation
51
- CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
52
-
53
- TE_ERR_MSG = 'Invalid Transfer-Encoding'
54
-
55
39
  # The object used for a request with no body. All requests with
56
40
  # no body share this one object since it has no state.
57
41
  EmptyBody = NullIO.new
@@ -254,12 +238,23 @@ module Puma
254
238
  return false unless IO.select([@to_io], nil, nil, 0)
255
239
  try_to_finish
256
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
257
244
  end # IS_JRUBY
258
245
 
259
- def finish
246
+ def finish(timeout)
260
247
  return true if @ready
261
248
  until try_to_finish
262
- 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
263
258
  end
264
259
  true
265
260
  end
@@ -275,7 +270,7 @@ module Puma
275
270
  return @peerip if @peerip
276
271
 
277
272
  if @remote_addr_header
278
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
279
274
  @peerip = hdr
280
275
  return hdr
281
276
  end
@@ -283,6 +278,18 @@ module Puma
283
278
  @peerip ||= @io.peeraddr.last
284
279
  end
285
280
 
281
+ # Returns true if the persistent connection can be closed immediately
282
+ # without waiting for the configured idle/shutdown timeout.
283
+ def can_close?
284
+ # Allow connection to close if it's received at least one full request
285
+ # and hasn't received any data for a future request.
286
+ #
287
+ # From RFC 2616 section 8.1.4:
288
+ # Servers SHOULD always respond to at least one request per connection,
289
+ # if at all possible.
290
+ @requests_served > 0 && @parsed_bytes == 0
291
+ end
292
+
286
293
  private
287
294
 
288
295
  def setup_body
@@ -300,40 +307,16 @@ module Puma
300
307
  body = @parser.body
301
308
 
302
309
  te = @env[TRANSFER_ENCODING2]
303
- if te
304
- te_lwr = te.downcase
305
- if te.include? ','
306
- te_ary = te_lwr.split ','
307
- te_count = te_ary.count CHUNKED
308
- te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
309
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
310
- @env.delete TRANSFER_ENCODING2
311
- return setup_chunked_body body
312
- elsif te_count >= 1
313
- raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
314
- elsif !te_valid
315
- raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
316
- end
317
- elsif te_lwr == CHUNKED
318
- @env.delete TRANSFER_ENCODING2
319
- return setup_chunked_body body
320
- elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
321
- raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
322
- else
323
- raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
324
- end
310
+
311
+ if te && CHUNKED.casecmp(te) == 0
312
+ return setup_chunked_body(body)
325
313
  end
326
314
 
327
315
  @chunked_body = false
328
316
 
329
317
  cl = @env[CONTENT_LENGTH]
330
318
 
331
- if cl
332
- # cannot contain characters that are not \d
333
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
334
- raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
335
- end
336
- else
319
+ unless cl
337
320
  @buffer = body.empty? ? nil : body
338
321
  @body = EmptyBody
339
322
  set_ready
@@ -429,10 +412,7 @@ module Puma
429
412
  raise EOFError
430
413
  end
431
414
 
432
- if decode_chunk(chunk)
433
- @env[CONTENT_LENGTH] = @chunked_content_length
434
- return true
435
- end
415
+ return true if decode_chunk(chunk)
436
416
  end
437
417
  end
438
418
 
@@ -445,28 +425,19 @@ module Puma
445
425
  @body.binmode
446
426
  @tempfile = @body
447
427
 
448
- @chunked_content_length = 0
449
-
450
- if decode_chunk(body)
451
- @env[CONTENT_LENGTH] = @chunked_content_length
452
- return true
453
- end
454
- end
455
-
456
- def write_chunk(str)
457
- @chunked_content_length += @body.write(str)
428
+ return decode_chunk(body)
458
429
  end
459
430
 
460
431
  def decode_chunk(chunk)
461
432
  if @partial_part_left > 0
462
433
  if @partial_part_left <= chunk.size
463
434
  if @partial_part_left > 2
464
- write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
435
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
465
436
  end
466
437
  chunk = chunk[@partial_part_left..-1]
467
438
  @partial_part_left = 0
468
439
  else
469
- write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
440
+ @body << chunk if @partial_part_left > 2 # don't include the last \r\n
470
441
  @partial_part_left -= chunk.size
471
442
  return false
472
443
  end
@@ -482,13 +453,7 @@ module Puma
482
453
  while !io.eof?
483
454
  line = io.gets
484
455
  if line.end_with?("\r\n")
485
- # Puma doesn't process chunk extensions, but should parse if they're
486
- # present, which is the reason for the semicolon regex
487
- chunk_hex = line.strip[/\A[^;]+/]
488
- if chunk_hex =~ CHUNK_SIZE_INVALID
489
- raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
490
- end
491
- len = chunk_hex.to_i(16)
456
+ len = line.strip.to_i(16)
492
457
  if len == 0
493
458
  @in_last_chunk = true
494
459
  @body.rewind
@@ -519,17 +484,12 @@ module Puma
519
484
 
520
485
  case
521
486
  when got == len
522
- # proper chunked segment must end with "\r\n"
523
- if part.end_with? CHUNK_VALID_ENDING
524
- write_chunk(part[0..-3]) # to skip the ending \r\n
525
- else
526
- raise HttpParserError, "Chunk size mismatch"
527
- end
487
+ @body << part[0..-3] # to skip the ending \r\n
528
488
  when got <= len - 2
529
- write_chunk(part)
489
+ @body << part
530
490
  @partial_part_left = len - part.size
531
491
  when got == len - 1 # edge where we get just \r but not \n
532
- write_chunk(part[0..-2])
492
+ @body << part[0..-2]
533
493
  @partial_part_left = len - part.size
534
494
  end
535
495
  else