app/models/meter_cat/meter.rb
# 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