mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 13:09:33 -06:00
2a22a3b51d
Currently when a cache entry is corrupt, we log the event without doing anything else. It means the cache is still corrupt, and the proper value isn’t computed again. Normally, it’s very rare the cache becomes corrupt, but it can happen when upgrading Rails for example and the cache format changes. This is normally handled automatically by Rails but since we’re using a custom cache class, we have to do it ourselves. This patch takes the same approach the Rails team did, when a cache entry is corrupt, we treat it as a miss, recomputing the proper value and caching it in the new format.
122 lines
3.2 KiB
Ruby
122 lines
3.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Discourse specific cache, enforces 1 day expiry by default
|
|
|
|
# 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 `Discourse.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
|
|
# it is dangerous cause if we rename keys we will be stuck with
|
|
# pointless data
|
|
MAX_CACHE_AGE = 1.day unless defined?(MAX_CACHE_AGE)
|
|
|
|
attr_reader :namespace
|
|
|
|
# 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
|
|
Discourse.redis
|
|
end
|
|
|
|
def reconnect
|
|
redis.reconnect
|
|
end
|
|
|
|
def keys(pattern = "*")
|
|
redis.scan_each(match: "#{@namespace}:#{pattern}").to_a
|
|
end
|
|
|
|
def clear
|
|
keys.each { |k| redis.del(k) }
|
|
end
|
|
|
|
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).tap { |entry| break if entry == :__corrupt_cache__ }
|
|
end
|
|
|
|
def write(name, value, expires_in: nil)
|
|
write_entry(normalize_key(name), value, expires_in: expires_in)
|
|
end
|
|
|
|
def delete(name)
|
|
redis.del(normalize_key(name))
|
|
end
|
|
|
|
def fetch(name, expires_in: nil, force: nil, &blk)
|
|
if !block_given?
|
|
if force
|
|
raise ArgumentError,
|
|
"Missing block: Calling `Cache#fetch` with `force: true` requires a block."
|
|
end
|
|
return read(name)
|
|
end
|
|
|
|
key = normalize_key(name)
|
|
raw = redis.get(key) if !force
|
|
entry = read_entry(key) if raw
|
|
return entry if raw && !(entry == :__corrupt_cache__)
|
|
|
|
val = blk.call
|
|
write_entry(key, val, expires_in: expires_in)
|
|
val
|
|
end
|
|
|
|
protected
|
|
|
|
def log_first_exception(e, key)
|
|
return if defined?(@logged_a_warning)
|
|
@logged_a_warning = true
|
|
Discourse.warn_exception(e, message: "Corrupt cache... skipping entry for key #{key}")
|
|
end
|
|
|
|
def read_entry(key)
|
|
if data = redis.get(key)
|
|
Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
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, key)
|
|
:__corrupt_cache__
|
|
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
|
|
end
|