puma 6.6.0 → 7.0.3

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.
@@ -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,17 @@ 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
156
158
  queue_requests: true,
157
159
  rackup: 'config.ru'.freeze,
158
160
  raise_exception_on_sigterm: true,
@@ -176,24 +178,31 @@ module Puma
176
178
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
177
179
  default_options = self.puma_default_options(env).merge(default_options)
178
180
 
179
- @options = UserFileDefaultOptions.new(user_options, default_options)
181
+ @_options = UserFileDefaultOptions.new(user_options, default_options)
180
182
  @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
183
+ @events = @_options[:events] || Events.new
184
+ @hooks = {}
185
+ @user_dsl = DSL.new(@_options.user_options, self)
186
+ @file_dsl = DSL.new(@_options.file_options, self)
187
+ @default_dsl = DSL.new(@_options.default_options, self)
188
188
 
189
189
  @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'
190
190
 
191
191
  if block
192
192
  configure(&block)
193
193
  end
194
+
195
+ @loaded = false
196
+ @clamped = false
194
197
  end
195
198
 
196
- attr_reader :options, :plugins
199
+ attr_reader :plugins, :events, :hooks
200
+
201
+ def options
202
+ raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped
203
+
204
+ @_options
205
+ end
197
206
 
198
207
  def configure
199
208
  yield @user_dsl, @file_dsl, @default_dsl
@@ -206,7 +215,7 @@ module Puma
206
215
  def initialize_copy(other)
207
216
  @conf = nil
208
217
  @cli_options = nil
209
- @options = @options.dup
218
+ @_options = @_options.dup
210
219
  end
211
220
 
212
221
  def flatten
@@ -214,7 +223,7 @@ module Puma
214
223
  end
215
224
 
216
225
  def flatten!
217
- @options = @options.flatten
226
+ @_options = @_options.flatten
218
227
  self
219
228
  end
220
229
 
@@ -227,6 +236,7 @@ module Puma
227
236
  def puma_options_from_env(env = ENV)
228
237
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
229
238
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
239
+ persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
230
240
  workers = if env['WEB_CONCURRENCY'] == 'auto'
231
241
  require_processor_counter
232
242
  ::Concurrent.available_processor_count
@@ -237,24 +247,27 @@ module Puma
237
247
  {
238
248
  min_threads: min && min != "" && Integer(min),
239
249
  max_threads: max && max != "" && Integer(max),
250
+ persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
240
251
  workers: workers && workers != "" && Integer(workers),
241
252
  environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
242
253
  }
243
254
  end
244
255
 
245
256
  def load
257
+ @loaded = true
246
258
  config_files.each { |config_file| @file_dsl._load_from(config_file) }
247
-
248
- @options
259
+ @_options
249
260
  end
250
261
 
251
262
  def config_files
252
- files = @options.all_of(:config_files)
263
+ raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded
264
+
265
+ files = @_options.all_of(:config_files)
253
266
 
254
267
  return [] if files == ['-']
255
268
  return files if files.any?
256
269
 
257
- first_default_file = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find do |f|
270
+ first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
258
271
  File.exist?(f)
259
272
  end
260
273
 
@@ -262,9 +275,16 @@ module Puma
262
275
  end
263
276
 
264
277
  # Call once all configuration (included from rackup files)
265
- # is loaded to flesh out any defaults
278
+ # is loaded to finalize defaults and lock in the configuration.
279
+ #
280
+ # This also calls load if it hasn't been called yet.
266
281
  def clamp
267
- @options.finalize_values
282
+ load unless @loaded
283
+ set_conditional_default_options
284
+ @_options.finalize_values
285
+ @clamped = true
286
+ warn_hooks
287
+ options
268
288
  end
269
289
 
270
290
  # Injects the Configuration object into the env
@@ -283,11 +303,11 @@ module Puma
283
303
  # Indicate if there is a properly configured app
284
304
  #
285
305
  def app_configured?
286
- @options[:app] || File.exist?(rackup)
306
+ options[:app] || File.exist?(rackup)
287
307
  end
288
308
 
289
309
  def rackup
290
- @options[:rackup]
310
+ options[:rackup]
291
311
  end
292
312
 
293
313
  # Load the specified rackup file, pull options from
@@ -296,9 +316,9 @@ module Puma
296
316
  def app
297
317
  found = options[:app] || load_rackup
298
318
 
299
- if @options[:log_requests]
319
+ if options[:log_requests]
300
320
  require_relative 'commonlogger'
301
- logger = @options[:logger]
321
+ logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
302
322
  found = CommonLogger.new(found, logger)
303
323
  end
304
324
 
@@ -307,7 +327,7 @@ module Puma
307
327
 
308
328
  # Return which environment we're running in
309
329
  def environment
310
- @options[:environment]
330
+ options[:environment]
311
331
  end
312
332
 
313
333
  def load_plugin(name)
@@ -315,18 +335,19 @@ module Puma
315
335
  end
316
336
 
317
337
  # @param key [:Symbol] hook to run
318
- # @param arg [Launcher, Int] `:on_restart` passes Launcher
338
+ # @param arg [Launcher, Int] `:before_restart` passes Launcher
319
339
  #
320
340
  def run_hooks(key, arg, log_writer, hook_data = nil)
321
341
  log_writer.debug "Running #{key} hooks"
322
342
 
323
- @options.all_of(key).each do |b|
343
+ options.all_of(key).each do |hook_options|
324
344
  begin
325
- if Array === b
326
- hook_data[b[1]] ||= Hash.new
327
- b[0].call arg, hook_data[b[1]]
345
+ block = hook_options[:block]
346
+ if id = hook_options[:id]
347
+ hook_data[id] ||= Hash.new
348
+ block.call arg, hook_data[id]
328
349
  else
329
- b.call arg
350
+ block.call arg
330
351
  end
331
352
  rescue => e
332
353
  log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
@@ -336,7 +357,7 @@ module Puma
336
357
  end
337
358
 
338
359
  def final_options
339
- @options.final_options
360
+ options.final_options
340
361
  end
341
362
 
342
363
  def self.temp_path
@@ -346,6 +367,12 @@ module Puma
346
367
  "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
347
368
  end
348
369
 
370
+ def self.random_token
371
+ require 'securerandom' unless defined?(SecureRandom)
372
+
373
+ SecureRandom.hex(16)
374
+ end
375
+
349
376
  private
350
377
 
351
378
  def require_processor_counter
@@ -386,22 +413,40 @@ module Puma
386
413
  rack_app, rack_options = rack_builder.parse_file(rackup)
387
414
  rack_options = rack_options || {}
388
415
 
389
- @options.file_options.merge!(rack_options)
416
+ options.file_options.merge!(rack_options)
390
417
 
391
418
  config_ru_binds = []
392
419
  rack_options.each do |k, v|
393
420
  config_ru_binds << v if k.to_s.start_with?("bind")
394
421
  end
395
422
 
396
- @options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
423
+ options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?
397
424
 
398
425
  rack_app
399
426
  end
400
427
 
401
- def self.random_token
402
- require 'securerandom' unless defined?(SecureRandom)
428
+ def set_conditional_default_options
429
+ @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
430
+ (@_options[:workers] > 1) && Puma.forkable?
431
+ end
403
432
 
404
- SecureRandom.hex(16)
433
+ def warn_hooks
434
+ return if options[:workers] > 0
435
+ return if options[:silence_fork_callback_warning]
436
+
437
+ log_writer = LogWriter.stdio
438
+ @hooks.each_key do |hook|
439
+ options.all_of(hook).each do |hook_options|
440
+ next unless hook_options[:cluster_only]
441
+
442
+ log_writer.log(<<~MSG.tr("\n", " "))
443
+ Warning: The code in the `#{hook}` block will not execute
444
+ in the current Puma configuration. The `#{hook}` block only
445
+ executes in Puma's cluster mode. To fix this, either remove the
446
+ `#{hook}` call or increase Puma's worker count above zero.
447
+ MSG
448
+ end
449
+ end
405
450
  end
406
451
  end
407
452
  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.0"
104
- CODE_NAME = "Return to Forever"
103
+ PUMA_VERSION = VERSION = "7.0.3"
104
+ CODE_NAME = "Romantic Warrior"
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