puma 7.0.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +139 -0
  3. data/README.md +25 -13
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/grpc.md +62 -0
  10. data/docs/images/favicon.svg +1 -0
  11. data/docs/images/running-puma.svg +1 -0
  12. data/docs/images/standard-logo.svg +1 -0
  13. data/docs/jungle/README.md +1 -1
  14. data/docs/kubernetes.md +5 -12
  15. data/docs/plugins.md +2 -2
  16. data/docs/signals.md +10 -10
  17. data/docs/stats.md +2 -2
  18. data/docs/systemd.md +3 -3
  19. data/ext/puma_http11/http11_parser.java.rl +51 -65
  20. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  21. data/ext/puma_http11/org/jruby/puma/Http11.java +168 -104
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  23. data/ext/puma_http11/puma_http11.c +101 -109
  24. data/lib/puma/app/status.rb +10 -2
  25. data/lib/puma/cli.rb +1 -1
  26. data/lib/puma/client.rb +111 -91
  27. data/lib/puma/client_env.rb +171 -0
  28. data/lib/puma/cluster/worker.rb +10 -9
  29. data/lib/puma/cluster/worker_handle.rb +2 -2
  30. data/lib/puma/cluster.rb +12 -11
  31. data/lib/puma/cluster_accept_loop_delay.rb +17 -18
  32. data/lib/puma/configuration.rb +86 -16
  33. data/lib/puma/const.rb +2 -2
  34. data/lib/puma/control_cli.rb +1 -1
  35. data/lib/puma/detect.rb +11 -0
  36. data/lib/puma/dsl.rb +115 -18
  37. data/lib/puma/launcher.rb +35 -30
  38. data/lib/puma/reactor.rb +3 -12
  39. data/lib/puma/{request.rb → response.rb} +25 -194
  40. data/lib/puma/runner.rb +1 -1
  41. data/lib/puma/server.rb +102 -63
  42. data/lib/puma/server_plugin_control.rb +32 -0
  43. data/lib/puma/single.rb +2 -2
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +139 -24
  46. data/lib/rack/handler/puma.rb +1 -1
  47. data/tools/Dockerfile +13 -5
  48. metadata +16 -5
  49. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/launcher.rb CHANGED
@@ -42,26 +42,40 @@ module Puma
42
42
  # end
43
43
  # Puma::Launcher.new(conf, log_writer: Puma::LogWriter.stdio).run
44
44
  def initialize(conf, launcher_args={})
45
- @runner = nil
46
- @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
47
- @events = launcher_args[:events] || Events.new
48
- @argv = launcher_args[:argv] || []
49
- @original_argv = @argv.dup
50
- @config = conf
51
-
52
- env = launcher_args.delete(:env) || ENV
45
+ ## Minimal initialization before potential early restart (e.g. from bundle pruning)
53
46
 
47
+ @config = conf
48
+ # Advertise the CLI Configuration before config files are loaded
49
+ Puma.cli_config = @config if defined?(Puma.cli_config)
54
50
  @config.clamp
51
+
55
52
  @options = @config.options
56
53
 
54
+ @log_writer = launcher_args[:log_writer] || LogWriter::DEFAULT
55
+ @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
56
+ @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
57
+ @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
57
58
  @options[:log_writer] = @log_writer
58
59
  @options[:logger] = @log_writer if clustered?
59
60
 
60
- # Advertise the Configuration
61
- Puma.cli_config = @config if defined?(Puma.cli_config)
61
+ @events = launcher_args[:events] || Events.new
62
+
63
+ @argv = launcher_args[:argv] || []
64
+ @original_argv = @argv.dup
65
+
66
+ ## End minimal initialization
67
+
68
+ generate_restart_data
69
+ Dir.chdir(@restart_dir)
70
+
71
+ prune_bundler!
72
+
73
+ env = launcher_args.delete(:env) || ENV
74
+
75
+ # Log after prune_bundler! to avoid duplicate logging if a restart occurs
62
76
  log_config if env['PUMA_LOG_CONFIG']
63
77
 
64
- @binder = Binder.new(@log_writer, @options)
78
+ @binder = Binder.new(@log_writer, @options)
65
79
  @binder.create_inherited_fds(env).each { |k| env.delete k }
66
80
  @binder.create_activated_fds(env).each { |k| env.delete k }
67
81
 
@@ -81,21 +95,10 @@ module Puma
81
95
  )
82
96
  end
83
97
 
84
- @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
85
- @log_writer.formatter = @options[:log_formatter] if @options[:log_formatter]
86
-
87
- @log_writer.custom_logger = @options[:custom_logger] if @options[:custom_logger]
88
-
89
- generate_restart_data
90
-
91
98
  if clustered? && !Puma.forkable?
92
99
  unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
93
100
  end
94
101
 
95
- Dir.chdir(@restart_dir)
96
-
97
- prune_bundler!
98
-
99
102
  @environment = @options[:environment] if @options[:environment]
100
103
  set_rack_environment
101
104
 
@@ -139,7 +142,10 @@ module Puma
139
142
  # Delete the configured pidfile
140
143
  def delete_pidfile
141
144
  path = @options[:pidfile]
142
- File.unlink(path) if path && File.exist?(path)
145
+ begin
146
+ File.unlink(path) if path
147
+ rescue Errno::ENOENT
148
+ end
143
149
  end
144
150
 
145
151
  # Begin async shutdown of the server
@@ -381,9 +387,9 @@ module Puma
381
387
  # using it.
382
388
  @restart_dir = Dir.pwd
383
389
 
384
- # Use the same trick as unicorn, namely favor PWD because
385
- # it will contain an unresolved symlink, useful for when
386
- # the pwd is /data/releases/current.
390
+ # Use the same trick as unicorn, namely favor PWD because
391
+ # it will contain an unresolved symlink, useful for when
392
+ # the pwd is /data/releases/current.
387
393
  elsif dir = ENV['PWD']
388
394
  s_env = File.stat(dir)
389
395
  s_pwd = File.stat(Dir.pwd)
@@ -470,8 +476,8 @@ module Puma
470
476
  end
471
477
 
472
478
  begin
473
- unless Puma.jruby? # INFO in use by JVM already
474
- Signal.trap "SIGINFO" do
479
+ if Puma.backtrace_signal
480
+ Signal.trap Puma.backtrace_signal do
475
481
  thread_status do |name, backtrace|
476
482
  @log_writer.log(name)
477
483
  @log_writer.log(backtrace.map { |bt| " #{bt}" })
@@ -479,8 +485,7 @@ module Puma
479
485
  end
480
486
  end
481
487
  rescue Exception
482
- # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
483
- # to see this constantly on Linux.
488
+ log "*** SIGINFO/SIGPWR not implemented, signal based backtrace unavailable!"
484
489
  end
485
490
  end
486
491
 
data/lib/puma/reactor.rb CHANGED
@@ -75,15 +75,12 @@ module Puma
75
75
  private
76
76
 
77
77
  def select_loop
78
- close_selector = true
79
78
  begin
80
79
  until @input.closed? && @input.empty?
81
80
  # Wakeup any registered object that receives incoming data.
82
81
  # Block until the earliest timeout or Selector#wakeup is called.
83
82
  timeout = (earliest = @timeouts.first) && earliest.timeout
84
- monitor_wake_up = false
85
83
  @selector.select(timeout) do |monitor|
86
- monitor_wake_up = true
87
84
  wakeup!(monitor.value)
88
85
  end
89
86
 
@@ -103,18 +100,12 @@ module Puma
103
100
  STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
104
101
  STDERR.puts e.backtrace
105
102
 
106
- # NoMethodError may be rarely raised when calling @selector.select, which
107
- # is odd. Regardless, it may continue for thousands of calls if retried.
108
- # Also, when it raises, @selector.close also raises an error.
109
- if !monitor_wake_up && NoMethodError === e
110
- close_selector = false
111
- else
112
- retry
113
- end
103
+ retry
114
104
  end
105
+
115
106
  # Wakeup all remaining objects on shutdown.
116
107
  @timeouts.each(&@block)
117
- @selector.close if close_selector
108
+ @selector.close
118
109
  end
119
110
 
120
111
  # Start monitoring the object.
@@ -12,7 +12,7 @@ module Puma
12
12
  # #handle_request, which is called in Server#process_client.
13
13
  # @version 5.0.3
14
14
  #
15
- module Request # :nodoc:
15
+ module Response # :nodoc:
16
16
 
17
17
  # Single element array body: smaller bodies are written to io_buffer first,
18
18
  # then a single write from io_buffer. Larger sizes are written separately.
@@ -36,43 +36,28 @@ module Puma
36
36
  # Takes the request contained in +client+, invokes the Rack application to construct
37
37
  # the response and writes it back to +client.io+.
38
38
  #
39
- # It'll return +false+ when the connection is closed, this doesn't mean
39
+ # It'll return +:close+ when the connection is closed, this doesn't mean
40
40
  # that the response wasn't successful.
41
41
  #
42
+ # It'll return +:keep_alive+ if the connection is a pipeline or keep-alive connection.
43
+ # Which may contain additional requests.
44
+ #
42
45
  # It'll return +:async+ if the connection remains open but will be handled
43
46
  # elsewhere, i.e. the connection has been hijacked by the Rack application.
44
47
  #
45
48
  # Finally, it'll return +true+ on keep-alive connections.
49
+ # @param processor [Puma::ThreadPool::ProcessorThread]
46
50
  # @param client [Puma::Client]
47
51
  # @param requests [Integer]
48
- # @return [Boolean,:async]
49
- #
50
- def handle_request(client, requests)
52
+ # @return [:close, :keep_alive, :async]
53
+ def handle_request(processor, client, requests)
51
54
  env = client.env
52
55
  io_buffer = client.io_buffer
53
56
  socket = client.io # io may be a MiniSSL::Socket
54
57
  app_body = nil
55
58
  error = nil
56
59
 
57
- return false if closed_socket?(socket)
58
-
59
- if client.http_content_length_limit_exceeded
60
- return prepare_response(413, {}, ["Payload Too Large"], requests, client)
61
- end
62
-
63
- normalize_env env, client
64
-
65
- env[PUMA_SOCKET] = socket
66
-
67
- if env[HTTPS_KEY] && socket.peercert
68
- env[PUMA_PEERCERT] = socket.peercert
69
- end
70
-
71
- env[HIJACK_P] = true
72
- env[HIJACK] = client.method :full_hijack
73
-
74
- env[RACK_INPUT] = client.body
75
- env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
60
+ return :close if closed_socket?(socket)
76
61
 
77
62
  if @early_hints
78
63
  env[EARLY_HINTS] = lambda { |headers|
@@ -87,22 +72,11 @@ module Puma
87
72
  }
88
73
  end
89
74
 
90
- req_env_post_parse env
91
-
92
- # A rack extension. If the app writes #call'ables to this
93
- # array, we will invoke them when the request is done.
94
- #
95
- env[RACK_AFTER_REPLY] ||= []
96
- env[RACK_RESPONSE_FINISHED] ||= []
75
+ env["puma.mark_as_io_bound"] = -> { processor.mark_as_io_thread! }
97
76
 
98
77
  begin
99
- if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
100
- status, headers, app_body = @thread_pool.with_force_shutdown do
101
- @app.call(env)
102
- end
103
- else
104
- @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
105
- status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
78
+ status, headers, app_body = @thread_pool.with_force_shutdown do
79
+ @app.call(env)
106
80
  end
107
81
 
108
82
  # app_body needs to always be closed, hold value in case lowlevel_error
@@ -134,7 +108,6 @@ module Puma
134
108
  prepare_response(status, headers, res_body, requests, client)
135
109
  ensure
136
110
  io_buffer.reset
137
- uncork_socket client.io
138
111
  app_body.close if app_body.respond_to? :close
139
112
  client&.tempfile_close
140
113
  if after_reply = env[RACK_AFTER_REPLY]
@@ -167,13 +140,13 @@ module Puma
167
140
  # a call to `Server#lowlevel_error`
168
141
  # @param requests [Integer] number of inline requests handled
169
142
  # @param client [Puma::Client]
170
- # @return [Boolean,:async] keep-alive status or `:async`
143
+ # @return [:close, :keep_alive, :async]
171
144
  def prepare_response(status, headers, res_body, requests, client)
172
145
  env = client.env
173
146
  socket = client.io
174
147
  io_buffer = client.io_buffer
175
148
 
176
- return false if closed_socket?(socket)
149
+ return :close if closed_socket?(socket)
177
150
 
178
151
  # Close the connection after a reasonable number of inline requests
179
152
  force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
@@ -243,8 +216,9 @@ module Puma
243
216
 
244
217
  io_buffer << LINE_END
245
218
  fast_write_str socket, io_buffer.read_and_reset
246
- socket.flush
247
- return keep_alive
219
+
220
+ uncork_socket socket.flush
221
+ return keep_alive ? :keep_alive : :close
248
222
  end
249
223
  else
250
224
  if content_length
@@ -262,32 +236,16 @@ module Puma
262
236
  # response_hijack.call
263
237
  if response_hijack
264
238
  fast_write_str socket, io_buffer.read_and_reset
265
- uncork_socket socket
239
+ uncork_socket socket.flush
266
240
  response_hijack.call socket
267
241
  return :async
268
242
  end
269
243
 
270
244
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
271
245
  body.close if close_body
272
- # if we're shutting down, close keep_alive connections
273
- !shutting_down? && keep_alive
274
- end
275
-
276
- HTTP_ON_VALUES = { "on" => true, HTTPS => true }
277
- private_constant :HTTP_ON_VALUES
278
246
 
279
- # @param env [Hash] see Puma::Client#env, from request
280
- # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
281
- #
282
- def default_server_port(env)
283
- if HTTP_ON_VALUES[env[HTTPS_KEY]] ||
284
- env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
285
- env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
286
- env[HTTP_X_FORWARDED_SSL] == "on"
287
- PORT_443
288
- else
289
- PORT_80
290
- end
247
+ # if we're shutting down, close keep_alive connections
248
+ !shutting_down? && keep_alive ? :keep_alive : :close
291
249
  end
292
250
 
293
251
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
@@ -404,6 +362,7 @@ module Puma
404
362
  end
405
363
  end
406
364
  socket.flush
365
+ uncork_socket socket
407
366
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
408
367
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
409
368
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -412,85 +371,6 @@ module Puma
412
371
 
413
372
  private :fast_write_str, :fast_write_response
414
373
 
415
- # Given a Hash +env+ for the request read from +client+, add
416
- # and fixup keys to comply with Rack's env guidelines.
417
- # @param env [Hash] see Puma::Client#env, from request
418
- # @param client [Puma::Client] only needed for Client#peerip
419
- #
420
- def normalize_env(env, client)
421
- if host = env[HTTP_HOST]
422
- # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
423
- if colon = host.rindex("]:") # IPV6 with port
424
- env[SERVER_NAME] = host[0, colon+1]
425
- env[SERVER_PORT] = host[colon+2, host.bytesize]
426
- elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
427
- env[SERVER_NAME] = host[0, colon]
428
- env[SERVER_PORT] = host[colon+1, host.bytesize]
429
- else
430
- env[SERVER_NAME] = host
431
- env[SERVER_PORT] = default_server_port(env)
432
- end
433
- else
434
- env[SERVER_NAME] = LOCALHOST
435
- env[SERVER_PORT] = default_server_port(env)
436
- end
437
-
438
- unless env[REQUEST_PATH]
439
- # it might be a dumbass full host request header
440
- uri = begin
441
- URI.parse(env[REQUEST_URI])
442
- rescue URI::InvalidURIError
443
- raise Puma::HttpParserError
444
- end
445
- env[REQUEST_PATH] = uri.path
446
-
447
- # A nil env value will cause a LintError (and fatal errors elsewhere),
448
- # so only set the env value if there actually is a value.
449
- env[QUERY_STRING] = uri.query if uri.query
450
- end
451
-
452
- env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
453
-
454
- # From https://www.ietf.org/rfc/rfc3875 :
455
- # "Script authors should be aware that the REMOTE_ADDR and
456
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
457
- # may not identify the ultimate source of the request.
458
- # They identify the client for the immediate request to the
459
- # server; that client may be a proxy, gateway, or other
460
- # intermediary acting on behalf of the actual source client."
461
- #
462
-
463
- unless env.key?(REMOTE_ADDR)
464
- begin
465
- addr = client.peerip
466
- rescue Errno::ENOTCONN
467
- # Client disconnects can result in an inability to get the
468
- # peeraddr from the socket; default to unspec.
469
- if client.peer_family == Socket::AF_INET6
470
- addr = UNSPECIFIED_IPV6
471
- else
472
- addr = UNSPECIFIED_IPV4
473
- end
474
- end
475
-
476
- # Set unix socket addrs to localhost
477
- if addr.empty?
478
- if client.peer_family == Socket::AF_INET6
479
- addr = LOCALHOST_IPV6
480
- else
481
- addr = LOCALHOST_IPV4
482
- end
483
- end
484
-
485
- env[REMOTE_ADDR] = addr
486
- end
487
-
488
- # The legacy HTTP_VERSION header can be sent as a client header.
489
- # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
490
- env[HTTP_VERSION] = env[SERVER_PROTOCOL] if @env_set_http_version
491
- end
492
- private :normalize_env
493
-
494
374
  # @param header_key [#to_s]
495
375
  # @return [Boolean]
496
376
  #
@@ -504,56 +384,6 @@ module Puma
504
384
  def illegal_header_value?(header_value)
505
385
  !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
506
386
  end
507
- private :illegal_header_key?, :illegal_header_value?
508
-
509
- # Fixup any headers with `,` in the name to have `_` now. We emit
510
- # headers with `,` in them during the parse phase to avoid ambiguity
511
- # with the `-` to `_` conversion for critical headers. But here for
512
- # compatibility, we'll convert them back. This code is written to
513
- # avoid allocation in the common case (ie there are no headers
514
- # with `,` in their names), that's why it has the extra conditionals.
515
- #
516
- # @note If a normalized version of a `,` header already exists, we ignore
517
- # the `,` version. This prevents clobbering headers managed by proxies
518
- # but not by clients (Like X-Forwarded-For).
519
- #
520
- # @param env [Hash] see Puma::Client#env, from request, modifies in place
521
- # @version 5.0.3
522
- #
523
- def req_env_post_parse(env)
524
- to_delete = nil
525
- to_add = nil
526
-
527
- env.each do |k,v|
528
- if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
529
- if to_delete
530
- to_delete << k
531
- else
532
- to_delete = [k]
533
- end
534
-
535
- new_k = k.tr(",", "_")
536
- if env.key?(new_k)
537
- next
538
- end
539
-
540
- unless to_add
541
- to_add = {}
542
- end
543
-
544
- to_add[new_k] = v
545
- end
546
- end
547
-
548
- if to_delete # rubocop:disable Style/SafeNavigation
549
- to_delete.each { |k| env.delete(k) }
550
- end
551
-
552
- if to_add
553
- env.merge! to_add
554
- end
555
- end
556
- private :req_env_post_parse
557
387
 
558
388
  # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
559
389
  # @param headers [Hash] the headers returned by the Rack application
@@ -650,7 +480,8 @@ module Puma
650
480
  headers.each do |k, vs|
651
481
  next if illegal_header_key?(k)
652
482
 
653
- case k.downcase
483
+ key = k.downcase
484
+ case key
654
485
  when CONTENT_LENGTH2
655
486
  next if illegal_header_value?(vs)
656
487
  # nil.to_i is 0, nil&.to_i is nil
@@ -677,10 +508,10 @@ module Puma
677
508
  if ary
678
509
  ary.each do |v|
679
510
  next if illegal_header_value?(v)
680
- io_buffer.append k.downcase, colon, v, line_ending
511
+ io_buffer.append key, colon, v, line_ending
681
512
  end
682
513
  else
683
- io_buffer.append k.downcase, colon, line_ending
514
+ io_buffer.append key, colon, line_ending
684
515
  end
685
516
  end
686
517
 
data/lib/puma/runner.rb CHANGED
@@ -70,7 +70,7 @@ module Puma
70
70
  token = nil if token.empty? || token == 'none'
71
71
  end
72
72
 
73
- app = Puma::App::Status.new @launcher, token
73
+ app = Puma::App::Status.new @launcher, token: token, data_only: @options[:control_data_only]
74
74
 
75
75
  # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
76
76
  # Use `nil` for events, no hooks in control server