discourse/app/models/concerns/cached_counting.rb
Daniel Waterworth 55a1394342 DEV: pluck_first
Doing .pluck(:column).first is a very common pattern in Discourse and in
most cases, a limit cause isn't being added. Instead of adding a limit
clause to all these callsites, this commit adds two new methods to
ActiveRecord::Relation:

pluck_first, equivalent to limit(1).pluck(*columns).first

and pluck_first! which, like other finder methods, raises an exception
when no record is found
2019-10-21 12:08:20 +01:00

72 lines
1.7 KiB
Ruby

# frozen_string_literal: true
module CachedCounting
extend ActiveSupport::Concern
included do
class << self
attr_accessor :autoflush, :autoflush_seconds, :last_flush
end
# auto flush if backlog is larger than this
self.autoflush = 2000
# auto flush if older than this
self.autoflush_seconds = 5.minutes
self.last_flush = Time.now.utc
end
class_methods do
def perform_increment!(key, opts = nil)
val = $redis.incr(key).to_i
# readonly mode it is going to be 0, skip
return if val == 0
# 3.days, see: https://github.com/rails/rails/issues/21296
$redis.expire(key, 259200)
autoflush = (opts && opts[:autoflush]) || self.autoflush
if autoflush > 0 && val >= autoflush
write_cache!
return
end
if (Time.now.utc - last_flush).to_i > autoflush_seconds
write_cache!
end
end
def write_cache!(date = nil)
raise NotImplementedError
end
GET_AND_RESET = <<~LUA
local val = redis.call('get', KEYS[1])
redis.call('set', KEYS[1], '0')
return val
LUA
# this may seem a bit fancy but in so it allows
# for concurrent calls without double counting
def get_and_reset(key)
namespaced_key = $redis.namespace_key(key)
val = $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i
$redis.expire(key, 259200) # SET removes expiry, so set it again
val
end
def request_id(query_params, retries = 0)
id = where(query_params).pluck_first(:id)
id ||= create!(query_params.merge(count: 0)).id
rescue # primary key violation
if retries == 0
request_id(query_params, 1)
else
raise
end
end
end
end