puma 6.6.1 → 7.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +224 -4
  3. data/README.md +34 -34
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +5 -5
  6. data/docs/jungle/README.md +1 -1
  7. data/docs/kubernetes.md +11 -16
  8. data/docs/plugins.md +2 -2
  9. data/docs/restart.md +2 -2
  10. data/docs/signals.md +19 -19
  11. data/docs/stats.md +4 -3
  12. data/docs/systemd.md +3 -3
  13. data/ext/puma_http11/extconf.rb +2 -17
  14. data/ext/puma_http11/mini_ssl.c +18 -8
  15. data/ext/puma_http11/org/jruby/puma/Http11.java +9 -1
  16. data/ext/puma_http11/puma_http11.c +122 -118
  17. data/lib/puma/app/status.rb +10 -2
  18. data/lib/puma/binder.rb +10 -8
  19. data/lib/puma/cli.rb +3 -5
  20. data/lib/puma/client.rb +52 -56
  21. data/lib/puma/cluster/worker.rb +17 -17
  22. data/lib/puma/cluster/worker_handle.rb +38 -7
  23. data/lib/puma/cluster.rb +23 -23
  24. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  25. data/lib/puma/commonlogger.rb +3 -3
  26. data/lib/puma/configuration.rb +104 -51
  27. data/lib/puma/const.rb +9 -10
  28. data/lib/puma/control_cli.rb +6 -2
  29. data/lib/puma/detect.rb +2 -0
  30. data/lib/puma/dsl.rb +149 -91
  31. data/lib/puma/error_logger.rb +3 -1
  32. data/lib/puma/events.rb +25 -10
  33. data/lib/puma/io_buffer.rb +8 -4
  34. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  35. data/lib/puma/launcher.rb +54 -49
  36. data/lib/puma/minissl.rb +0 -1
  37. data/lib/puma/plugin/systemd.rb +3 -3
  38. data/lib/puma/rack/urlmap.rb +1 -1
  39. data/lib/puma/reactor.rb +19 -13
  40. data/lib/puma/request.rb +42 -31
  41. data/lib/puma/runner.rb +9 -18
  42. data/lib/puma/server.rb +114 -64
  43. data/lib/puma/single.rb +6 -3
  44. data/lib/puma/state_file.rb +3 -2
  45. data/lib/puma/thread_pool.rb +47 -82
  46. data/lib/puma/util.rb +0 -7
  47. data/lib/puma.rb +10 -0
  48. data/lib/rack/handler/puma.rb +2 -2
  49. data/tools/Dockerfile +13 -5
  50. metadata +6 -5
  51. data/ext/puma_http11/ext_help.h +0 -15
@@ -3,6 +3,7 @@
3
3
  require_relative 'plugin'
4
4
  require_relative 'const'
5
5
  require_relative 'dsl'
6
+ require_relative 'events'
6
7
 
7
8
  module Puma
8
9
  # A class used for storing "leveled" configuration options.
@@ -112,7 +113,7 @@ module Puma
112
113
  # config = Configuration.new({}) do |user_config, file_config, default_config|
113
114
  # user_config.port 3003
114
115
  # end
115
- # config.load
116
+ # config.clamp
116
117
  # puts config.options[:port]
117
118
  # # => 3003
118
119
  #
@@ -125,10 +126,13 @@ module Puma
125
126
  # is done because an environment variable may have been modified while loading
126
127
  # configuration files.
127
128
  class Configuration
129
+ class NotLoadedError < StandardError; end
130
+ class NotClampedError < StandardError; end
131
+
128
132
  DEFAULTS = {
129
133
  auto_trim_time: 30,
130
134
  binds: ['tcp://0.0.0.0:9292'.freeze],
131
- clean_thread_locals: false,
135
+ fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
132
136
  debug: false,
133
137
  enable_keep_alives: true,
134
138
  early_hints: nil,
@@ -140,19 +144,18 @@ module Puma
140
144
  io_selector_backend: :auto,
141
145
  log_requests: false,
142
146
  logger: STDOUT,
143
- # How many requests to attempt inline before sending a client back to
144
- # the reactor to be subject to normal ordering. The idea here is that
145
- # we amortize the cost of going back to the reactor for a well behaved
146
- # but very "greedy" client across 10 requests. This prevents a not
147
- # well behaved client from monopolizing the thread forever.
148
- max_fast_inline: 10,
147
+ # Limits how many requests a keep alive connection can make.
148
+ # The connection will be closed after it reaches `max_keep_alive`
149
+ # requests.
150
+ max_keep_alive: 999,
149
151
  max_threads: Puma.mri? ? 5 : 16,
150
152
  min_threads: 0,
151
153
  mode: :http,
152
154
  mutate_stdout_and_stderr_to_sync_on_write: true,
153
155
  out_of_band: [],
154
156
  # Number of seconds for another request within a persistent session.
155
- persistent_timeout: 20,
157
+ persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
158
+ prune_bundler: false,
156
159
  queue_requests: true,
157
160
  rackup: 'config.ru'.freeze,
158
161
  raise_exception_on_sigterm: true,
@@ -176,24 +179,31 @@ module Puma
176
179
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
177
180
  default_options = self.puma_default_options(env).merge(default_options)
178
181
 
179
- @options = UserFileDefaultOptions.new(user_options, default_options)
182
+ @_options = UserFileDefaultOptions.new(user_options, default_options)
180
183
  @plugins = PluginLoader.new
181
- @user_dsl = DSL.new(@options.user_options, self)
182
- @file_dsl = DSL.new(@options.file_options, self)
183
- @default_dsl = DSL.new(@options.default_options, self)
184
-
185
- if !@options[:prune_bundler]
186
- default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
187
- end
184
+ @events = @_options[:events] || Events.new
185
+ @hooks = {}
186
+ @user_dsl = DSL.new(@_options.user_options, self)
187
+ @file_dsl = DSL.new(@_options.file_options, self)
188
+ @default_dsl = DSL.new(@_options.default_options, self)
188
189
 
189
190
  @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
190
191
 
191
192
  if block
192
193
  configure(&block)
193
194
  end
195
+
196
+ @loaded = false
197
+ @clamped = false
194
198
  end
195
199
 
196
- attr_reader :options, :plugins
200
+ attr_reader :plugins, :events, :hooks, :_options
201
+
202
+ def options
203
+ raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
204
+
205
+ @_options
206
+ end
197
207
 
198
208
  def configure
199
209
  yield @user_dsl, @file_dsl, @default_dsl
@@ -206,7 +216,7 @@ module Puma
206
216
  def initialize_copy(other)
207
217
  @conf = nil
208
218
  @cli_options = nil
209
- @options = @options.dup
219
+ @_options = @_options.dup
210
220
  end
211
221
 
212
222
  def flatten
@@ -214,7 +224,7 @@ module Puma
214
224
  end
215
225
 
216
226
  def flatten!
217
- @options = @options.flatten
227
+ @_options = @_options.flatten
218
228
  self
219
229
  end
220
230
 
@@ -227,34 +237,34 @@ module Puma
227
237
  def puma_options_from_env(env = ENV)
228
238
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
229
239
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
230
- workers = if env['WEB_CONCURRENCY'] == 'auto'
231
- require_processor_counter
232
- ::Concurrent.available_processor_count
233
- else
234
- env['WEB_CONCURRENCY']
235
- end
240
+ persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
241
+ workers_env = env['WEB_CONCURRENCY']
242
+ workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil
236
243
 
237
244
  {
238
245
  min_threads: min && min != "" && Integer(min),
239
246
  max_threads: max && max != "" && Integer(max),
240
- workers: workers && workers != "" && Integer(workers),
247
+ persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
248
+ workers: workers,
241
249
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
242
250
  }
243
251
  end
244
252
 
245
253
  def load
254
+ @loaded = true
246
255
  config_files.each { |config_file| @file_dsl._load_from(config_file) }
247
-
248
- @options
256
+ @_options
249
257
  end
250
258
 
251
259
  def config_files
252
- files = @options.all_of(:config_files)
260
+ raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
261
+
262
+ files = @_options.all_of(:config_files)
253
263
 
254
264
  return [] if files == ['-']
255
265
  return files if files.any?
256
266
 
257
- first_default_file = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find do |f|
267
+ first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
258
268
  File.exist?(f)
259
269
  end
260
270
 
@@ -262,9 +272,16 @@ module Puma
262
272
  end
263
273
 
264
274
  # Call once all configuration (included from rackup files)
265
- # is loaded to flesh out any defaults
275
+ # is loaded to finalize defaults and lock in the configuration.
276
+ #
277
+ # This also calls load if it hasn't been called yet.
266
278
  def clamp
267
- @options.finalize_values
279
+ load unless @loaded
280
+ set_conditional_default_options
281
+ @_options.finalize_values
282
+ @clamped = true
283
+ warn_hooks
284
+ options
268
285
  end
269
286
 
270
287
  # Injects the Configuration object into the env
@@ -283,11 +300,11 @@ module Puma
283
300
  # Indicate if there is a properly configured app
284
301
  #
285
302
  def app_configured?
286
- @options[:app] || File.exist?(rackup)
303
+ options[:app] || File.exist?(rackup)
287
304
  end
288
305
 
289
306
  def rackup
290
- @options[:rackup]
307
+ options[:rackup]
291
308
  end
292
309
 
293
310
  # Load the specified rackup file, pull options from
@@ -296,9 +313,9 @@ module Puma
296
313
  def app
297
314
  found = options[:app] || load_rackup
298
315
 
299
- if @options[:log_requests]
316
+ if options[:log_requests]
300
317
  require_relative 'commonlogger'
301
- logger = @options[:logger]
318
+ logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
302
319
  found = CommonLogger.new(found, logger)
303
320
  end
304
321
 
@@ -307,7 +324,7 @@ module Puma
307
324
 
308
325
  # Return which environment we're running in
309
326
  def environment
310
- @options[:environment]
327
+ options[:environment]
311
328
  end
312
329
 
313
330
  def load_plugin(name)
@@ -315,18 +332,19 @@ module Puma
315
332
  end
316
333
 
317
334
  # @param key [:Symbol] hook to run
318
- # @param arg [Launcher, Int] `:on_restart` passes Launcher
335
+ # @param arg [Launcher, Int] `:before_restart` passes Launcher
319
336
  #
320
337
  def run_hooks(key, arg, log_writer, hook_data = nil)
321
338
  log_writer.debug "Running #{key} hooks"
322
339
 
323
- @options.all_of(key).each do |b|
340
+ options.all_of(key).each do |hook_options|
324
341
  begin
325
- if Array === b
326
- hook_data[b[1]] ||= Hash.new
327
- b[0].call arg, hook_data[b[1]]
342
+ block = hook_options[:block]
343
+ if id = hook_options[:id]
344
+ hook_data[id] ||= Hash.new
345
+ block.call arg, hook_data[id]
328
346
  else
329
- b.call arg
347
+ block.call arg
330
348
  end
331
349
  rescue => e
332
350
  log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
@@ -336,7 +354,7 @@ module Puma
336
354
  end
337
355
 
338
356
  def final_options
339
- @options.final_options
357
+ options.final_options
340
358
  end
341
359
 
342
360
  def self.temp_path
@@ -346,18 +364,35 @@ module Puma
346
364
  "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
347
365
  end
348
366
 
367
+ def self.random_token
368
+ require 'securerandom' unless defined?(SecureRandom)
369
+
370
+ SecureRandom.hex(16)
371
+ end
372
+
349
373
  private
350
374
 
351
375
  def require_processor_counter
352
376
  require 'concurrent/utility/processor_counter'
353
377
  rescue LoadError
354
378
  warn <<~MESSAGE
355
- WEB_CONCURRENCY=auto requires the "concurrent-ruby" gem to be installed.
379
+ WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
356
380
  Please add "concurrent-ruby" to your Gemfile.
357
381
  MESSAGE
358
382
  raise
359
383
  end
360
384
 
385
+ def parse_workers(value)
386
+ if value == :auto || value == 'auto'
387
+ require_processor_counter
388
+ Integer(::Concurrent.available_processor_count)
389
+ else
390
+ Integer(value)
391
+ end
392
+ rescue ArgumentError, TypeError
393
+ raise ArgumentError, "workers must be an Integer or :auto"
394
+ end
395
+
361
396
  # Load and use the normal Rack builder if we can, otherwise
362
397
  # fallback to our minimal version.
363
398
  def rack_builder
@@ -386,22 +421,40 @@ module Puma
386
421
  rack_app, rack_options = rack_builder.parse_file(rackup)
387
422
  rack_options = rack_options || {}
388
423
 
389
- @options.file_options.merge!(rack_options)
424
+ options.file_options.merge!(rack_options)
390
425
 
391
426
  config_ru_binds = []
392
427
  rack_options.each do |k, v|
393
428
  config_ru_binds << v if k.to_s.start_with?("bind")
394
429
  end
395
430
 
396
- @options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
431
+ options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
397
432
 
398
433
  rack_app
399
434
  end
400
435
 
401
- def self.random_token
402
- require 'securerandom' unless defined?(SecureRandom)
436
+ def set_conditional_default_options
437
+ @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
438
+ (@_options[:workers] > 1) && Puma.forkable?
439
+ end
403
440
 
404
- SecureRandom.hex(16)
441
+ def warn_hooks
442
+ return if options[:workers] > 0
443
+ return if options[:silence_fork_callback_warning]
444
+
445
+ log_writer = LogWriter.stdio
446
+ @hooks.each_key do |hook|
447
+ options.all_of(hook).each do |hook_options|
448
+ next unless hook_options[:cluster_only]
449
+
450
+ log_writer.log(<<~MSG.tr("\n", " "))
451
+ Warning: The code in the `#{hook}` block will not execute
452
+ in the current Puma configuration. The `#{hook}` block only
453
+ executes in Puma's cluster mode. To fix this, either remove the
454
+ `#{hook}` call or increase Puma's worker count above zero.
455
+ MSG
456
+ end
457
+ end
405
458
  end
406
459
  end
407
460
  end
data/lib/puma/const.rb CHANGED
@@ -100,13 +100,11 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "6.6.1"
104
- CODE_NAME = "Return to Forever"
103
+ PUMA_VERSION = VERSION = "7.2.0"
104
+ CODE_NAME = "On The Corner"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
108
- FAST_TRACK_KA_TIMEOUT = 0.2
109
-
110
108
  # How long to wait when getting some write blocking on the socket when
111
109
  # sending data back
112
110
  WRITE_TIMEOUT = 10
@@ -125,9 +123,9 @@ module Puma
125
123
  # Indicate that we couldn't parse the request
126
124
  400 => "HTTP/1.1 400 Bad Request\r\n\r\n",
127
125
  # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
128
- 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n",
126
+ 404 => "HTTP/1.1 404 Not Found\r\nconnection: close\r\n\r\n",
129
127
  # The standard empty 408 response for requests that timed out.
130
- 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n",
128
+ 408 => "HTTP/1.1 408 Request Timeout\r\nconnection: close\r\n\r\n",
131
129
  # Indicate that there was an internal error, obviously.
132
130
  500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n",
133
131
  # Incorrect or invalid header value
@@ -230,6 +228,7 @@ module Puma
230
228
  RACK_INPUT = "rack.input"
231
229
  RACK_URL_SCHEME = "rack.url_scheme"
232
230
  RACK_AFTER_REPLY = "rack.after_reply"
231
+ RACK_RESPONSE_FINISHED = "rack.response_finished"
233
232
  PUMA_SOCKET = "puma.socket"
234
233
  PUMA_CONFIG = "puma.config"
235
234
  PUMA_PEERCERT = "puma.peercert"
@@ -252,14 +251,14 @@ module Puma
252
251
  KEEP_ALIVE = "keep-alive"
253
252
 
254
253
  CONTENT_LENGTH2 = "content-length"
255
- CONTENT_LENGTH_S = "Content-Length: "
254
+ CONTENT_LENGTH_S = "content-length: "
256
255
  TRANSFER_ENCODING = "transfer-encoding"
257
256
  TRANSFER_ENCODING2 = "HTTP_TRANSFER_ENCODING"
258
257
 
259
- CONNECTION_CLOSE = "Connection: close\r\n"
260
- CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n"
258
+ CONNECTION_CLOSE = "connection: close\r\n"
259
+ CONNECTION_KEEP_ALIVE = "connection: keep-alive\r\n"
261
260
 
262
- TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n"
261
+ TRANSFER_ENCODING_CHUNKED = "transfer-encoding: chunked\r\n"
263
262
  CLOSE_CHUNKED = "0\r\n\r\n"
264
263
 
265
264
  CHUNKED = "chunked"
@@ -124,11 +124,15 @@ module Puma
124
124
  end
125
125
 
126
126
  if @config_file
127
+ # needed because neither `Puma::CLI` or `Puma::Server` are loaded
128
+ require_relative '../puma'
129
+
127
130
  require_relative 'configuration'
128
131
  require_relative 'log_writer'
129
132
 
130
133
  config = Puma::Configuration.new({ config_files: [@config_file] }, {} , env)
131
- config.load
134
+ config.clamp
135
+
132
136
  @state ||= config.options[:state]
133
137
  @control_url ||= config.options[:control_url]
134
138
  @control_auth_token ||= config.options[:control_auth_token]
@@ -248,7 +252,7 @@ module Puma
248
252
  @stdout.flush unless @stdout.sync
249
253
  return
250
254
  elsif sig.start_with? 'SIG'
251
- if Signal.list.key? sig.sub(/\ASIG/, '')
255
+ if Signal.list.key? sig.delete_prefix('SIG')
252
256
  Process.kill sig, @pid
253
257
  else
254
258
  raise "Signal '#{sig}' not available'"
data/lib/puma/detect.rb CHANGED
@@ -18,6 +18,8 @@ module Puma
18
18
 
19
19
  IS_LINUX = !(IS_OSX || IS_WINDOWS)
20
20
 
21
+ IS_ARM = RUBY_PLATFORM.include? 'aarch64'
22
+
21
23
  # @version 5.2.0
22
24
  IS_MRI = RUBY_ENGINE == 'ruby'
23
25