2018-08-03 13:22:24 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-01-16 07:08:32 -05:00
|
|
|
# The usage of the ReactiveCaching module is documented here:
|
2020-12-03 07:09:39 -05:00
|
|
|
# https://docs.gitlab.com/ee/development/reactive_caching.html
|
|
|
|
#
|
2016-12-16 23:09:50 -05:00
|
|
|
module ReactiveCaching
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
2018-08-07 10:02:57 -04:00
|
|
|
InvalidateReactiveCache = Class.new(StandardError)
|
2020-02-10 10:08:54 -05:00
|
|
|
ExceededReactiveCacheLimit = Class.new(StandardError)
|
2018-08-07 10:02:57 -04:00
|
|
|
|
2020-04-24 05:09:44 -04:00
|
|
|
WORK_TYPE = {
|
2020-09-22 08:09:39 -04:00
|
|
|
no_dependency: ReactiveCachingWorker,
|
2020-04-24 05:09:44 -04:00
|
|
|
external_dependency: ExternalServiceReactiveCachingWorker
|
|
|
|
}.freeze
|
|
|
|
|
2016-12-16 23:09:50 -05:00
|
|
|
included do
|
2020-02-13 07:08:49 -05:00
|
|
|
extend ActiveModel::Naming
|
|
|
|
|
2016-12-16 23:09:50 -05:00
|
|
|
class_attribute :reactive_cache_key
|
2020-02-10 10:08:54 -05:00
|
|
|
class_attribute :reactive_cache_lease_timeout
|
2016-12-16 23:09:50 -05:00
|
|
|
class_attribute :reactive_cache_refresh_interval
|
2020-02-10 10:08:54 -05:00
|
|
|
class_attribute :reactive_cache_lifetime
|
|
|
|
class_attribute :reactive_cache_hard_limit
|
2020-04-24 05:09:44 -04:00
|
|
|
class_attribute :reactive_cache_work_type
|
2019-04-04 13:27:29 -04:00
|
|
|
class_attribute :reactive_cache_worker_finder
|
2016-12-16 23:09:50 -05:00
|
|
|
|
|
|
|
# defaults
|
2019-06-07 03:17:33 -04:00
|
|
|
self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
|
2016-12-16 23:09:50 -05:00
|
|
|
self.reactive_cache_lease_timeout = 2.minutes
|
|
|
|
self.reactive_cache_refresh_interval = 1.minute
|
|
|
|
self.reactive_cache_lifetime = 10.minutes
|
2020-06-24 08:09:24 -04:00
|
|
|
self.reactive_cache_hard_limit = nil # this value should be set in megabytes. E.g: 1.megabyte
|
2019-04-04 13:27:29 -04:00
|
|
|
self.reactive_cache_worker_finder = ->(id, *_args) do
|
|
|
|
find_by(primary_key => id)
|
|
|
|
end
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
def calculate_reactive_cache(*args)
|
2016-11-22 14:55:56 -05:00
|
|
|
raise NotImplementedError
|
|
|
|
end
|
|
|
|
|
2018-07-25 06:03:18 -04:00
|
|
|
def reactive_cache_updated(*args)
|
|
|
|
end
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
def with_reactive_cache(*args, &blk)
|
2018-08-07 10:02:57 -04:00
|
|
|
unless within_reactive_cache_lifetime?(*args)
|
|
|
|
refresh_reactive_cache!(*args)
|
2019-02-08 07:19:53 -05:00
|
|
|
return
|
2018-08-07 10:02:57 -04:00
|
|
|
end
|
2018-05-08 02:07:48 -04:00
|
|
|
|
2018-08-07 10:02:57 -04:00
|
|
|
keep_alive_reactive_cache!(*args)
|
|
|
|
|
|
|
|
begin
|
2017-01-12 17:31:02 -05:00
|
|
|
data = Rails.cache.read(full_reactive_cache_key(*args))
|
2019-02-18 06:24:00 -05:00
|
|
|
yield data unless data.nil?
|
2018-08-07 10:02:57 -04:00
|
|
|
rescue InvalidateReactiveCache
|
|
|
|
refresh_reactive_cache!(*args)
|
|
|
|
nil
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-02-26 10:08:56 -05:00
|
|
|
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
|
|
|
|
|
2019-12-23 04:07:42 -05:00
|
|
|
# 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
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
def clear_reactive_cache!(*args)
|
|
|
|
Rails.cache.delete(full_reactive_cache_key(*args))
|
2018-06-01 09:15:13 -04:00
|
|
|
Rails.cache.delete(alive_reactive_cache_key(*args))
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
|
2020-02-26 10:08:56 -05:00
|
|
|
def clear_reactive_cache_set!(*args)
|
|
|
|
cache_key = full_reactive_cache_key(args)
|
|
|
|
|
|
|
|
reactive_set_cache.clear_cache!(cache_key)
|
|
|
|
end
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
def exclusively_update_reactive_cache!(*args)
|
|
|
|
locking_reactive_cache(*args) do
|
2019-11-19 13:06:27 -05:00
|
|
|
key = full_reactive_cache_key(*args)
|
|
|
|
|
2018-05-08 02:07:48 -04:00
|
|
|
if within_reactive_cache_lifetime?(*args)
|
2017-01-12 17:31:02 -05:00
|
|
|
enqueuing_update(*args) do
|
2018-07-25 06:03:18 -04:00
|
|
|
new_value = calculate_reactive_cache(*args)
|
2020-02-10 10:08:54 -05:00
|
|
|
check_exceeded_reactive_cache_limit!(new_value)
|
|
|
|
|
2018-07-25 06:03:18 -04:00
|
|
|
old_value = Rails.cache.read(key)
|
|
|
|
Rails.cache.write(key, new_value)
|
|
|
|
reactive_cache_updated(*args) if new_value != old_value
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
2019-11-19 13:06:27 -05:00
|
|
|
else
|
|
|
|
Rails.cache.delete(key)
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-02-26 10:08:56 -05:00
|
|
|
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
|
|
|
|
|
2018-08-07 10:02:57 -04:00
|
|
|
def refresh_reactive_cache!(*args)
|
|
|
|
clear_reactive_cache!(*args)
|
|
|
|
keep_alive_reactive_cache!(*args)
|
2020-04-24 05:09:44 -04:00
|
|
|
worker_class.perform_async(self.class, id, *args)
|
2018-08-07 10:02:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def keep_alive_reactive_cache!(*args)
|
|
|
|
Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
|
|
|
|
end
|
|
|
|
|
2016-12-16 23:09:50 -05:00
|
|
|
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
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
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)
|
2016-12-16 23:09:50 -05:00
|
|
|
uuid = lease.try_obtain
|
|
|
|
yield if uuid
|
|
|
|
ensure
|
2017-01-12 17:31:02 -05:00
|
|
|
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
|
2018-05-08 02:07:48 -04:00
|
|
|
def within_reactive_cache_lifetime?(*args)
|
2019-09-18 10:02:45 -04:00
|
|
|
Rails.cache.exist?(alive_reactive_cache_key(*args))
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
|
2017-01-12 17:31:02 -05:00
|
|
|
def enqueuing_update(*args)
|
2016-12-16 23:09:50 -05:00
|
|
|
yield
|
2019-07-06 03:58:13 -04:00
|
|
|
|
2020-04-24 05:09:44 -04:00
|
|
|
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)
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
2020-02-10 10:08:54 -05:00
|
|
|
|
2020-06-24 08:09:24 -04:00
|
|
|
def reactive_cache_limit_enabled?
|
|
|
|
!!self.reactive_cache_hard_limit
|
|
|
|
end
|
|
|
|
|
2020-02-10 10:08:54 -05:00
|
|
|
def check_exceeded_reactive_cache_limit!(data)
|
2020-06-24 08:09:24 -04:00
|
|
|
return unless reactive_cache_limit_enabled?
|
2020-02-10 10:08:54 -05:00
|
|
|
|
|
|
|
data_deep_size = Gitlab::Utils::DeepSize.new(data, max_size: self.class.reactive_cache_hard_limit)
|
|
|
|
|
2021-05-04 11:10:36 -04:00
|
|
|
raise ExceededReactiveCacheLimit unless data_deep_size.valid?
|
2020-02-10 10:08:54 -05:00
|
|
|
end
|
2016-12-16 23:09:50 -05:00
|
|
|
end
|
|
|
|
end
|