perf improvements for traffic intensity

TrafficHelper's hot-path intensity does a single select for a single row and
loads the value rather than instantiate an ActiveRecord object.

Updating the intensity has moved entirely to the cron job to prevent dogpiling,
and does three upserts instead of select + upsert.

No more hassling around with different keys for intensity when we only use the
latest value.
This commit is contained in:
Peter Bhat Harkins 2022-03-19 08:15:20 -05:00
parent c98648933b
commit e334ae41b4
3 changed files with 20 additions and 37 deletions

View File

@ -29,19 +29,15 @@ module TrafficHelper
result.to_a.first
end
def self.cached_traffic_range
low, high = nil, nil
low = Keystore.readthrough_cache('traffic:low') do
low, high = traffic_range
Keystore.put('traffic:high', high)
low
end
high ||= Keystore.value_for('traffic:high')
[low, high]
def self.cache_traffic!
low, high = self.traffic_range
Keystore.put('traffic:low', low)
Keystore.put('traffic:high', high)
Keystore.put('traffic:intensity', current_intensity(low, high))
end
def self.current_activity
start_at = Time.now.utc - 15.minutes
start_at = PERIOD_LENGTH.minutes.ago
result = ActiveRecord::Base.connection.execute <<-SQL
select
(SELECT count(1) AS n_votes FROM votes WHERE updated_at >= '#{start_at}') +
@ -51,20 +47,13 @@ module TrafficHelper
result.to_a.first.first
end
def self.current_intensity
low, high = cached_traffic_range
def self.current_intensity(low, high)
return 0.5 if low.nil? || high.nil? || high == low
activity = [low, current_activity, high].sort[1]
[0, ((activity - low)*1.0/(high - low) * 100).round, 100].sort[1]
end
def self.current_period_key
"traffic:at:#{(Time.now.utc.to_i/CACHE_FOR.minutes).floor}"
end
def self.cached_current_intensity
Keystore.readthrough_cache(current_period_key) do
current_intensity
end
Keystore.value_for('traffic:intensity') || 0.5
end
end

View File

@ -6,11 +6,11 @@ class Keystore < ApplicationRecord
validates :key, presence: true, length: { maximum: MAX_KEY_LENGTH }
def self.get(key)
self.find_by(:key => key)
self.find_by(key: key)
end
def self.value_for(key)
self.find_by(:key => key).try(:value)
self.where(key: 'traffic:intensity').limit(1).pluck(:value).first
end
def self.put(key, value)
@ -76,6 +76,14 @@ class Keystore < ApplicationRecord
end
end
def self.decrement_value_for(key, amount = -1)
self.increment_value_for(key, amount)
end
def self.decremented_value_for(key, amount = -1)
self.incremented_value_for(key, amount)
end
# deliberately no lock/transaction as TrafficHelper is on the hot path of every request
def self.readthrough_cache(key, &blk)
if (found = value_for(key))
@ -87,14 +95,6 @@ class Keystore < ApplicationRecord
end
end
def self.decrement_value_for(key, amount = -1)
self.increment_value_for(key, amount)
end
def self.decremented_value_for(key, amount = -1)
self.incremented_value_for(key, amount)
end
def self.validate_input_key(key)
exception = ActiveRecord::ValueTooLong.new("#{MAX_KEY_LENGTH}" \
" characters is the maximum allowed for key")

View File

@ -9,11 +9,5 @@ require File.expand_path('../../config/boot', __FILE__)
require APP_PATH
Rails.application.require_environment!
# Delete old period keys
Keystore
.where('`key` like "traffic:at:%"')
.where.not('`key` = ?', TrafficHelper.current_period_key)
.delete_all
# calculate and cache new low and high activity range for traffic
TrafficHelper.cached_traffic_range
# calculate and cache activity range for traffic
TrafficHelper.cache_traffic!