schrodingersbox/meter_cat

View on GitHub
app/models/meter_cat/meter.rb

Summary

Maintainability
A
25 mins
Test Coverage
# A Meter is simply an integer value for a unique name+date pair

require 'csv'

module MeterCat
  class Meter < ActiveRecord::Base

    validates :name, presence: true

    ###########################################################################
    # Constants

    # The expiration time for an in-memory cached meter

    DEFAULT_EXPIRATION = 3600

    # The number of retries in the event of an optimistic locking failure or creation collision

    DEFAULT_RETRY_ATTEMPTS = 5

    # The delay between retries, in seconds.
    # Not using exponential back-off to prevent blocking controllers.
    # Better to lose a little data than create a bad user experience.

    DEFAULT_RETRY_DELAY = 1

    ###########################################################################
    # Instance methods

    # Create an object for this name+date in the db if one does not already exist.
    # Add the value from this object to the one in the DB.
    # Returns the result of the ActiveRecord save operation.

    def add
      meter = nil
      Meter.uncached { meter = Meter.find_by_name_and_created_on(name, created_on) }
      meter ||= Meter.new(name: name, created_on: created_on)
      meter.value += value
      return meter.save
    end

    # Calls #add with retry logic up to ::MAX_ADD_ATTEMPTS times.
    # Catches ActiveRecord::StaleObjectError and ActiveRecord::RecordNotUnique for retries.
    # Returns the result of the final call to #add

    def add_with_retry
      success = false

      (1..MeterCat.config.retry_attempts).each do
        begin
          success = add
          break if success
        rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotUnique
        end
        Kernel.sleep(MeterCat.config.retry_delay)
      end

      return success
    end

    # Determines if the meter is expired and should be flushed from memory to DB

    def expired?
      return (Time.now - created_at) > MeterCat.config.expiration
    end

    ###########################################################################
    # Class methods

    # Returns all unique meter names sorted

    def self.names
      Meter.distinct.pluck(:name).sort.map(&:to_sym)
    end

    # Generates a random sequence for a meter given the following args:
    # [ :name, :min, :max, :days ]

    def self.random(args)
      name = args[:name]
      min = args[:min].to_i
      max = args[:max].to_i
      stop = Date.today
      start = Date.today - args[:days].to_i

      (start..stop).each do |date|
        value = min + rand(max - min)
        begin
          Meter.create(name: name, value: value, created_on: date)
        rescue
        end
      end
    end

    # Sets or creates a new record for the name and day

    def self.set(name, value = 1, created_on = Date.today)
      meter = Meter.find_by_name_and_created_on(name, created_on)
      meter ||= Meter.new(name: name, created_on: created_on)
      meter.value = value
      return meter.save
    end

    # Returns a hash of names to dates to values

    def self.to_h(range, names = nil)
      meters = {}

      # Inject dependencies into the names array

      calculator = MeterCat.config.calculator
      calculator.dependencies(names) if names

      # Build conditions for the query

      conditions = {}
      conditions[:created_on] = range if range
      conditions[:name] = names if names

      # Retrieve the data

      Meter.select([:id, :name, :created_on, :value]).where(conditions).find_each do |meter|
        name = meter.name.to_sym
        meters[name] ||= {}
        meters[name][meter.created_on] = meter.value
      end

      # Fill in calculated and missing values

      calculator.calculate(meters, range, names)
      names.each { |name| meters[name] ||= {} } if names

      return meters
    end

    # Returns a CSV where rows represent days

    def self.to_csv(range, names = nil)
      meters = to_h(range, names)
      keys = meters.keys.sort!

      CSV.generate do |csv|
        csv << [nil] + keys
        range.each do |date|
          csv << [date] + keys.map { |key| meters[key][date] }
        end
      end
    end
  end
end