1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
# frozen_string_literal: true
# The usage of the ReactiveCaching module is documented here:
# https://docs.gitlab.com/ee/development/reactive_caching.html
#
module ReactiveCaching
extend ActiveSupport::Concern
InvalidateReactiveCache = Class.new(StandardError)
ExceededReactiveCacheLimit = Class.new(StandardError)
WORK_TYPE = {
no_dependency: ReactiveCachingWorker,
external_dependency: ExternalServiceReactiveCachingWorker
}.freeze
included do
extend ActiveModel::Naming
class_attribute :reactive_cache_key
class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_refresh_interval
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_hard_limit
class_attribute :reactive_cache_work_type
class_attribute :reactive_cache_worker_finder
# defaults
self.reactive_cache_key = ->(record) { [model_name.singular, record.id] }
self.reactive_cache_lease_timeout = 2.minutes
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = nil # this value should be set in megabytes. E.g: 1.megabyte
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
def calculate_reactive_cache(*args)
raise NotImplementedError
end
def reactive_cache_updated(*args); end
def with_reactive_cache(*args, &blk)
unless within_reactive_cache_lifetime?(*args)
refresh_reactive_cache!(*args)
return
end
keep_alive_reactive_cache!(*args)
begin
data = Rails.cache.read(full_reactive_cache_key(*args))
yield data unless data.nil?
rescue InvalidateReactiveCache
refresh_reactive_cache!(*args)
nil
end
end
def with_reactive_cache_set(resource, opts, &blk)
data = with_reactive_cache(resource, opts, &blk)
save_keys_in_set(resource, opts) if data
data
end
# This method is used for debugging purposes and should not be used otherwise.
def without_reactive_cache(*args, &blk)
return with_reactive_cache(*args, &blk) unless Rails.env.development?
data = self.class.reactive_cache_worker_finder.call(id, *args).calculate_reactive_cache(*args)
yield data
end
def clear_reactive_cache!(*args)
Rails.cache.delete(full_reactive_cache_key(*args))
Rails.cache.delete(alive_reactive_cache_key(*args))
end
def clear_reactive_cache_set!(*args)
cache_key = full_reactive_cache_key(args)
reactive_set_cache.clear_cache!(cache_key)
end
def exclusively_update_reactive_cache!(*args)
locking_reactive_cache(*args) do
key = full_reactive_cache_key(*args)
if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do
new_value = calculate_reactive_cache(*args)
check_exceeded_reactive_cache_limit!(new_value)
old_value = Rails.cache.read(key)
Rails.cache.write(key, new_value)
reactive_cache_updated(*args) if new_value != old_value
end
else
Rails.cache.delete(key)
end
end
end
private
def save_keys_in_set(resource, opts)
cache_key = full_reactive_cache_key(resource)
reactive_set_cache.write(cache_key, "#{cache_key}:#{opts}")
end
def reactive_set_cache
Gitlab::ReactiveCacheSetCache.new(expires_in: reactive_cache_lifetime)
end
def refresh_reactive_cache!(*args)
clear_reactive_cache!(*args)
keep_alive_reactive_cache!(*args)
worker_class.perform_async(self.class, id, *args)
end
def keep_alive_reactive_cache!(...)
Rails.cache.write(alive_reactive_cache_key(...), true, expires_in: self.class.reactive_cache_lifetime)
end
def full_reactive_cache_key(*qualifiers)
prefix = self.class.reactive_cache_key
prefix = prefix.call(self) if prefix.respond_to?(:call)
([prefix].flatten + qualifiers).join(':')
end
def alive_reactive_cache_key(*qualifiers)
full_reactive_cache_key(*(qualifiers + ['alive']))
end
def locking_reactive_cache(*args)
lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
uuid = lease.try_obtain
yield if uuid
ensure
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
def within_reactive_cache_lifetime?(...)
Rails.cache.exist?(alive_reactive_cache_key(...))
end
def enqueuing_update(*args)
yield
worker_class.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
end
def worker_class
WORK_TYPE.fetch(self.class.reactive_cache_work_type.to_sym)
end
def reactive_cache_limit_enabled?
!!self.reactive_cache_hard_limit
end
def check_exceeded_reactive_cache_limit!(data)
return unless reactive_cache_limit_enabled?
data_deep_size = Gitlab::Utils::DeepSize.new(data, max_size: self.class.reactive_cache_hard_limit)
raise ExceededReactiveCacheLimit unless data_deep_size.valid?
end
end
end
|