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