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