puma 6.4.3 → 8.0.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +448 -8
  3. data/README.md +110 -51
  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/fork_worker.md +11 -1
  10. data/docs/grpc.md +62 -0
  11. data/docs/images/favicon.svg +1 -0
  12. data/docs/images/running-puma.svg +1 -0
  13. data/docs/images/standard-logo.svg +1 -0
  14. data/docs/java_options.md +54 -0
  15. data/docs/jungle/README.md +1 -1
  16. data/docs/kubernetes.md +11 -16
  17. data/docs/plugins.md +6 -2
  18. data/docs/restart.md +2 -2
  19. data/docs/signals.md +21 -21
  20. data/docs/stats.md +11 -5
  21. data/docs/systemd.md +14 -5
  22. data/ext/puma_http11/extconf.rb +20 -32
  23. data/ext/puma_http11/http11_parser.java.rl +51 -65
  24. data/ext/puma_http11/mini_ssl.c +29 -9
  25. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  28. data/ext/puma_http11/puma_http11.c +125 -118
  29. data/lib/puma/app/status.rb +11 -3
  30. data/lib/puma/binder.rb +22 -12
  31. data/lib/puma/cli.rb +11 -9
  32. data/lib/puma/client.rb +233 -136
  33. data/lib/puma/client_env.rb +171 -0
  34. data/lib/puma/cluster/worker.rb +24 -21
  35. data/lib/puma/cluster/worker_handle.rb +38 -8
  36. data/lib/puma/cluster.rb +74 -48
  37. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  38. data/lib/puma/commonlogger.rb +3 -3
  39. data/lib/puma/configuration.rb +197 -64
  40. data/lib/puma/const.rb +23 -12
  41. data/lib/puma/control_cli.rb +11 -7
  42. data/lib/puma/detect.rb +13 -0
  43. data/lib/puma/dsl.rb +483 -127
  44. data/lib/puma/error_logger.rb +7 -5
  45. data/lib/puma/events.rb +25 -10
  46. data/lib/puma/io_buffer.rb +8 -4
  47. data/lib/puma/jruby_restart.rb +0 -16
  48. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  49. data/lib/puma/launcher.rb +76 -59
  50. data/lib/puma/log_writer.rb +17 -11
  51. data/lib/puma/minissl/context_builder.rb +1 -0
  52. data/lib/puma/minissl.rb +1 -1
  53. data/lib/puma/null_io.rb +26 -0
  54. data/lib/puma/plugin/systemd.rb +3 -3
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/reactor.rb +19 -13
  57. data/lib/puma/{request.rb → response.rb} +57 -209
  58. data/lib/puma/runner.rb +15 -17
  59. data/lib/puma/sd_notify.rb +1 -4
  60. data/lib/puma/server.rb +200 -104
  61. data/lib/puma/server_plugin_control.rb +32 -0
  62. data/lib/puma/single.rb +7 -4
  63. data/lib/puma/state_file.rb +3 -2
  64. data/lib/puma/thread_pool.rb +179 -96
  65. data/lib/puma/util.rb +0 -7
  66. data/lib/puma.rb +10 -0
  67. data/lib/rack/handler/puma.rb +11 -8
  68. data/tools/Dockerfile +15 -5
  69. metadata +26 -16
  70. data/ext/puma_http11/ext_help.h +0 -15
@@ -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
58
+ error = nil
55
59
 
56
-
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
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,21 +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] ||= []
75
+ env["puma.mark_as_io_bound"] = -> { processor.mark_as_io_thread! }
96
76
 
97
77
  begin
98
- if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
99
- status, headers, app_body = @thread_pool.with_force_shutdown do
100
- @app.call(env)
101
- end
102
- else
103
- @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
104
- 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)
105
80
  end
106
81
 
107
82
  # app_body needs to always be closed, hold value in case lowlevel_error
@@ -120,28 +95,40 @@ module Puma
120
95
 
121
96
  return :async
122
97
  end
123
- rescue ThreadPool::ForceShutdown => e
124
- @log_writer.unknown_error e, client, "Rack app"
98
+ rescue ThreadPool::ForceShutdown => error
99
+ @log_writer.unknown_error error, client, "Rack app"
125
100
  @log_writer.log "Detected force shutdown of a thread"
126
101
 
127
- status, headers, res_body = lowlevel_error(e, env, 503)
128
- rescue Exception => e
129
- @log_writer.unknown_error e, client, "Rack app"
102
+ status, headers, res_body = lowlevel_error(error, env, 503)
103
+ rescue Exception => error
104
+ @log_writer.unknown_error error, client, "Rack app"
130
105
 
131
- status, headers, res_body = lowlevel_error(e, env, 500)
106
+ status, headers, res_body = lowlevel_error(error, env, 500)
132
107
  end
133
108
  prepare_response(status, headers, res_body, requests, client)
134
109
  ensure
135
110
  io_buffer.reset
136
- uncork_socket client.io
137
111
  app_body.close if app_body.respond_to? :close
138
- client.tempfile&.unlink
139
- after_reply = env[RACK_AFTER_REPLY] || []
140
- begin
141
- after_reply.each { |o| o.call }
142
- rescue StandardError => e
143
- @log_writer.debug_error e
144
- end unless after_reply.empty?
112
+ client&.tempfile_close
113
+ if after_reply = env[RACK_AFTER_REPLY]
114
+ after_reply.each do |o|
115
+ begin
116
+ o.call
117
+ rescue StandardError => e
118
+ @log_writer.debug_error e
119
+ end
120
+ end
121
+ end
122
+
123
+ if response_finished = env[RACK_RESPONSE_FINISHED]
124
+ response_finished.reverse_each do |o|
125
+ begin
126
+ o.call(env, status, headers, error)
127
+ rescue StandardError => e
128
+ @log_writer.debug_error e
129
+ end
130
+ end
131
+ end
145
132
  end
146
133
 
147
134
  # Assembles the headers and prepares the body for actually sending the
@@ -153,21 +140,16 @@ module Puma
153
140
  # a call to `Server#lowlevel_error`
154
141
  # @param requests [Integer] number of inline requests handled
155
142
  # @param client [Puma::Client]
156
- # @return [Boolean,:async] keep-alive status or `:async`
143
+ # @return [:close, :keep_alive, :async]
157
144
  def prepare_response(status, headers, res_body, requests, client)
158
145
  env = client.env
159
146
  socket = client.io
160
147
  io_buffer = client.io_buffer
161
148
 
162
- return false if closed_socket?(socket)
149
+ return :close if closed_socket?(socket)
163
150
 
164
151
  # Close the connection after a reasonable number of inline requests
165
- # if the server is at capacity and the listener has a new connection ready.
166
- # This allows Puma to service connections fairly when the number
167
- # of concurrent connections exceeds the size of the threadpool.
168
- force_keep_alive = requests < @max_fast_inline ||
169
- @thread_pool.busy_threads < @max_threads ||
170
- !client.listener.to_io.wait_readable(0)
152
+ force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
171
153
 
172
154
  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
173
155
 
@@ -187,7 +169,8 @@ module Puma
187
169
  elsif res_body.is_a?(File) && res_body.respond_to?(:size)
188
170
  body = res_body
189
171
  content_length = body.size
190
- elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
172
+ elsif res_body.respond_to?(:to_path) && (fn = res_body.to_path) &&
173
+ File.readable?(fn)
191
174
  body = File.open fn, 'rb'
192
175
  content_length = body.size
193
176
  close_body = true
@@ -195,7 +178,7 @@ module Puma
195
178
  body = res_body
196
179
  end
197
180
  elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
198
- File.readable?(fn = res_body.to_path)
181
+ (fn = res_body.to_path) && File.readable?(fn = res_body.to_path)
199
182
  body = File.open fn, 'rb'
200
183
  content_length = body.size
201
184
  close_body = true
@@ -233,8 +216,9 @@ module Puma
233
216
 
234
217
  io_buffer << LINE_END
235
218
  fast_write_str socket, io_buffer.read_and_reset
236
- socket.flush
237
- return keep_alive
219
+
220
+ uncork_socket socket.flush
221
+ return keep_alive ? :keep_alive : :close
238
222
  end
239
223
  else
240
224
  if content_length
@@ -252,25 +236,16 @@ module Puma
252
236
  # response_hijack.call
253
237
  if response_hijack
254
238
  fast_write_str socket, io_buffer.read_and_reset
255
- uncork_socket socket
239
+ uncork_socket socket.flush
256
240
  response_hijack.call socket
257
241
  return :async
258
242
  end
259
243
 
260
244
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
261
245
  body.close if close_body
262
- keep_alive
263
- end
264
246
 
265
- # @param env [Hash] see Puma::Client#env, from request
266
- # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
267
- #
268
- def default_server_port(env)
269
- if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
270
- PORT_443
271
- else
272
- PORT_80
273
- end
247
+ # if we're shutting down, close keep_alive connections
248
+ !shutting_down? && keep_alive ? :keep_alive : :close
274
249
  end
275
250
 
276
251
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
@@ -387,6 +362,7 @@ module Puma
387
362
  end
388
363
  end
389
364
  socket.flush
365
+ uncork_socket socket
390
366
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
391
367
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
392
368
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -395,85 +371,6 @@ module Puma
395
371
 
396
372
  private :fast_write_str, :fast_write_response
397
373
 
398
- # Given a Hash +env+ for the request read from +client+, add
399
- # and fixup keys to comply with Rack's env guidelines.
400
- # @param env [Hash] see Puma::Client#env, from request
401
- # @param client [Puma::Client] only needed for Client#peerip
402
- #
403
- def normalize_env(env, client)
404
- if host = env[HTTP_HOST]
405
- # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
406
- if colon = host.rindex("]:") # IPV6 with port
407
- env[SERVER_NAME] = host[0, colon+1]
408
- env[SERVER_PORT] = host[colon+2, host.bytesize]
409
- elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
410
- env[SERVER_NAME] = host[0, colon]
411
- env[SERVER_PORT] = host[colon+1, host.bytesize]
412
- else
413
- env[SERVER_NAME] = host
414
- env[SERVER_PORT] = default_server_port(env)
415
- end
416
- else
417
- env[SERVER_NAME] = LOCALHOST
418
- env[SERVER_PORT] = default_server_port(env)
419
- end
420
-
421
- unless env[REQUEST_PATH]
422
- # it might be a dumbass full host request header
423
- uri = begin
424
- URI.parse(env[REQUEST_URI])
425
- rescue URI::InvalidURIError
426
- raise Puma::HttpParserError
427
- end
428
- env[REQUEST_PATH] = uri.path
429
-
430
- # A nil env value will cause a LintError (and fatal errors elsewhere),
431
- # so only set the env value if there actually is a value.
432
- env[QUERY_STRING] = uri.query if uri.query
433
- end
434
-
435
- env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
436
-
437
- # From https://www.ietf.org/rfc/rfc3875 :
438
- # "Script authors should be aware that the REMOTE_ADDR and
439
- # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
440
- # may not identify the ultimate source of the request.
441
- # They identify the client for the immediate request to the
442
- # server; that client may be a proxy, gateway, or other
443
- # intermediary acting on behalf of the actual source client."
444
- #
445
-
446
- unless env.key?(REMOTE_ADDR)
447
- begin
448
- addr = client.peerip
449
- rescue Errno::ENOTCONN
450
- # Client disconnects can result in an inability to get the
451
- # peeraddr from the socket; default to unspec.
452
- if client.peer_family == Socket::AF_INET6
453
- addr = UNSPECIFIED_IPV6
454
- else
455
- addr = UNSPECIFIED_IPV4
456
- end
457
- end
458
-
459
- # Set unix socket addrs to localhost
460
- if addr.empty?
461
- if client.peer_family == Socket::AF_INET6
462
- addr = LOCALHOST_IPV6
463
- else
464
- addr = LOCALHOST_IPV4
465
- end
466
- end
467
-
468
- env[REMOTE_ADDR] = addr
469
- end
470
-
471
- # The legacy HTTP_VERSION header can be sent as a client header.
472
- # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
473
- env[HTTP_VERSION] = env[SERVER_PROTOCOL]
474
- end
475
- private :normalize_env
476
-
477
374
  # @param header_key [#to_s]
478
375
  # @return [Boolean]
479
376
  #
@@ -487,56 +384,6 @@ module Puma
487
384
  def illegal_header_value?(header_value)
488
385
  !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
489
386
  end
490
- private :illegal_header_key?, :illegal_header_value?
491
-
492
- # Fixup any headers with `,` in the name to have `_` now. We emit
493
- # headers with `,` in them during the parse phase to avoid ambiguity
494
- # with the `-` to `_` conversion for critical headers. But here for
495
- # compatibility, we'll convert them back. This code is written to
496
- # avoid allocation in the common case (ie there are no headers
497
- # with `,` in their names), that's why it has the extra conditionals.
498
- #
499
- # @note If a normalized version of a `,` header already exists, we ignore
500
- # the `,` version. This prevents clobbering headers managed by proxies
501
- # but not by clients (Like X-Forwarded-For).
502
- #
503
- # @param env [Hash] see Puma::Client#env, from request, modifies in place
504
- # @version 5.0.3
505
- #
506
- def req_env_post_parse(env)
507
- to_delete = nil
508
- to_add = nil
509
-
510
- env.each do |k,v|
511
- if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
512
- if to_delete
513
- to_delete << k
514
- else
515
- to_delete = [k]
516
- end
517
-
518
- new_k = k.tr(",", "_")
519
- if env.key?(new_k)
520
- next
521
- end
522
-
523
- unless to_add
524
- to_add = {}
525
- end
526
-
527
- to_add[new_k] = v
528
- end
529
- end
530
-
531
- if to_delete # rubocop:disable Style/SafeNavigation
532
- to_delete.each { |k| env.delete(k) }
533
- end
534
-
535
- if to_add
536
- env.merge! to_add
537
- end
538
- end
539
- private :req_env_post_parse
540
387
 
541
388
  # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
542
389
  # @param headers [Hash] the headers returned by the Rack application
@@ -577,7 +424,7 @@ module Puma
577
424
  # response body
578
425
  # @param io_buffer [Puma::IOBuffer] modified inn place
579
426
  # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
580
- # status and `@max_fast_inline`
427
+ # status and `@max_keep_alive`
581
428
  # @return [Hash] resp_info
582
429
  # @version 5.0.3
583
430
  #
@@ -633,7 +480,8 @@ module Puma
633
480
  headers.each do |k, vs|
634
481
  next if illegal_header_key?(k)
635
482
 
636
- case k.downcase
483
+ key = k.downcase
484
+ case key
637
485
  when CONTENT_LENGTH2
638
486
  next if illegal_header_value?(vs)
639
487
  # nil.to_i is 0, nil&.to_i is nil
@@ -660,10 +508,10 @@ module Puma
660
508
  if ary
661
509
  ary.each do |v|
662
510
  next if illegal_header_value?(v)
663
- io_buffer.append k, colon, v, line_ending
511
+ io_buffer.append key, colon, v, line_ending
664
512
  end
665
513
  else
666
- io_buffer.append k, colon, line_ending
514
+ io_buffer.append key, colon, line_ending
667
515
  end
668
516
  end
669
517
 
data/lib/puma/runner.rb CHANGED
@@ -8,6 +8,9 @@ module Puma
8
8
  # serve requests. This class spawns a new instance of `Puma::Server` via
9
9
  # a call to `start_server`.
10
10
  class Runner
11
+
12
+ include ::Puma::Const::PipeRequest
13
+
11
14
  def initialize(launcher)
12
15
  @launcher = launcher
13
16
  @log_writer = launcher.log_writer
@@ -27,10 +30,9 @@ module Puma
27
30
  def wakeup!
28
31
  return unless @wakeup
29
32
 
30
- @wakeup.write "!" unless @wakeup.closed?
33
+ @wakeup.write PIPE_WAKEUP unless @wakeup.closed?
31
34
 
32
35
  rescue SystemCallError, IOError
33
- Puma::Util.purge_interrupt_queue
34
36
  end
35
37
 
36
38
  def development?
@@ -68,7 +70,7 @@ module Puma
68
70
  token = nil if token.empty? || token == 'none'
69
71
  end
70
72
 
71
- app = Puma::App::Status.new @launcher, token
73
+ app = Puma::App::Status.new @launcher, token: token, data_only: @options[:control_data_only]
72
74
 
73
75
  # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
74
76
  # Use `nil` for events, no hooks in control server
@@ -90,26 +92,14 @@ module Puma
90
92
  @control.binder.close_listeners if @control
91
93
  end
92
94
 
93
- # @!attribute [r] ruby_engine
94
- def ruby_engine
95
- if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
96
- "ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
97
- else
98
- if defined?(RUBY_ENGINE_VERSION)
99
- "#{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} - ruby #{RUBY_VERSION}"
100
- else
101
- "#{RUBY_ENGINE} #{RUBY_VERSION}"
102
- end
103
- end
104
- end
105
-
106
95
  def output_header(mode)
107
96
  min_t = @options[:min_threads]
108
97
  max_t = @options[:max_threads]
109
98
  environment = @options[:environment]
110
99
 
111
100
  log "Puma starting in #{mode} mode..."
112
- log "* Puma version: #{Puma::Const::PUMA_VERSION} (#{ruby_engine}) (\"#{Puma::Const::CODE_NAME}\")"
101
+ log "* Puma version: #{Puma::Const::PUMA_VERSION} (\"#{Puma::Const::CODE_NAME}\")"
102
+ log "* Ruby version: #{RUBY_DESCRIPTION}"
113
103
  log "* Min threads: #{min_t}"
114
104
  log "* Max threads: #{max_t}"
115
105
  log "* Environment: #{environment}"
@@ -121,6 +111,14 @@ module Puma
121
111
  end
122
112
  end
123
113
 
114
+ def warn_ruby_mn_threads
115
+ return if !ENV.key?('RUBY_MN_THREADS')
116
+
117
+ log "! WARNING: Detected `RUBY_MN_THREADS=#{ENV['RUBY_MN_THREADS']}`"
118
+ log "! This setting is known to cause performance regressions with Puma."
119
+ log "! Consider disabling this environment variable: https://github.com/puma/puma/issues/3720"
120
+ end
121
+
124
122
  def redirected_io?
125
123
  @options[:redirect_stdout] || @options[:redirect_stderr]
126
124
  end
@@ -137,10 +137,7 @@ module Puma
137
137
  ENV.delete("NOTIFY_SOCKET") if unset_env
138
138
 
139
139
  begin
140
- Addrinfo.unix(sock, :DGRAM).connect do |s|
141
- s.close_on_exec = true
142
- s.write(state)
143
- end
140
+ Addrinfo.unix(sock, :DGRAM).connect { |s| s.write state }
144
141
  rescue StandardError => e
145
142
  raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
143
  end