mirror of
https://github.com/discourse/discourse.git
synced 2025-05-28 22:17:45 +08:00
DEV: Implement a faster Discourse.cache
This is a bottom up rewrite of Discourse cache to support faster performance and a limited surface area. ActiveSupport::Cache::Store accepts many options we do not use, this partial implementation only picks the bits out that we do use and want to support. Additionally params are named which avoids typos such as "expires_at" vs "expires_in" This also moves a few spots in Discourse to use Discourse.cache over setex Performance of setex and Discourse.cache.write is similar.
This commit is contained in:
107
lib/cache.rb
107
lib/cache.rb
@ -1,8 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Discourse specific cache, enforces 1 day expiry
|
||||
# Discourse specific cache, enforces 1 day expiry by default
|
||||
|
||||
class Cache < ActiveSupport::Cache::Store
|
||||
# This is a bottom up implementation of ActiveSupport::Cache::Store
|
||||
# this allows us to cleanly implement without using cache entries and version
|
||||
# support which we do not use, in tern this makes the cache as fast as simply
|
||||
# using `$redis.setex` with a more convenient API
|
||||
#
|
||||
# It only implements a subset of ActiveSupport::Cache::Store as we make no use
|
||||
# of large parts of the interface.
|
||||
#
|
||||
# An additional advantage of this class is that all methods have named params
|
||||
# Rails tends to use options hash for lots of stuff due to legacy reasons
|
||||
# this makes it harder to reason about the API
|
||||
|
||||
class Cache
|
||||
|
||||
# nothing is cached for longer than 1 day EVER
|
||||
# there is no reason to have data older than this clogging redis
|
||||
@ -10,9 +22,14 @@ class Cache < ActiveSupport::Cache::Store
|
||||
# pointless data
|
||||
MAX_CACHE_AGE = 1.day unless defined? MAX_CACHE_AGE
|
||||
|
||||
def initialize(opts = {})
|
||||
@namespace = opts[:namespace] || "_CACHE_"
|
||||
super(opts)
|
||||
# we don't need this feature, 1 day expiry is enough
|
||||
# it makes lookups a tad cheaper
|
||||
def self.supports_cache_versioning?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize(namespace: "_CACHE")
|
||||
@namespace = namespace
|
||||
end
|
||||
|
||||
def redis
|
||||
@ -33,30 +50,82 @@ class Cache < ActiveSupport::Cache::Store
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_key(key, opts = nil)
|
||||
def normalize_key(key)
|
||||
"#{@namespace}:#{key}"
|
||||
end
|
||||
|
||||
def exist?(name)
|
||||
key = normalize_key(name)
|
||||
redis.exists(key)
|
||||
end
|
||||
|
||||
# this removes a bunch of stuff we do not need like instrumentation and versioning
|
||||
def read(name)
|
||||
key = normalize_key(name)
|
||||
read_entry(key)
|
||||
end
|
||||
|
||||
def write(name, value, expires_in: nil)
|
||||
write_entry(normalize_key(name), value, expires_in: nil)
|
||||
end
|
||||
|
||||
def delete(name)
|
||||
redis.del(normalize_key(name))
|
||||
end
|
||||
|
||||
def fetch(name, expires_in: nil, force: nil, &blk)
|
||||
if block_given?
|
||||
key = normalize_key(name)
|
||||
raw = nil
|
||||
|
||||
if !force
|
||||
raw = redis.get(key)
|
||||
end
|
||||
|
||||
if raw
|
||||
begin
|
||||
Marshal.load(raw)
|
||||
rescue => e
|
||||
log_first_exception(e)
|
||||
end
|
||||
else
|
||||
val = blk.call
|
||||
write_entry(key, val, expires_in: expires_in)
|
||||
val
|
||||
end
|
||||
elsif force
|
||||
raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
|
||||
else
|
||||
read(name)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_entry(key, options)
|
||||
if data = redis.get(key)
|
||||
data = Marshal.load(data)
|
||||
ActiveSupport::Cache::Entry.new data
|
||||
def log_first_exception(e)
|
||||
if !defined? @logged_a_warning
|
||||
@logged_a_warning = true
|
||||
Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}")
|
||||
end
|
||||
rescue
|
||||
# corrupt cache, fail silently for now, remove rescue later
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options)
|
||||
dumped = Marshal.dump(entry.value)
|
||||
expiry = options[:expires_in] || MAX_CACHE_AGE
|
||||
def read_entry(key)
|
||||
if data = redis.get(key)
|
||||
Marshal.load(data)
|
||||
end
|
||||
rescue => e
|
||||
# corrupt cache, this can happen if Marshal version
|
||||
# changes. Log it once so we can tell it is happening.
|
||||
# should not happen under any normal circumstances, but we
|
||||
# do not want to flood logs
|
||||
log_first_exception(e)
|
||||
end
|
||||
|
||||
def write_entry(key, value, expires_in: nil)
|
||||
dumped = Marshal.dump(value)
|
||||
expiry = expires_in || MAX_CACHE_AGE
|
||||
redis.setex(key, expiry, dumped)
|
||||
true
|
||||
end
|
||||
|
||||
def delete_entry(key, options)
|
||||
redis.del key
|
||||
end
|
||||
|
||||
end
|
||||
|
Reference in New Issue
Block a user