puma 6.4.1 → 7.2.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +407 -8
  3. data/README.md +109 -49
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +11 -1
  6. data/docs/java_options.md +54 -0
  7. data/docs/jungle/README.md +1 -1
  8. data/docs/kubernetes.md +11 -16
  9. data/docs/plugins.md +6 -2
  10. data/docs/restart.md +2 -2
  11. data/docs/signals.md +21 -21
  12. data/docs/stats.md +11 -5
  13. data/docs/systemd.md +14 -5
  14. data/ext/puma_http11/extconf.rb +20 -32
  15. data/ext/puma_http11/mini_ssl.c +29 -9
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
  17. data/ext/puma_http11/puma_http11.c +125 -118
  18. data/lib/puma/app/status.rb +11 -3
  19. data/lib/puma/binder.rb +21 -11
  20. data/lib/puma/cli.rb +10 -8
  21. data/lib/puma/client.rb +183 -83
  22. data/lib/puma/cluster/worker.rb +24 -21
  23. data/lib/puma/cluster/worker_handle.rb +38 -8
  24. data/lib/puma/cluster.rb +73 -47
  25. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  26. data/lib/puma/commonlogger.rb +3 -3
  27. data/lib/puma/configuration.rb +131 -60
  28. data/lib/puma/const.rb +31 -12
  29. data/lib/puma/control_cli.rb +10 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +411 -121
  32. data/lib/puma/error_logger.rb +7 -5
  33. data/lib/puma/events.rb +25 -10
  34. data/lib/puma/io_buffer.rb +8 -4
  35. data/lib/puma/jruby_restart.rb +0 -16
  36. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  37. data/lib/puma/launcher.rb +73 -55
  38. data/lib/puma/log_writer.rb +9 -9
  39. data/lib/puma/minissl/context_builder.rb +1 -0
  40. data/lib/puma/minissl.rb +1 -1
  41. data/lib/puma/null_io.rb +26 -0
  42. data/lib/puma/plugin/systemd.rb +3 -3
  43. data/lib/puma/rack/urlmap.rb +1 -1
  44. data/lib/puma/reactor.rb +19 -13
  45. data/lib/puma/request.rb +71 -39
  46. data/lib/puma/runner.rb +15 -17
  47. data/lib/puma/sd_notify.rb +1 -4
  48. data/lib/puma/server.rb +134 -73
  49. data/lib/puma/single.rb +7 -4
  50. data/lib/puma/state_file.rb +3 -2
  51. data/lib/puma/thread_pool.rb +57 -80
  52. data/lib/puma/util.rb +0 -7
  53. data/lib/puma.rb +10 -0
  54. data/lib/rack/handler/puma.rb +10 -7
  55. data/tools/Dockerfile +15 -5
  56. metadata +14 -15
  57. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/request.rb CHANGED
@@ -36,25 +36,27 @@ 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.
46
49
  # @param client [Puma::Client]
47
50
  # @param requests [Integer]
48
- # @return [Boolean,:async]
49
- #
51
+ # @return [:close, :keep_alive, :async]
50
52
  def handle_request(client, requests)
51
53
  env = client.env
52
54
  io_buffer = client.io_buffer
53
55
  socket = client.io # io may be a MiniSSL::Socket
54
56
  app_body = nil
57
+ error = nil
55
58
 
56
-
57
- return false if closed_socket?(socket)
59
+ return :close if closed_socket?(socket)
58
60
 
59
61
  if client.http_content_length_limit_exceeded
60
62
  return prepare_response(413, {}, ["Payload Too Large"], requests, client)
@@ -69,7 +71,7 @@ module Puma
69
71
  end
70
72
 
71
73
  env[HIJACK_P] = true
72
- env[HIJACK] = client
74
+ env[HIJACK] = client.method :full_hijack
73
75
 
74
76
  env[RACK_INPUT] = client.body
75
77
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
@@ -93,6 +95,7 @@ module Puma
93
95
  # array, we will invoke them when the request is done.
94
96
  #
95
97
  env[RACK_AFTER_REPLY] ||= []
98
+ env[RACK_RESPONSE_FINISHED] ||= []
96
99
 
97
100
  begin
98
101
  if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
@@ -120,28 +123,41 @@ module Puma
120
123
 
121
124
  return :async
122
125
  end
123
- rescue ThreadPool::ForceShutdown => e
124
- @log_writer.unknown_error e, client, "Rack app"
126
+ rescue ThreadPool::ForceShutdown => error
127
+ @log_writer.unknown_error error, client, "Rack app"
125
128
  @log_writer.log "Detected force shutdown of a thread"
126
129
 
127
- status, headers, res_body = lowlevel_error(e, env, 503)
128
- rescue Exception => e
129
- @log_writer.unknown_error e, client, "Rack app"
130
+ status, headers, res_body = lowlevel_error(error, env, 503)
131
+ rescue Exception => error
132
+ @log_writer.unknown_error error, client, "Rack app"
130
133
 
131
- status, headers, res_body = lowlevel_error(e, env, 500)
134
+ status, headers, res_body = lowlevel_error(error, env, 500)
132
135
  end
133
136
  prepare_response(status, headers, res_body, requests, client)
134
137
  ensure
135
138
  io_buffer.reset
136
139
  uncork_socket client.io
137
140
  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?
141
+ client&.tempfile_close
142
+ if after_reply = env[RACK_AFTER_REPLY]
143
+ after_reply.each do |o|
144
+ begin
145
+ o.call
146
+ rescue StandardError => e
147
+ @log_writer.debug_error e
148
+ end
149
+ end
150
+ end
151
+
152
+ if response_finished = env[RACK_RESPONSE_FINISHED]
153
+ response_finished.reverse_each do |o|
154
+ begin
155
+ o.call(env, status, headers, error)
156
+ rescue StandardError => e
157
+ @log_writer.debug_error e
158
+ end
159
+ end
160
+ end
145
161
  end
146
162
 
147
163
  # Assembles the headers and prepares the body for actually sending the
@@ -153,21 +169,16 @@ module Puma
153
169
  # a call to `Server#lowlevel_error`
154
170
  # @param requests [Integer] number of inline requests handled
155
171
  # @param client [Puma::Client]
156
- # @return [Boolean,:async] keep-alive status or `:async`
172
+ # @return [:close, :keep_alive, :async]
157
173
  def prepare_response(status, headers, res_body, requests, client)
158
174
  env = client.env
159
175
  socket = client.io
160
176
  io_buffer = client.io_buffer
161
177
 
162
- return false if closed_socket?(socket)
178
+ return :close if closed_socket?(socket)
163
179
 
164
180
  # 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)
181
+ force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
171
182
 
172
183
  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
173
184
 
@@ -187,7 +198,8 @@ module Puma
187
198
  elsif res_body.is_a?(File) && res_body.respond_to?(:size)
188
199
  body = res_body
189
200
  content_length = body.size
190
- elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
201
+ elsif res_body.respond_to?(:to_path) && (fn = res_body.to_path) &&
202
+ File.readable?(fn)
191
203
  body = File.open fn, 'rb'
192
204
  content_length = body.size
193
205
  close_body = true
@@ -195,7 +207,7 @@ module Puma
195
207
  body = res_body
196
208
  end
197
209
  elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
198
- File.readable?(fn = res_body.to_path)
210
+ (fn = res_body.to_path) && File.readable?(fn = res_body.to_path)
199
211
  body = File.open fn, 'rb'
200
212
  content_length = body.size
201
213
  close_body = true
@@ -234,7 +246,7 @@ module Puma
234
246
  io_buffer << LINE_END
235
247
  fast_write_str socket, io_buffer.read_and_reset
236
248
  socket.flush
237
- return keep_alive
249
+ return keep_alive ? :keep_alive : :close
238
250
  end
239
251
  else
240
252
  if content_length
@@ -259,14 +271,21 @@ module Puma
259
271
 
260
272
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
261
273
  body.close if close_body
262
- keep_alive
274
+ # if we're shutting down, close keep_alive connections
275
+ !shutting_down? && keep_alive ? :keep_alive : :close
263
276
  end
264
277
 
278
+ HTTP_ON_VALUES = { "on" => true, HTTPS => true }
279
+ private_constant :HTTP_ON_VALUES
280
+
265
281
  # @param env [Hash] see Puma::Client#env, from request
266
282
  # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
267
283
  #
268
284
  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"
285
+ if HTTP_ON_VALUES[env[HTTPS_KEY]] ||
286
+ env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
287
+ env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
288
+ env[HTTP_X_FORWARDED_SSL] == "on"
270
289
  PORT_443
271
290
  else
272
291
  PORT_80
@@ -470,7 +489,7 @@ module Puma
470
489
 
471
490
  # The legacy HTTP_VERSION header can be sent as a client header.
472
491
  # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
473
- env[HTTP_VERSION] = env[SERVER_PROTOCOL]
492
+ env[HTTP_VERSION] = env[SERVER_PROTOCOL] if @env_set_http_version
474
493
  end
475
494
  private :normalize_env
476
495
 
@@ -495,6 +514,11 @@ module Puma
495
514
  # compatibility, we'll convert them back. This code is written to
496
515
  # avoid allocation in the common case (ie there are no headers
497
516
  # with `,` in their names), that's why it has the extra conditionals.
517
+ #
518
+ # @note If a normalized version of a `,` header already exists, we ignore
519
+ # the `,` version. This prevents clobbering headers managed by proxies
520
+ # but not by clients (Like X-Forwarded-For).
521
+ #
498
522
  # @param env [Hash] see Puma::Client#env, from request, modifies in place
499
523
  # @version 5.0.3
500
524
  #
@@ -503,23 +527,31 @@ module Puma
503
527
  to_add = nil
504
528
 
505
529
  env.each do |k,v|
506
- if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
530
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
507
531
  if to_delete
508
532
  to_delete << k
509
533
  else
510
534
  to_delete = [k]
511
535
  end
512
536
 
537
+ new_k = k.tr(",", "_")
538
+ if env.key?(new_k)
539
+ next
540
+ end
541
+
513
542
  unless to_add
514
543
  to_add = {}
515
544
  end
516
545
 
517
- to_add[k.tr(",", "_")] = v
546
+ to_add[new_k] = v
518
547
  end
519
548
  end
520
549
 
521
- if to_delete
550
+ if to_delete # rubocop:disable Style/SafeNavigation
522
551
  to_delete.each { |k| env.delete(k) }
552
+ end
553
+
554
+ if to_add
523
555
  env.merge! to_add
524
556
  end
525
557
  end
@@ -564,7 +596,7 @@ module Puma
564
596
  # response body
565
597
  # @param io_buffer [Puma::IOBuffer] modified inn place
566
598
  # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
567
- # status and `@max_fast_inline`
599
+ # status and `@max_keep_alive`
568
600
  # @return [Hash] resp_info
569
601
  # @version 5.0.3
570
602
  #
@@ -647,10 +679,10 @@ module Puma
647
679
  if ary
648
680
  ary.each do |v|
649
681
  next if illegal_header_value?(v)
650
- io_buffer.append k, colon, v, line_ending
682
+ io_buffer.append k.downcase, colon, v, line_ending
651
683
  end
652
684
  else
653
- io_buffer.append k, colon, line_ending
685
+ io_buffer.append k.downcase, colon, line_ending
654
686
  end
655
687
  end
656
688
 
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