ManageIQ/manageiq

View on GitHub
app/models/time_profile.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
class TimeProfile < ApplicationRecord
  ALL_DAYS  = (0...7).to_a.freeze
  ALL_HOURS = (0...24).to_a.freeze
  DEFAULT_TZ = "UTC".freeze

  serialize :profile
  default_value_for :days,  ALL_DAYS
  default_value_for :hours, ALL_HOURS

  has_many  :miq_reports
  has_many  :metric_rollups

  scope :rollup_daily_metrics, -> { where(:rollup_daily_metrics => true) }

  after_create :rebuild_daily_metrics_on_create
  after_save   :rebuild_daily_metrics_on_save

  def self.find_all_with_entire_tz
    all.select(&:entire_tz?)
  end

  def self.all_timezones
    select(%w[id profile]).collect(&:tz).uniq
  end

  def self.seed
    default_time_profile || create!(
      :description          => DEFAULT_TZ,
      :tz                   => DEFAULT_TZ,
      :profile_type         => "global",
      :rollup_daily_metrics => true
    ) do |_|
      _log.info("Creating global time profile")
    end
  end

  def global?
    profile_type == "global"
  end

  def ts_in_profile?(ts, default_tz = DEFAULT_TZ)
    ts = Time.parse(ts) if ts.kind_of?(String)
    ts_day_in_profile?(ts, default_tz) && ts_hour_in_profile?(ts, default_tz)
  end

  def ts_hour_in_profile?(ts, default_tz = DEFAULT_TZ)
    ts = Time.parse(ts) if ts.kind_of?(String)
    hours.include?(ts.in_time_zone(tz_or_default(default_tz)).hour)
  end

  def ts_day_in_profile?(ts, default_tz = DEFAULT_TZ)
    ts = Time.parse(ts) if ts.kind_of?(String)
    days.include?(ts.in_time_zone(tz_or_default(default_tz)).wday)
  end

  def profile
    super || (self.profile = {})
  end

  def profile=(value)
    super(value&.symbolize_keys)
  end

  def tz
    profile[:tz]
  end

  def tz=(val)
    profile_will_change! if profile[:tz] != val
    profile[:tz] = val
  end

  def tz_or_default(default_tz = DEFAULT_TZ)
    tz || default_tz
  end

  def days
    profile[:days]
  end

  def days=(arr)
    arr = arr.collect(&:to_i)
    profile_will_change! if profile[:days] != arr
    profile[:days] = arr
  end

  def hours
    profile[:hours]
  end

  def hours=(arr)
    arr = arr.collect(&:to_i)
    profile_will_change! if profile[:hours] != arr
    profile[:hours] = arr
  end

  def entire_tz?
    days&.sort == ALL_DAYS && hours&.sort == ALL_HOURS
  end

  def default?
    entire_tz? && tz_or_default == DEFAULT_TZ
  end

  def rebuild_daily_metrics
    oldest_hourly = MetricRollup.select(:timestamp).where(:capture_interval_name => "hourly").order(:timestamp).first
    destroy_metric_rollups
    return if oldest_hourly.nil?

    start_time = [oldest_hourly.timestamp, Metric::Purging.purge_date(:keep_hourly_metrics) || 6.months.ago.utc].max
    end_time   = Time.now.utc
    Metric::Rollup.perf_rollup_gap_queue(start_time, end_time, 'daily', id)
  end

  def rebuild_daily_metrics_queue
    MiqQueue.put(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => 'rebuild_daily_metrics',
      :msg_timeout => 1.hour
    )
  end

  def destroy_metric_rollups
    metric_rollups.destroy_all
  end

  def destroy_metric_rollups_queue
    MiqQueue.put(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => 'destroy_metric_rollups',
      :msg_timeout => 1.hour
    )
  end

  # Support for multi-region DB. We need to try to find a time profile in each
  # region that matches the selected profile to ensure that we get results for
  # all the regions in the database. We only want one match from each region
  # otherwise we'll end up with duplicate daily rows.
  def profile_for_each_region
    if rollup_daily_metrics
      TimeProfile.rollup_daily_metrics.select { |p| p.profile == profile }
                 .group_by(&:region_id).values.map(&:first)
    else
      TimeProfile.none
    end
  end

  def match_user_tz?(user_id, user_tz)
    user_id = user_id.to_s
    tz == user_tz &&
      (profile_type == "global" ||
        (profile_type == "user" && profile_key == user_id))
  end

  private

  def rebuild_daily_metrics_on_create
    @rebuild_daily_metrics_on_create = true
  end

  def rebuild_daily_metrics_on_save
    if rollup_daily_metrics
      rebuild_daily_metrics_queue if @rebuild_daily_metrics_on_create || saved_change_to_profile? || saved_change_to_rollup_daily_metrics?
    elsif saved_change_to_rollup_daily_metrics?
      destroy_metric_rollups_queue
    end
  ensure
    @rebuild_daily_metrics_on_create = false
  end

  # TODO: use AR "or" here
  def self.for_user(user_id)
    where("profile_type = ? or (profile_type = ? and profile_key = ?)", "global", "user", user_id)
  end

  def self.ordered_by_desc
    order(Arel.sql("lower(description) ASC"))
  end

  def self.profiles_for_user(user_id, region_id)
    in_region(region_id)
      .for_user(user_id)
      .rollup_daily_metrics
      .ordered_by_desc
  end

  def self.profile_for_user_tz(user_id, user_tz)
    TimeProfile.rollup_daily_metrics.detect { |tp| tp.match_user_tz?(user_id, user_tz) }
  end

  # @param tz [nil|TimeProfile|TimeZone] (default timezone "UTC")
  # @return [TimeProfile] time profile that uses this time zone
  def self.default_time_profile(tz = DEFAULT_TZ)
    return tz if tz.kind_of?(TimeProfile)

    tz ||= DEFAULT_TZ
    rollup_daily_metrics.find_all_with_entire_tz.detect { |tp| tp.tz_or_default == tz }
  end
end