123456789_123456789_123456789_123456789_123456789_

Class: Puma::Configuration

Relationships & Source Files
Namespace Children
Classes:
Exceptions:
Inherits: Object
Defined in: lib/puma/configuration.rb

Overview

The main configuration class of ::Puma.

It can be initialized with a set of “user” options and “default” options. Defaults will be merged with #puma_default_options.

This class works together with 2 main other classes the UserFileDefaultOptions which stores configuration options in order so the precedence is that user set configuration wins over “file” based configuration wins over “default” configuration. These configurations are set via the DSL class. This class powers the ::Puma config file syntax and does double duty as a configuration DSL used by the CLI and ::Puma rack handler.

It also handles loading plugins.

Note:

:port and :host are not valid keys. By the time they make it to the configuration options they are expected to be incorporated into a :binds key. Under the hood the DSL maps port and host calls to :binds

config = Configuration.new({}) do |user_config, file_config, default_config|
  user_config.port 3003
end
config.clamp
puts config.options[:port]
# => 3003

It is expected that #load is called on the configuration instance after setting config. This method expands any values in config_file and puts them into the correct configuration option hash.

Once all configuration is complete it is expected that #clamp will be called on the instance. This will expand any procs stored under “default” values. This is done because an environment variable may have been modified while loading configuration files.

Constant Summary

  • DEFAULTS =
    # File 'lib/puma/configuration.rb', line 135
    {
      auto_trim_time: 30,
      binds: ['tcp://[::]:9292'.freeze],
      debug: false,
      early_hints: nil,
      enable_keep_alives: true,
      environment: 'development'.freeze,
      fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
      # Number of seconds to wait until we get the first data for the request.
      first_data_timeout: 30,
      force_shutdown_after: -1,
      http_content_length_limit: nil,
      # Number of seconds to wait until the next request before shutting down.
      idle_timeout: nil,
      io_selector_backend: :auto,
      log_requests: false,
      logger: STDOUT,
      # Limits how many requests a keep alive connection can make.
      # The connection will be closed after it reaches `max_keep_alive`
      # requests.
      max_io_threads: 0,
      max_keep_alive: 999,
      max_threads: Puma.mri? ? 5 : 16,
      min_threads: 0,
      mode: :http,
      mutate_stdout_and_stderr_to_sync_on_write: true,
      out_of_band: [],
      # Number of seconds for another request within a persistent session.
      persistent_timeout: 65, # PUMA_PERSISTENT_TIMEOUT
      prune_bundler: false,
      queue_requests: true,
      rackup: 'config.ru'.freeze,
      raise_exception_on_sigterm: true,
      reaping_time: 1,
      remote_address: :socket,
      silence_fork_callback_warning: false,
      silence_single_worker_warning: false,
      tag: File.basename(Dir.getwd),
      tcp_host: '::'.freeze,
      tcp_port: 9292,
      wait_for_less_busy_worker: 0.005,
      worker_boot_timeout: 60,
      worker_check_interval: 5,
      worker_culling_strategy: :youngest,
      worker_shutdown_timeout: 30,
      worker_timeout: 60,
      workers: 0,
    }

Class Attribute Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(user_options = {}, default_options = {}, env = ENV, &block) ⇒ Configuration

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 184

def initialize(user_options={}, default_options = {}, env = ENV, &block)
  default_options = self.puma_default_options(env).merge(events: Events.new).merge(default_options)

  @_options    = UserFileDefaultOptions.new(user_options, default_options)
  @plugins     = PluginLoader.new
  @events      = @_options[:events] || Events.new
  @hooks       = {}
  @user_dsl    = DSL.new(@_options.user_options, self)
  @file_dsl    = DSL.new(@_options.file_options, self)
  @default_dsl = DSL.new(@_options.default_options, self)

  @puma_bundler_pruned = env.key? 'PUMA_BUNDLER_PRUNED'

  if block
    configure(&block)
  end

  @loaded = false
  @clamped = false
end

Class Attribute Details

.ipv6_interface_available?Boolean (readonly)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 377

def self.ipv6_interface_available?
  Socket.getifaddrs.any? do |ifaddr|
    addr = ifaddr.addr
    addr&.ipv6? && !addr&.ipv6_loopback?
  end
rescue StandardError
  false
end

Class Method Details

.default_tcp_bind(port = )

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 373

def self.default_tcp_bind(port = DEFAULTS[:tcp_port])
  URI::Generic.build(scheme: 'tcp', host: default_tcp_host, port: Integer(port)).to_s
end

.default_tcp_host

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 369

def self.default_tcp_host
  ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
end

.random_token

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 393

def self.random_token
  require 'securerandom' unless defined?(SecureRandom)

  SecureRandom.hex(16)
end

.temp_path

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 386

def self.temp_path
  require 'tmpdir'

  t = (Time.now.to_f * 1000).to_i
  "#{Dir.tmpdir}/puma-status-#{t}-#{$$}"
end

Instance Attribute Details

#_options (readonly)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 205

attr_reader :plugins, :events, :hooks, :_options

#app_configured?Boolean (readonly)

Indicate if there is a properly configured app

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 311

def app_configured?
  options[:app] || File.exist?(rackup)
end

#events (readonly)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 205

attr_reader :plugins, :events, :hooks, :_options

#hooks (readonly)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 205

attr_reader :plugins, :events, :hooks, :_options

#plugins (readonly)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 205

attr_reader :plugins, :events, :hooks, :_options

Instance Method Details

#app

Load the specified rackup file, pull options from the rackup file, and set @app.

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 322

def app
  found = options[:app] || load_rackup

  if options[:log_requests]
    require_relative 'commonlogger'
    logger = options[:custom_logger] ? options[:custom_logger] : options[:logger]
    found = CommonLogger.new(found, logger)
  end

  ConfigMiddleware.new(self, found)
end

#clamp

Call once all configuration (included from rackup files) is loaded to finalize defaults and lock in the configuration.

This also calls load if it hasn’t been called yet.

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 285

def clamp
  load unless @loaded
  run_mode_hooks
  set_conditional_default_options
  @_options.finalize_values
  rewrite_unavailable_ipv6_binds!
  @clamped = true
  warn_hooks
  options
end

#config_files

Raises:

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 266

def config_files
  raise NotLoadedError, "ensure load is called before accessing config_files" unless @loaded

  files = @_options.all_of(:config_files)

  return [] if files == ['-']
  return files if files.any?

  first_default_file = %W(config/puma/#{@_options[:environment]}.rb config/puma.rb).find do |f|
    File.exist?(f)
  end

  [first_default_file]
end

#configure

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 213

def configure
  yield @user_dsl, @file_dsl, @default_dsl
ensure
  @user_dsl._offer_plugins
  @file_dsl._offer_plugins
  @default_dsl._offer_plugins
end

#environment

Return which environment we’re running in

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 335

def environment
  options[:environment]
end

#final_options

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 365

def final_options
  options.final_options
end

#flatten

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 227

def flatten
  dup.flatten!
end

#flatten!

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 231

def flatten!
  @_options = @_options.flatten
  self
end

#initialize_copy(other)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 221

def initialize_copy(other)
  @conf        = nil
  @cli_options = nil
  @_options     = @_options.dup
end

#load

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 260

def load
  @loaded = true
  config_files.each { |config_file| @file_dsl._load_from(config_file) }
  @_options
end

#load_plugin(name)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 339

def load_plugin(name)
  @plugins.create name
end

#load_rackup (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 469

def load_rackup
  raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup)

  rack_app, rack_options = rack_builder.parse_file(rackup)
  rack_options = rack_options || {}

  options.file_options.merge!(rack_options)

  config_ru_binds = []
  rack_options.each do |k, v|
    config_ru_binds << v if k.to_s.start_with?("bind")
  end

  options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty?

  rack_app
end

#options

Raises:

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 207

def options
  raise NotClampedError, "ensure clamp is called before accessing options" unless @clamped

  @_options
end

#parse_workers(value) (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 436

def parse_workers(value)
  if value == :auto || value == 'auto'
    require_processor_counter
    Integer(::Concurrent.available_processor_count)
  else
    Integer(value)
  end
rescue ArgumentError, TypeError
  raise ArgumentError, "workers must be an Integer or :auto"
end

#puma_default_options(env = ENV)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 236

def puma_default_options(env = ENV)
  defaults = DEFAULTS.dup
  defaults[:tcp_host] = self.class.default_tcp_host
  defaults[:binds] = [self.class.default_tcp_bind]
  puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
  defaults
end

#puma_options_from_env(env = ENV)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 244

def puma_options_from_env(env = ENV)
  min = env['PUMA_MIN_THREADS'] || env['MIN_THREADS']
  max = env['PUMA_MAX_THREADS'] || env['MAX_THREADS']
  persistent_timeout = env['PUMA_PERSISTENT_TIMEOUT']
  workers_env = env['WEB_CONCURRENCY']
  workers = workers_env && workers_env.strip != "" ? parse_workers(workers_env.strip) : nil

  {
    min_threads: min && min != "" && Integer(min),
    max_threads: max && max != "" && Integer(max),
    persistent_timeout: persistent_timeout && persistent_timeout != "" && Integer(persistent_timeout),
    workers: workers,
    environment: env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV'],
  }
end

#rack_builder (private)

Load and use the normal Rack builder if we can, otherwise fallback to our minimal version.

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 449

def rack_builder
  # Load bundler now if we can so that we can pickup rack from
  # a Gemfile
  if @puma_bundler_pruned
    begin
      require 'bundler/setup'
    rescue LoadError
    end
  end

  begin
    require 'rack'
    require 'rack/builder'
    ::Rack::Builder
  rescue LoadError
    require_relative 'rack/builder'
    Puma::Rack::Builder
  end
end

#rackup

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 315

def rackup
  options[:rackup]
end

#require_processor_counter (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 426

def require_processor_counter
  require 'concurrent/utility/processor_counter'
rescue LoadError
  warn <<~MESSAGE
    WEB_CONCURRENCY=auto or workers(:auto) requires the "concurrent-ruby" gem to be installed.
    Please add "concurrent-ruby" to your Gemfile.
  MESSAGE
  raise
end

#rewrite_unavailable_ipv6_binds! (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 401

def rewrite_unavailable_ipv6_binds!
  return if self.class.ipv6_interface_available?

  tried_ipv6_bind = false
  bind_schemes = ['tcp', 'ssl']

  @_options[:binds] = Array(@_options[:binds]).map do |bind|
    uri = URI.parse(bind)
    next bind unless bind_schemes.include?(uri.scheme)

    host = uri.host&.delete_prefix('[')&.delete_suffix(']')
    next bind unless host&.include?(':')

    tried_ipv6_bind = true
    next bind unless host == Const::UNSPECIFIED_IPV6

    uri.host = Const::UNSPECIFIED_IPV4
    uri.to_s
  rescue URI::InvalidURIError
    bind
  end

  warn "WARNING: IPv6 bind requested but no non-loopback IPv6 interface was detected" if tried_ipv6_bind
end

#run_hooks(key, arg, log_writer, hook_data = nil)

Parameters:

  • key (:Symbol)

    hook to run

  • arg (Launcher, Int)

    :before_restart passes Launcher

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 346

def run_hooks(key, arg, log_writer, hook_data = nil)
  log_writer.debug { "Running #{key} hooks" }

  options.all_of(key).each do |hook_options|
    begin
      block = hook_options[:block]
      if id = hook_options[:id]
        hook_data[id] ||= Hash.new
        block.call arg, hook_data[id]
      else
        block.call arg
      end
    rescue => e
      log_writer.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
      log_writer.debug e.backtrace.join("\n")
    end
  end
end

#run_mode_hooks (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 487

def run_mode_hooks
  workers_before = @_options[:workers]
  key = workers_before > 0 ? :cluster : :single

  @_options.all_of(key).each(&:call)

  unless @_options[:workers] == workers_before
    raise "cannot change the number of workers inside a #{key} configuration hook"
  end
end

#set_conditional_default_options (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 498

def set_conditional_default_options
  @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
    (@_options[:workers] > 1) && Puma.forkable?
end

#warn_hooks (private)

[ GitHub ]

  
# File 'lib/puma/configuration.rb', line 503

def warn_hooks
  return if options[:workers] > 0
  return if options[:silence_fork_callback_warning]

  log_writer = LogWriter.stdio
  @hooks.each_key do |hook|
    options.all_of(hook).each do |hook_options|
      next unless hook_options[:cluster_only]

      log_writer.log(<<~MSG.tr("\n", " "))
        Warning: The code in the `#{hook}` block will not execute
        in the current Puma configuration. The `#{hook}` block only
        executes in Puma's cluster mode. To fix this, either remove the
        `#{hook}` call or increase Puma's worker count above zero.
      MSG
    end
  end
end