Skip to content

Commit

Permalink
Merge pull request #11648 from lpichler/new_chargeback_calculations
Browse files Browse the repository at this point in the history
New chargeback calculations
(cherry picked from commit 63996cc)

https://bugzilla.redhat.com/show_bug.cgi?id=1346047
  • Loading branch information
gtanzillo authored and chessbyte committed Oct 21, 2016
1 parent 3cfd800 commit d1b8be2
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 120 deletions.
109 changes: 76 additions & 33 deletions app/models/chargeback.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class Chargeback < ActsAsArModel
HOURS_IN_DAY = 24
HOURS_IN_WEEK = 168

VIRTUAL_COL_USES = {
"v_derived_cpu_total_cores_used" => "cpu_usage_rate_average"
}
Expand Down Expand Up @@ -29,41 +32,74 @@ def self.build_results_for_report_chargeback(options)
rate_cols = ChargebackRate.where(:default => true).flat_map do |rate|
rate.chargeback_rate_details.map(&:metric).select { |metric| perf_cols.include?(metric.to_s) }
end

rate_cols.map! { |x| VIRTUAL_COL_USES.include?(x) ? VIRTUAL_COL_USES[x] : x }.flatten!
base_rollup = base_rollup.select(*rate_cols)

timerange = get_report_time_range(options, interval, tz)
data = {}

timerange.step_value(1.day).each_cons(2) do |query_start_time, query_end_time|
recs = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => "hourly")
recs = where_clause(recs, options)
recs = Metric::Helper.remove_duplicate_timestamps(recs)
_log.info("Found #{recs.length} records for time range #{[query_start_time, query_end_time].inspect}")

unless recs.empty?
recs.each do |perf|
next if perf.resource.nil?
key, extra_fields = key_and_fields(perf, interval, tz)
data[key] ||= extra_fields

if perf.chargeback_fields_present?
data[key]['fixed_compute_metric'] ||= 0
data[key]['fixed_compute_metric'] = data[key]['fixed_compute_metric'] + 1
end

rates_to_apply = cb.get_rates(perf)
chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description)
data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ')
calculate_costs(perf, data[key], rates_to_apply)
end
interval_duration = interval_to_duration(interval)

timerange.step_value(interval_duration).each_cons(2) do |query_start_time, query_end_time|
records = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => "hourly")
records = where_clause(records, options)
records = Metric::Helper.remove_duplicate_timestamps(records)
next if records.empty?
_log.info("Found #{records.length} records for time range #{[query_start_time, query_end_time].inspect}")

hours_in_interval = hours_in_interval(query_start_time, query_end_time, interval)

# we are building hash with grouped calculated values
# values are grouped by resource_id and timestamp (query_start_time...query_end_time)
records.group_by(&:resource_id).each do |_, metric_rollup_records|
metric_rollup_records = metric_rollup_records.select { |x| x.resource.present? }
next if metric_rollup_records.empty?

# we need to select ChargebackRates for groups of MetricRollups records
# and rates are selected by first MetricRollup record
metric_rollup_record = metric_rollup_records.first
rates_to_apply = cb.get_rates(metric_rollup_record)

# key contains resource_id and timestamp (query_start_time...query_end_time)
# extra_fields there some extra field like resource name and
# some of them are related to specific chargeback (ChargebackVm, ChargebackContainer,...)
key, extra_fields = key_and_fields(metric_rollup_record, interval, tz)
data[key] ||= extra_fields

chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description)
data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ')

# we are getting hash with metrics and costs for metrics defined for chargeback
metrics_and_costs = calculate_costs(metric_rollup_records, rates_to_apply, hours_in_interval)

data[key].merge!(metrics_and_costs)
end
end

_log.info("Calculating chargeback costs...Complete")

[data.map { |r| new(r.last) }]
end

def self.hours_in_interval(query_start_time, query_end_time, interval)
return HOURS_IN_DAY if interval == "daily"
return HOURS_IN_WEEK if interval == "weekly"

(query_end_time - query_start_time) / 1.hour
end

def self.interval_to_duration(interval)
case interval
when "daily"
1.day
when "weekly"
1.week
when "monthly"
1.month
end
end

def self.key_and_fields(metric_rollup_record, interval, tz)
ts_key = get_group_key_ts(metric_rollup_record, interval, tz)

Expand Down Expand Up @@ -110,25 +146,32 @@ def get_rates(perf)
end
end

def self.calculate_costs(perf, h, rates)
# This expects perf interval to be hourly. That will be the most granular interval available for chargeback.
unless perf.capture_interval_name == "hourly"
raise _("expected 'hourly' performance interval but got '%{interval}") % {:interval => perf.capture_interval_name}
end
def self.calculate_costs(metric_rollup_records, rates, hours_in_interval)
calculated_costs = {}

chargeback_fields_present = metric_rollup_records.count(&:chargeback_fields_present?)
calculated_costs['fixed_compute_metric'] = chargeback_fields_present if chargeback_fields_present

rates.each do |rate|
rate.chargeback_rate_details.each do |r|
rec = r.metric && perf.respond_to?(r.metric) ? perf : perf.resource
metric = r.metric.nil? ? 0 : rec.send(r.metric) || 0
cost = r.group == 'fixed' && !perf.chargeback_fields_present? ? 0 : r.cost(metric)
if !chargeback_fields_present && r.fixed?
cost = 0
else
metric_value = r.metric_value_by(metric_rollup_records)
r.hours_in_interval = hours_in_interval
cost = r.cost(metric_value) * hours_in_interval
end

reportable_metric_and_cost_fields(r.rate_name, r.group, metric, cost).each do |k, val|
# add values to hash and sum
reportable_metric_and_cost_fields(r.rate_name, r.group, metric_value, cost).each do |k, val|
next unless attribute_names.include?(k)
h[k] ||= 0
h[k] += val
calculated_costs[k] ||= 0
calculated_costs[k] += val
end
end
end

calculated_costs
end

def self.reportable_metric_and_cost_fields(rate_name, rate_group, metric, cost)
Expand Down
47 changes: 43 additions & 4 deletions app/models/chargeback_rate_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,39 @@ class ChargebackRateDetail < ApplicationRecord

FORM_ATTRIBUTES = %i(description per_time per_unit metric group source metric).freeze

attr_accessor :hours_in_interval

def max_of_metric_from(metric_rollup_records)
metric_rollup_records.map(&metric.to_sym).max
end

def avg_of_metric_from(metric_rollup_records)
record_count = metric_rollup_records.count
metric_sum = metric_rollup_records.sum(&metric.to_sym)
metric_sum / record_count
end

def metric_value_by(metric_rollup_records)
return 1.0 if fixed?

metric_rollups_without_nils = metric_rollup_records.select { |x| x.send(metric.to_sym).present? }
return 0 if metric_rollups_without_nils.empty?
return max_of_metric_from(metric_rollups_without_nils) if allocated?
return avg_of_metric_from(metric_rollups_without_nils) if used?
end

def used?
source == "used"
end

def allocated?
source == "allocated"
end

def fixed?
group == "fixed"
end

# Set the rates according to the tiers
def find_rate(value)
fixed_rate = 0.0
Expand All @@ -36,22 +69,28 @@ def find_rate(value)

def cost(value)
return 0.0 unless self.enabled?
value = 1 if group == 'fixed'

value = 1.0 if fixed?

(fixed_rate, variable_rate) = find_rate(value)
hourly(fixed_rate) + hourly(variable_rate) * value

hourly_fixed_rate = hourly(fixed_rate)
hourly_variable_rate = hourly(variable_rate)

hourly_fixed_rate + rate_adjustment(hourly_variable_rate) * value
end

def hourly(rate)
hourly_rate = case per_time
when "hourly" then rate
when "daily" then rate / 24
when "weekly" then rate / 24 / 7
when "monthly" then rate / 24 / 30
when "monthly" then rate / @hours_in_interval
when "yearly" then rate / 24 / 365
else raise "rate time unit of '#{per_time}' not supported"
end

rate_adjustment(hourly_rate)
hourly_rate
end

# Scale the rate in the unit difine by user to the default unit of the metric
Expand Down
9 changes: 6 additions & 3 deletions spec/models/chargeback_container_image_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
end

context "Daily" do
let(:hours_in_day) { 24 }

before do
@options[:interval] = "daily"
@options[:entity_id] = @project.id
Expand All @@ -64,7 +66,6 @@
:timestamp => t,
:image_tag_names => "environment/prod")
end
@metric_size = @container.metric_rollups.size
end

subject { ChargebackContainerImage.build_results_for_report_ChargebackContainerImage(@options).first.first }
Expand All @@ -83,7 +84,7 @@
:chargeback_tiers => [cbt])
}
it "fixed_compute" do
expect(subject.fixed_compute_1_cost).to eq(@hourly_rate * @metric_size)
expect(subject.fixed_compute_1_cost).to eq(@hourly_rate * hours_in_day)
end
end

Expand All @@ -98,6 +99,8 @@
time = ts.beginning_of_month.utc
end_time = ts.end_of_month.utc

@hours_in_month = Time.days_in_month(time.month, time.year) * 24

while time < end_time
@container.metric_rollups << FactoryGirl.create(:metric_rollup_vm_hr,
:timestamp => time,
Expand Down Expand Up @@ -135,7 +138,7 @@
}
it "fixed_compute" do
# .to be_within(0.01) is used since theres a float error here
expect(subject.fixed_compute_1_cost).to be_within(0.01).of(@hourly_rate * @metric_size)
expect(subject.fixed_compute_1_cost).to be_within(0.01).of(@hourly_rate * @hours_in_month)
end
end
end
Loading

0 comments on commit d1b8be2

Please sign in to comment.