puma 4.3.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +94 -3
  3. data/LICENSE +23 -20
  4. data/README.md +26 -13
  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/signals.md +7 -6
  16. data/docs/systemd.md +1 -63
  17. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  18. data/ext/puma_http11/extconf.rb +4 -3
  19. data/ext/puma_http11/http11_parser.c +3 -1
  20. data/ext/puma_http11/http11_parser.rl +3 -1
  21. data/ext/puma_http11/mini_ssl.c +15 -2
  22. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  23. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  24. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  25. data/ext/puma_http11/puma_http11.c +7 -38
  26. data/lib/puma.rb +17 -0
  27. data/lib/puma/app/status.rb +18 -3
  28. data/lib/puma/binder.rb +88 -68
  29. data/lib/puma/cli.rb +7 -15
  30. data/lib/puma/client.rb +67 -14
  31. data/lib/puma/cluster.rb +191 -74
  32. data/lib/puma/commonlogger.rb +2 -2
  33. data/lib/puma/configuration.rb +31 -42
  34. data/lib/puma/const.rb +4 -3
  35. data/lib/puma/control_cli.rb +29 -17
  36. data/lib/puma/detect.rb +17 -0
  37. data/lib/puma/dsl.rb +144 -70
  38. data/lib/puma/error_logger.rb +97 -0
  39. data/lib/puma/events.rb +35 -31
  40. data/lib/puma/io_buffer.rb +9 -2
  41. data/lib/puma/jruby_restart.rb +0 -58
  42. data/lib/puma/launcher.rb +49 -31
  43. data/lib/puma/minissl.rb +60 -18
  44. data/lib/puma/minissl/context_builder.rb +0 -3
  45. data/lib/puma/null_io.rb +1 -1
  46. data/lib/puma/plugin.rb +1 -10
  47. data/lib/puma/rack/builder.rb +0 -4
  48. data/lib/puma/reactor.rb +9 -4
  49. data/lib/puma/runner.rb +8 -36
  50. data/lib/puma/server.rb +149 -186
  51. data/lib/puma/single.rb +7 -64
  52. data/lib/puma/state_file.rb +6 -3
  53. data/lib/puma/thread_pool.rb +94 -49
  54. data/lib/rack/handler/puma.rb +1 -3
  55. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  56. metadata +21 -23
  57. data/docs/tcp_mode.md +0 -96
  58. data/ext/puma_http11/io_buffer.c +0 -155
  59. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  60. data/lib/puma/tcp_logger.rb +0 -41
  61. data/tools/jungle/README.md +0 -19
  62. data/tools/jungle/init.d/README.md +0 -61
  63. data/tools/jungle/init.d/puma +0 -421
  64. data/tools/jungle/init.d/run-puma +0 -18
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Puma
6
4
  module App
7
5
  # Check out {#call}'s source code to see what actions this web application
@@ -19,6 +17,10 @@ module Puma
19
17
  return rack_response(403, 'Invalid auth token', 'text/plain')
20
18
  end
21
19
 
20
+ if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
21
+ require 'json'
22
+ end
23
+
22
24
  case env['PATH_INFO']
23
25
  when /\/stop$/
24
26
  @cli.stop
@@ -54,7 +56,20 @@ module Puma
54
56
  rack_response(200, GC.stat.to_json)
55
57
 
56
58
  when /\/stats$/
57
- rack_response(200, @cli.stats)
59
+ rack_response(200, @cli.stats.to_json)
60
+
61
+ when /\/thread-backtraces$/
62
+ backtraces = []
63
+ @cli.thread_status do |name, backtrace|
64
+ backtraces << { name: name, backtrace: backtrace }
65
+ end
66
+
67
+ rack_response(200, backtraces.to_json)
68
+
69
+ when /\/refork$/
70
+ Process.kill "SIGURG", $$
71
+ rack_response(200, OK_STATUS)
72
+
58
73
  else
59
74
  rack_response 404, "Unsupported action", 'text/plain'
60
75
  end
@@ -5,15 +5,22 @@ require 'socket'
5
5
 
6
6
  require 'puma/const'
7
7
  require 'puma/util'
8
- require 'puma/minissl/context_builder'
8
+ require 'puma/configuration'
9
9
 
10
10
  module Puma
11
+
12
+ if HAS_SSL
13
+ require 'puma/minissl'
14
+ require 'puma/minissl/context_builder'
15
+ require 'puma/accept_nonblock'
16
+ end
17
+
11
18
  class Binder
12
19
  include Puma::Const
13
20
 
14
- RACK_VERSION = [1,3].freeze
21
+ RACK_VERSION = [1,6].freeze
15
22
 
16
- def initialize(events)
23
+ def initialize(events, conf = Configuration.new)
17
24
  @events = events
18
25
  @listeners = []
19
26
  @inherited_fds = {}
@@ -23,8 +30,8 @@ module Puma
23
30
  @proto_env = {
24
31
  "rack.version".freeze => RACK_VERSION,
25
32
  "rack.errors".freeze => events.stderr,
26
- "rack.multithread".freeze => true,
27
- "rack.multiprocess".freeze => false,
33
+ "rack.multithread".freeze => conf.options[:max_threads] > 1,
34
+ "rack.multiprocess".freeze => conf.options[:workers] >= 1,
28
35
  "rack.run_once".freeze => false,
29
36
  "SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",
30
37
 
@@ -45,6 +52,12 @@ module Puma
45
52
 
46
53
  attr_reader :ios
47
54
 
55
+ # @version 5.0.0
56
+ attr_reader :activated_sockets, :envs, :inherited_fds, :listeners, :proto_env, :unix_paths
57
+
58
+ # @version 5.0.0
59
+ attr_writer :ios, :listeners
60
+
48
61
  def env(sock)
49
62
  @envs.fetch(sock, @proto_env)
50
63
  end
@@ -53,40 +66,43 @@ module Puma
53
66
  @ios.each { |i| i.close }
54
67
  end
55
68
 
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
69
+ # @version 5.0.0
70
+ def connected_ports
71
+ ios.map { |io| io.addr[1] }.uniq
72
+ end
83
73
 
84
- remove.each do |k|
85
- ENV.delete k
74
+ # @version 5.0.0
75
+ def create_inherited_fds(env_hash)
76
+ env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v|
77
+ fd, url = v.split(":", 2)
78
+ @inherited_fds[url] = fd.to_i
79
+ end.keys # pass keys back for removal
80
+ end
81
+
82
+ # systemd socket activation.
83
+ # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4.
84
+ # LISTEN_PID = PID of the service process, aka us
85
+ # @see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html
86
+ # @version 5.0.0
87
+ #
88
+ def create_activated_fds(env_hash)
89
+ return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
90
+ env_hash['LISTEN_FDS'].to_i.times do |index|
91
+ sock = TCPServer.for_fd(socket_activation_fd(index))
92
+ key = begin # Try to parse as a path
93
+ [:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
94
+ rescue ArgumentError # Try to parse as a port/ip
95
+ port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
96
+ addr = "[#{addr}]" if addr =~ /\:/
97
+ [:tcp, addr, port]
98
+ end
99
+ @activated_sockets[key] = sock
100
+ @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
86
101
  end
102
+ ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
87
103
  end
88
104
 
89
- def parse(binds, logger)
105
+ def parse(binds, logger, log_msg = 'Listening')
90
106
  binds.each do |str|
91
107
  uri = URI.parse str
92
108
  case uri.scheme
@@ -113,7 +129,7 @@ module Puma
113
129
  i.local_address.ip_unpack.join(':')
114
130
  end
115
131
 
116
- logger.log "* Listening on tcp://#{addr}"
132
+ logger.log "* #{log_msg} on http://#{addr}"
117
133
  end
118
134
  end
119
135
 
@@ -149,11 +165,14 @@ module Puma
149
165
  end
150
166
 
151
167
  io = add_unix_listener path, umask, mode, backlog
152
- logger.log "* Listening on #{str}"
168
+ logger.log "* #{log_msg} on #{str}"
153
169
  end
154
170
 
155
171
  @listeners << [str, io]
156
172
  when "ssl"
173
+
174
+ raise "Puma compiled without SSL support" unless HAS_SSL
175
+
157
176
  params = Util.parse_query uri.query
158
177
  ctx = MiniSSL::ContextBuilder.new(params, @events).context
159
178
 
@@ -204,12 +223,6 @@ module Puma
204
223
  end
205
224
  end
206
225
 
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
226
  # Tell the server to listen on host +host+, port +port+.
214
227
  # If +optimize_for_latency+ is true (the default) then clients connecting
215
228
  # will be optimized for latency over throughput.
@@ -226,20 +239,17 @@ module Puma
226
239
  end
227
240
 
228
241
  host = host[1..-2] if host and host[0..0] == '['
229
- s = TCPServer.new(host, port)
242
+ tcp_server = TCPServer.new(host, port)
230
243
  if optimize_for_latency
231
- s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
244
+ tcp_server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
232
245
  end
233
- s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
234
- s.listen backlog
235
- @connected_port = s.addr[1]
246
+ tcp_server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
247
+ tcp_server.listen backlog
236
248
 
237
- @ios << s
238
- s
249
+ @ios << tcp_server
250
+ tcp_server
239
251
  end
240
252
 
241
- attr_reader :connected_port
242
-
243
253
  def inherit_tcp_listener(host, port, fd)
244
254
  if fd.kind_of? TCPServer
245
255
  s = fd
@@ -253,9 +263,8 @@ module Puma
253
263
 
254
264
  def add_ssl_listener(host, port, ctx,
255
265
  optimize_for_latency=true, backlog=1024)
256
- require 'puma/minissl'
257
266
 
258
- MiniSSL.check
267
+ raise "Puma compiled without SSL support" unless HAS_SSL
259
268
 
260
269
  if host == "localhost"
261
270
  loopback_addresses.each do |addr|
@@ -272,7 +281,6 @@ module Puma
272
281
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
273
282
  s.listen backlog
274
283
 
275
-
276
284
  ssl = MiniSSL::Server.new s, ctx
277
285
  env = @proto_env.dup
278
286
  env[HTTPS_KEY] = HTTPS
@@ -283,8 +291,7 @@ module Puma
283
291
  end
284
292
 
285
293
  def inherit_ssl_listener(fd, ctx)
286
- require 'puma/minissl'
287
- MiniSSL.check
294
+ raise "Puma compiled without SSL support" unless HAS_SSL
288
295
 
289
296
  if fd.kind_of? TCPServer
290
297
  s = fd
@@ -360,26 +367,39 @@ module Puma
360
367
  end
361
368
 
362
369
  def close_listeners
363
- @listeners.each do |l, io|
364
- io.close
370
+ listeners.each do |l, io|
371
+ io.close unless io.closed? # Ruby 2.2 issue
365
372
  uri = URI.parse(l)
366
373
  next unless uri.scheme == 'unix'
367
374
  unix_path = "#{uri.host}#{uri.path}"
368
- File.unlink unix_path if @unix_paths.include? unix_path
375
+ File.unlink unix_path if unix_paths.include? unix_path
369
376
  end
370
377
  end
371
378
 
372
- def close_unix_paths
373
- @unix_paths.each { |up| File.unlink(up) if File.exist? up }
379
+ def redirects_for_restart
380
+ redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h
381
+ redirects[:close_others] = true
382
+ redirects
374
383
  end
375
384
 
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
385
+ # @version 5.0.0
386
+ def redirects_for_restart_env
387
+ listeners.each_with_object({}).with_index do |(listen, memo), i|
388
+ memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}"
381
389
  end
382
- redirects
390
+ end
391
+
392
+ private
393
+
394
+ def loopback_addresses
395
+ Socket.ip_address_list.select do |addrinfo|
396
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
397
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
398
+ end
399
+
400
+ # @version 5.0.0
401
+ def socket_activation_fd(int)
402
+ int + 3 # 3 is the magic number you add to follow the SA protocol
383
403
  end
384
404
  end
385
405
  end
@@ -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
@@ -153,7 +153,7 @@ module Puma
153
153
 
154
154
  begin
155
155
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
157
157
  return false
158
158
  rescue SystemCallError, IOError, EOFError
159
159
  raise ConnectionError, "Connection error detected during read"
@@ -238,12 +238,23 @@ module Puma
238
238
  return false unless IO.select([@to_io], nil, nil, 0)
239
239
  try_to_finish
240
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
241
244
  end # IS_JRUBY
242
245
 
243
- def finish
246
+ def finish(timeout)
244
247
  return true if @ready
245
248
  until try_to_finish
246
- 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
247
258
  end
248
259
  true
249
260
  end
@@ -259,7 +270,7 @@ module Puma
259
270
  return @peerip if @peerip
260
271
 
261
272
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
274
  @peerip = hdr
264
275
  return hdr
265
276
  end
@@ -267,6 +278,20 @@ module Puma
267
278
  @peerip ||= @io.peeraddr.last
268
279
  end
269
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
+
270
295
  private
271
296
 
272
297
  def setup_body
@@ -285,8 +310,16 @@ module Puma
285
310
 
286
311
  te = @env[TRANSFER_ENCODING2]
287
312
 
288
- if te && CHUNKED.casecmp(te) == 0
289
- return setup_chunked_body(body)
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
290
323
  end
291
324
 
292
325
  @chunked_body = false
@@ -343,7 +376,7 @@ module Puma
343
376
 
344
377
  begin
345
378
  chunk = @io.read_nonblock(want)
346
- rescue Errno::EAGAIN
379
+ rescue IO::WaitReadable
347
380
  return false
348
381
  rescue SystemCallError, IOError
349
382
  raise ConnectionError, "Connection error detected during read"
@@ -389,7 +422,10 @@ module Puma
389
422
  raise EOFError
390
423
  end
391
424
 
392
- return true if decode_chunk(chunk)
425
+ if decode_chunk(chunk)
426
+ @env[CONTENT_LENGTH] = @chunked_content_length
427
+ return true
428
+ end
393
429
  end
394
430
  end
395
431
 
@@ -401,20 +437,37 @@ module Puma
401
437
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
402
438
  @body.binmode
403
439
  @tempfile = @body
440
+ @chunked_content_length = 0
404
441
 
405
- return decode_chunk(body)
442
+ if decode_chunk(body)
443
+ @env[CONTENT_LENGTH] = @chunked_content_length
444
+ return true
445
+ end
446
+ end
447
+
448
+ # @version 5.0.0
449
+ def write_chunk(str)
450
+ @chunked_content_length += @body.write(str)
406
451
  end
407
452
 
408
453
  def decode_chunk(chunk)
409
454
  if @partial_part_left > 0
410
455
  if @partial_part_left <= chunk.size
411
456
  if @partial_part_left > 2
412
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
457
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
413
458
  end
414
459
  chunk = chunk[@partial_part_left..-1]
415
460
  @partial_part_left = 0
416
461
  else
417
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
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
418
471
  @partial_part_left -= chunk.size
419
472
  return false
420
473
  end
@@ -461,12 +514,12 @@ module Puma
461
514
 
462
515
  case
463
516
  when got == len
464
- @body << part[0..-3] # to skip the ending \r\n
517
+ write_chunk(part[0..-3]) # to skip the ending \r\n
465
518
  when got <= len - 2
466
- @body << part
519
+ write_chunk(part)
467
520
  @partial_part_left = len - part.size
468
521
  when got == len - 1 # edge where we get just \r but not \n
469
- @body << part[0..-2]
522
+ write_chunk(part[0..-2])
470
523
  @partial_part_left = len - part.size
471
524
  end
472
525
  else