Skip to content

Commit

Permalink
Implement optimistic resource quota utilization
Browse files Browse the repository at this point in the history
Instead of re-computing the Resource Quota utilization by iterating all
Resource Quota hosts and determining their resources, an optimistic tracking
of the utilized resources is implemented.

When a host is created/added to a Resource Quota, its resources are
determined once and added to the Resource Quota utilization. When a
host is destroyed/unassigned from a Resource Quota, its utilization is
freed from the host's resource capacities.
  • Loading branch information
bastian-src committed May 29, 2024
1 parent 3f62d33 commit f84f23e
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 106 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Metrics/ClassLength:
Exclude:
- 'app/models/foreman_resource_quota/resource_quota.rb'

Metrics/ModuleLength:
Exclude:
- 'app/models/concerns/foreman_resource_quota/host_managed_extensions.rb'

Metrics/MethodLength:
Enabled: false

Expand Down
132 changes: 108 additions & 24 deletions app/models/concerns/foreman_resource_quota/host_managed_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ module HostManagedExtensions
include ForemanResourceQuota::Exceptions

included do
validate :check_resource_quota_capacity
validate :verify_resource_quota_on_create, if: -> { new_record? }
validate :verify_resource_quota_on_change, if: -> { resource_quota_id_change_to_be_saved && !new_record? }
after_create -> { add_host_capacity_to_quota(resource_quota) }
before_destroy -> { remove_host_capacity_from_quota(resource_quota) }

belongs_to :resource_quota, class_name: '::ForemanResourceQuota::ResourceQuota'
has_one :resource_quota_missing_resources, class_name: '::ForemanResourceQuota::ResourceQuotaMissingHost',
inverse_of: :missing_host, foreign_key: :missing_host_id, dependent: :destroy
scoped_search relation: :resource_quota, on: :name, complete_value: true, rename: :resource_quota
end

def check_resource_quota_capacity
handle_quota_check
def verify_resource_quota_on_create
handle_quota_check(resource_quota)
true
rescue ResourceQuotaException => e
handle_error('resource_quota_id',
Expand All @@ -32,13 +35,38 @@ def check_resource_quota_capacity
format('An unknown error occured while checking the resource quota capacity: %s', e))
end

# rubocop:disable Metrics/AbcSize
def verify_resource_quota_on_change
from_quota_id, to_quota_id = resource_quota_id_change_to_be_saved
from_quota = ResourceQuota.find_by(id: from_quota_id)
to_quota = ResourceQuota.find_by(id: to_quota_id)

remove_host_capacity_from_quota(from_quota) unless from_quota.nil?
handle_quota_check(to_quota) unless to_quota.nil?
add_host_capacity_to_quota(to_quota) unless to_quota.nil?
true
rescue ResourceQuotaException => e
handle_error('resource_quota_id',
e.bare_message,
format('An error occured while checking the resource quota capacity: %s', e))
rescue Foreman::Exception => e
handle_error(:base,
e.bare_message,
format('An unexpected Foreman error occured while checking the resource quota capacity: %s', e))
rescue StandardError => e
handle_error(:base,
e.message,
format('An unknown error occured while checking the resource quota capacity: %s', e))
end
# rubocop:enable Metrics/AbcSize

private

def handle_quota_check
return if early_return?
quota_utilization = determine_quota_utilization
host_resources = determine_host_resources
verify_resource_quota_limits(quota_utilization, host_resources)
def handle_quota_check(quota)
return if early_return?(quota)
quota_utilization = determine_quota_utilization(quota)
host_resources = determine_host_resources(quota.active_resources)
verify_resource_quota_limits(quota, quota_utilization, host_resources)
end

def handle_error(error_module, error_message, log_message)
Expand All @@ -47,60 +75,71 @@ def handle_error(error_module, error_message, log_message)
false
end

def determine_quota_utilization
resource_quota.determine_utilization
missing_hosts = resource_quota.missing_hosts
def determine_quota_utilization(quota)
missing_hosts = quota.missing_hosts
unless missing_hosts.empty?
raise ResourceQuotaUtilizationException,
"Resource Quota '#{resource_quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
"Resource Quota '#{quota.name}' cannot determine resources for #{missing_hosts.size} hosts."
end
resource_quota.utilization
quota.utilization
end

def determine_host_resources
(host_resources, missing_hosts) = call_utilization_helper(resource_quota.active_resources, [self])
def determine_host_resources(active_resources)
(host_resources, missing_hosts) = call_utilization_helper(active_resources, [self])
unless missing_hosts.empty?
raise HostResourcesException,
"Cannot determine host resources for #{name}"
end
host_resources
end

def verify_resource_quota_limits(quota_utilization, host_resources)
def verify_resource_quota_limits(quota, quota_utilization, host_resources)
quota_utilization.each do |resource_type, resource_utilization|
next if resource_utilization.nil?

max_quota = resource_quota[resource_type]
max_quota = quota[resource_type]
all_hosts_utilization = resource_utilization + host_resources[resource_type.to_sym]
next if all_hosts_utilization <= max_quota

raise ResourceLimitException, formulate_limit_error(resource_utilization,
raise ResourceLimitException, formulate_limit_error(quota.name, resource_utilization,
all_hosts_utilization, max_quota, resource_type)
end
end

def formulate_limit_error(resource_utilization, all_hosts_utilization, max_quota, resource_type)
def formulate_limit_error(quota_name, resource_utilization, all_hosts_utilization, max_quota, resource_type)
if resource_utilization < max_quota
N_(format("Host exceeds %s limit of '%s'-quota by %s (max. %s)",
natural_resource_name_by_type(resource_type),
resource_quota.name,
quota_name,
resource_value_to_string(all_hosts_utilization - max_quota, resource_type),
resource_value_to_string(max_quota, resource_type)))
else
N_(format("%s limit of '%s'-quota is already exceeded by %s without adding the new host (max. %s)",
natural_resource_name_by_type(resource_type),
resource_quota.name,
quota_name,
resource_value_to_string(resource_utilization - max_quota, resource_type),
resource_value_to_string(max_quota, resource_type)))
end
end

def early_return?
if resource_quota.nil?
def formulate_resource_inconsistency_error(quota_name, resource_type, quota_utilization_value, resource_value)
N_("Resource Quota '#{quota_name}' inconsistency detected while destroying host '#{name}':\n" \
"Resource Quota #{resource_type} current utilization: #{quota_utilization_value}.\n" \
"Host resource value: #{resource_value}.\n" \
'Skipping.')
end

def formulate_quota_inconsistency_error(quota_name)
N_("An error occured adapting the resource quota utilization of '#{quota_name}' " \
"while processing host '#{name}'. The resource quota utilization values might be inconsistent.")
end

def early_return?(quota)
if quota.nil?
return true if quota_assigment_optional?
raise HostResourceQuotaEmptyException, 'must be given.'
end
return true if resource_quota.active_resources.empty?
return true if quota.active_resources.empty?
return true if Setting[:resource_quota_global_no_action] # quota is assigned, but not supposed to be checked
false
end
Expand All @@ -113,5 +152,50 @@ def quota_assigment_optional?
def call_utilization_helper(resources, hosts)
utilization_from_resource_origins(resources, hosts)
end

def add_host_capacity_to_quota(quota)
return if quota.nil?

update_quota_with_host_resources(quota) do |quota_resource_utilization, resource_value, _|
quota_resource_utilization + resource_value
end
end

def remove_host_capacity_from_quota(quota)
return if quota.nil?

update_quota_with_host_resources(quota) do |quota_resource_utilization, resource_value, resource_type|
quota_resource_utilization - helper_resource_value_subtraction(
quota.name,
resource_value,
quota_resource_utilization,
resource_type
)
end
end

def update_quota_with_host_resources(quota)
host_resources = determine_host_resources(quota.active_resources)
new_utilization = quota.utilization

host_resources.each do |resource_type, resource_value|
new_utilization[resource_type] ||= 0
new_utilization[resource_type] = yield(new_utilization[resource_type], resource_value, resource_type)
end

quota.utilization = new_utilization
rescue ResourceQuotaException => e
Rails.logger.warn("#{formulate_quota_inconsistency_error(quota.name)}\n#{e.bare_message}")
end

def helper_resource_value_subtraction(quota_name, host_resource_value, quota_value, resource_type)
return host_resource_value if quota_value >= host_resource_value

# Log inconsistency warning and don't subtract anything from quota utilization value
Rails.logger.warn(formulate_quota_inconsistency_error(quota_name, resource_type,
new_utilization[resource_type],
host_resource_value))
0
end
end
end
Loading

0 comments on commit f84f23e

Please sign in to comment.