<commit_before>require 'atmosphere/utils'

module Atmosphere
  module OptimizationStrategy
    class Default
      def initialize(appliance)
        @appliance = appliance
      end

      def self.select_tmpls_and_flavors_and_tenants(tmpls, appliance, options)
        VmAndFlavorAndTenant.new(tmpls, appliance, options).select
      end

      def can_reuse_vm?
        !appliance.development? && appliance.appliance_type.shared
      end

      def vm_to_reuse
        VirtualMachine.reusable_by(appliance).select { |vm| reuse?(vm) }.first
      end

      def new_vms_tmpls_and_flavors_and_tenants
        tmpls = vmt_candidates_for(appliance)
        return [{template: nil, flavor: nil, tenant: nil}] if tmpls.blank?

        Default.select_tmpls_and_flavors_and_tenants(tmpls, appliance, preferences)
      end

      def can_scale_manually?
        false
      end

      protected

      def vmt_candidates_for(appliance)
        vmts = VirtualMachineTemplate.where(
          appliance_type: appliance.appliance_type,
          state: 'active'
        )
        if appliance.tenants.present?
          vmts = restrict_by_user_requirements(vmts, appliance)
        end
        vmts = restrict_by_tenant_availability(vmts, appliance)
        vmts
      end

      private

      attr_reader :appliance

      def restrict_by_user_requirements(vmts, appliance)
        # If the user requests that the appliance be bound to a specific set of tenants,
        # the optimizer should honor this selection.

        # Workaround
        funded_active_tenants = appliance.tenants.funded_by(appliance.fund).reject{|t| t.active == false}

        vmts.select do |vmt|
          (vmt.tenants & funded_active_tenants).present?
        end
      end

      def restrict_by_tenant_availability(vmts, appliance)
        # In all cases the optimizer should only suggest those vmts which the user is able to access
        # (i.e. vmts which reside on at least one tenant which shares a fund with the appliance).
        vmts.select do |vmt|
          (vmt.tenants & appliance.fund.tenants).present?
        end
      end

      def reuse?(vm)
        vm.appliances.count < Atmosphere.optimizer.max_appl_no &&
        !vm.appliances.first.development?
      end

      def preferences
        props = appliance.dev_mode_property_set
        if props
          {
            cpu: props.preference_cpu,
            memory: props. preference_memory,
            hdd: props.preference_disk
          }
        else
          {}
        end
      end

      class VmAndFlavorAndTenant
        include Atmosphere::Utils

        def initialize(tmpls, appliance, options={})
          @tmpls = tmpls
          @appliance = appliance
          @options = options
        end

        def select
          # We are given a list of tmpls which MAY be spawned by the user in the context of @appliance.
          # We are asked to return a single template, a specific flavor and a specific tenant for instantiation.
          # Flavor prices may vary by tenant.
          # !CAUTION! @appliance can be nil -- this will happen when the method is invoked prior to
          # creation of an appliance.
          best_template = nil
          best_tenant = nil
          best_flavor = nil
          instantiation_cost = Float::INFINITY

          tmpls.each do |tmpl|
            # Assuming tmpl is selected, figure out which tenant and which flavor to use.
            candidate_tenants = get_candidate_tenants_for_template(tmpl)
            # For each candidate tenant find optimal flavor and the corresponding VM instantiation cost
            # Note that candidate_tenants can be blank - we must handle this case.
            opt_tenant = nil
            opt_flavor_and_cost = [nil, Float::INFINITY]
            candidate_tenants.each do |t|
              opt_flavor, cost = get_optimal_flavor_for_tenant(tmpl, t)
              if cost < opt_flavor_and_cost[1]
                opt_tenant = t
                opt_flavor_and_cost = [opt_flavor, cost]
              end
            end
            # An optimal tenant may not have been found - in that case skip tmpl entirely.
            # Otherwise check if the tenant that has been found is better than the current "leader"
            if opt_tenant.present?
              if opt_flavor_and_cost[1] < instantiation_cost
                best_template = tmpl
                best_tenant = opt_tenant
                best_flavor = opt_flavor_and_cost[0]
                instantiation_cost = opt_flavor_and_cost[1]
              end
            end
          end

          # It is possible that nothing has been found - this would indicate unsatisfiable hardware requirements
          # (i.e. no flavor with sufficient CPU/memory/HDD) or non-existence of flavors supporting the selected
          # architecture.
          # If this occurs, return first template to avoid breaking user notification logic
          # in Atmosphere::Cloud::SatisfyAppliance
          # TODO: This is quite hacky; a better way to communicate errors to the user is needed.
          if best_template.blank?
            best_template = tmpls.first
          end

          [
            {
              template: best_template,
              tenant: best_tenant,
              flavor: best_flavor
            }
          ]
        end

        private

        attr_reader :tmpls, :options

        def get_candidate_tenants_for_template(tmpl)
          # Determine which tenants can be used to spawn this specific template in the context
          # of the current @appliance.
          # If @appliance is nil (which may happen) then return all of this tmpl's tenants:
          if @appliance.blank?
            # This will happen in pre-instantiation queries (such as in the CLEW
            # "Start application" window) where no appliance is yet present.
            tmpl.tenants
          else
            if @appliance.tenants.present?
              # If appliance is manually restricted to a specific subset of tenants,
              # return intersection of template tenants and appliance tenants.
              # The second intersection (with user tenants) is for safety -
              # we must never spawn a VM in a tenant which the given user has no access to.
              tmpl.tenants & @appliance.tenants & @appliance.appliance_set.user.tenants
            else
              # Otherwise simply return intersection of template tenants and user tenants.
              tmpl.tenants & @appliance.appliance_set.user.tenants
            end
          end<commit_msg>Extend optimizer logic to guard against anomalous conditions.<commit_after>require 'atmosphere/utils'

module Atmosphere
  module OptimizationStrategy
    class Default
      def initialize(appliance)
        @appliance = appliance
      end

      def self.select_tmpls_and_flavors_and_tenants(tmpls, appliance, options)
        VmAndFlavorAndTenant.new(tmpls, appliance, options).select
      end

      def can_reuse_vm?
        !appliance.development? && appliance.appliance_type.shared
      end

      def vm_to_reuse
        VirtualMachine.reusable_by(appliance).select { |vm| reuse?(vm) }.first
      end

      def new_vms_tmpls_and_flavors_and_tenants
        tmpls = vmt_candidates_for(appliance)
        return [{template: nil, flavor: nil, tenant: nil}] if tmpls.blank?

        Default.select_tmpls_and_flavors_and_tenants(tmpls, appliance, preferences)
      end

      def can_scale_manually?
        false
      end

      protected

      def vmt_candidates_for(appliance)
        vmts = VirtualMachineTemplate.where(
          appliance_type: appliance.appliance_type,
          state: 'active'
        )
        if appliance.tenants.present?
          vmts = restrict_by_user_requirements(vmts, appliance)
        end
        vmts = restrict_by_tenant_availability(vmts, appliance)
        vmts
      end

      private

      attr_reader :appliance

      def restrict_by_user_requirements(vmts, appliance)
        # If the user requests that the appliance be bound to a specific set of tenants,
        # the optimizer should honor this selection.

        # Workaround
        funded_active_tenants = appliance.tenants.funded_by(appliance.fund).reject{|t| t.active == false}

        vmts.select do |vmt|
          (vmt.tenants & funded_active_tenants).present?
        end
      end

      def restrict_by_tenant_availability(vmts, appliance)
        # In all cases the optimizer should only suggest those vmts which the user is able to access
        # (i.e. vmts which reside on at least one tenant which shares a fund with the appliance).
        vmts.select do |vmt|
          (vmt.tenants & appliance.fund.tenants).present?
        end
      end

      def reuse?(vm)
        vm.appliances.count < Atmosphere.optimizer.max_appl_no &&
        !vm.appliances.first.development?
      end

      def preferences
        props = appliance.dev_mode_property_set
        if props
          {
            cpu: props.preference_cpu,
            memory: props. preference_memory,
            hdd: props.preference_disk
          }
        else
          {}
        end
      end

      class VmAndFlavorAndTenant
        include Atmosphere::Utils

        def initialize(tmpls, appliance, options={})
          @tmpls = tmpls
          @appliance = appliance
          @options = options
        end

        def select
          # We are given a list of tmpls which MAY be spawned by the user in the context of @appliance.
          # We are asked to return a single template, a specific flavor and a specific tenant for instantiation.
          # Flavor prices may vary by tenant.
          # !CAUTION! @appliance can be nil -- this will happen when the method is invoked prior to
          # creation of an appliance.
          best_template = nil
          best_tenant = nil
          best_flavor = nil
          instantiation_cost = Float::INFINITY

          tmpls.each do |tmpl|
            # Assuming tmpl is selected, figure out which tenant and which flavor to use.
            candidate_tenants = get_candidate_tenants_for_template(tmpl)
            # For each candidate tenant find optimal flavor and the corresponding VM instantiation cost
            # Note that candidate_tenants can be blank - we must handle this case.
            opt_tenant = nil
            opt_flavor_and_cost = [nil, Float::INFINITY]
            candidate_tenants.each do |t|
              opt_flavor, cost = get_optimal_flavor_for_tenant(tmpl, t)
              if cost < opt_flavor_and_cost[1]
                opt_tenant = t
                opt_flavor_and_cost = [opt_flavor, cost]
              end
            end
            # An optimal tenant may not have been found - in that case skip tmpl entirely.
            # Otherwise check if the tenant that has been found is better than the current "leader"
            if opt_tenant.present?
              if opt_flavor_and_cost[1] < instantiation_cost
                best_template = tmpl
                best_tenant = opt_tenant
                best_flavor = opt_flavor_and_cost[0]
                instantiation_cost = opt_flavor_and_cost[1]
              end
            end
          end

          # It is possible that nothing has been found - this would indicate unsatisfiable hardware requirements
          # (i.e. no flavor with sufficient CPU/memory/HDD) or non-existence of flavors supporting the selected
          # architecture.
          # If this occurs, return first template to avoid breaking user notification logic
          # in Atmosphere::Cloud::SatisfyAppliance
          # TODO: This is quite hacky; a better way to communicate errors to the user is needed.
          if best_template.blank?
            best_template = tmpls.first
          end

          [
            {
              template: best_template,
              tenant: best_tenant,
              flavor: best_flavor
            }
          ]
        end

        private

        attr_reader :tmpls, :options

        def get_candidate_tenants_for_template(tmpl)
          # Determine which tenants can be used to spawn this specific template in the context
          # of the current @appliance.
          # If @appliance is nil (which may happen) then return all of this tmpl's tenants:
          if @appliance.blank?
            # This will happen in pre-instantiation queries (such as in the CLEW
            # "Start application" window) where no appliance is yet present.
            tmpl.tenants
          else
            if @appliance.tenants.present?
              # If appliance is manually restricted to a specific subset of tenants,
              # return intersection of template tenants and appliance tenants.
              # The second and third intersections (with appliance fund tenants and user tenants)
              # are for safety - we must never spawn a VM in a tenant which is not linked
              # to appliance.fund or cannot be accessed by the current user.
              tmpl.tenants &
                @appliance.tenants &
                @appliance.fund.tenants &
                @appliance.appliance_set.user.tenants
            else
              # Otherwise simply return intersection of template tenants, appliance fund tenants
              # and user tenants.
              tmpl.tenants &
                @appliance.fund.tenants &
                @appliance.appliance_set.user.tenants
            end
          end
{'fraction_non_alphanumeric': 0.05314654213920391, 'fraction_numerical': 0.0005929879178711734, 'mean_word_length': 1.964623159745111, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4226963', 'n_tokens_mistral': 3720, 'n_tokens_neox': 3519, 'n_words': 1164}