ManageIQ/manageiq

View on GitHub
app/models/mixins/miq_web_server_worker_mixin.rb

Summary

Maintainability
A
0 mins
Test Coverage
F
55%
class NoFreePortError < StandardError; end

module MiqWebServerWorkerMixin
  extend ActiveSupport::Concern

  BINDING_ADDRESS = ENV['BINDING_ADDRESS'] || (Rails.env.production? ? "127.0.0.1" : "0.0.0.0")

  included do
    class << self
      attr_accessor :registered_ports
    end

    try(:maximum_workers_count=, 10)
  end

  module ClassMethods
    def binding_address
      BINDING_ADDRESS
    end

    def preload_for_console
      configure_secret_token(SecureRandom.hex(64))
    end

    def preload_for_worker_role
      raise "Expected database to be seeded via `rake db:seed`." unless EvmDatabase.seeded_primordially?

      configure_secret_token
    end

    def configure_secret_token(token = MiqDatabase.first.session_secret_token)
      return if Rails.application.config.secret_key_base

      Rails.application.config.secret_key_base = token

      # To set a secret token after the Rails.application is initialized,
      # we need to reset the secrets since they are cached:
      # https://github.com/rails/rails/blob/7-0-stable/railties/lib/rails/application.rb#L392-L404
      Rails.application.secrets = nil if Rails.version < "7.1"
    end

    def rails_server
      "puma"
    end

    def all_ports_in_use
      server_scope.select(&:enabled_or_running?).collect(&:port)
    end

    def build_uri(port)
      URI::HTTP.build(:host => binding_address, :port => port).to_s
    end

    def sync_workers
      # TODO: add an at_exit to remove all registered ports and gracefully stop apache
      self.registered_ports ||= []

      workers = find_current_or_starting
      current = workers.length
      desired = self.workers
      result  = {:adds => [], :deletes => []}
      ports = all_ports_in_use

      # TODO: This tracking of adds/deletes of pids and ports is not DRY
      ports_hash = {:deletes => [], :adds => []}

      if current != desired
        _log.info("Workers are being synchronized: Current #: [#{current}], Desired #: [#{desired}]")

        if desired > current && enough_resource_to_start_worker?
          (desired - current).times do
            port = reserve_port(ports)
            _log.info("Reserved port=#{port}, Current ports in use: #{ports.inspect}")
            ports << port
            ports_hash[:adds] << port
            w = start_worker(:uri => build_uri(port))
            result[:adds] << w.pid
          end
        elsif desired < current
          workers = workers.to_a
          (current - desired).times do
            w = workers.pop
            port = w.port
            ports.delete(port)
            ports_hash[:deletes] << port

            _log.info("Unreserved port=#{port}, Current ports in use: #{ports.inspect}")
            result[:deletes] << w.pid
            w.stop
          end
        end
      end

      result
    end

    def pid_file(port)
      Rails.root.join("tmp/pids/rails_server.#{port}.pid")
    end

    def port_range
      self::STARTING_PORT...(self::STARTING_PORT + maximum_workers_count)
    end

    def reserve_port(ports)
      free_ports = port_range.to_a - ports
      raise NoFreePortError if free_ports.empty?

      free_ports.first
    end
  end

  def pid_file
    @pid_file ||= self.class.pid_file(port)
  end

  def rails_server_options
    # See Rack::Server options which is what Rails::Server uses:
    # https://github.com/rack/rack/blob/1.6.4/lib/rack/server.rb#L152-L183
    params = {
      :Host        => self.class.binding_address,
      :environment => Rails.env.to_s,
      :app         => rails_application,
      :server      => self.class.rails_server
    }

    params[:Port] = port.kind_of?(Numeric) ? port : ENV["PORT"] || 3000
    params[:pid]  = self.class.pid_file(params[:Port]).to_s

    params
  end

  def rails_application
    @app ||= defined?(self.class::RACK_APPLICATION) ? self.class::RACK_APPLICATION.new : Rails.application
  end

  def start
    delete_pid_file
    super
  end

  def kill
    deleted_worker = super
    delete_pid_file
    deleted_worker
  end

  def delete_pid_file
    File.delete(pid_file) if File.exist?(pid_file)
  end

  def port
    @port ||= uri.blank? ? nil : URI.parse(uri).port
  end

  def release_db_connection
    update_spid!(nil)
    self.class.release_db_connection
  end
end