diff options
Diffstat (limited to 'lib/gitlab/redis')
-rw-r--r-- | lib/gitlab/redis/cache.rb | 17 | ||||
-rw-r--r-- | lib/gitlab/redis/cluster_rate_limiting.rb | 11 | ||||
-rw-r--r-- | lib/gitlab/redis/db_load_balancing.rb | 23 | ||||
-rw-r--r-- | lib/gitlab/redis/duplicate_jobs.rb | 32 | ||||
-rw-r--r-- | lib/gitlab/redis/multi_store.rb | 64 | ||||
-rw-r--r-- | lib/gitlab/redis/rate_limiting.rb | 27 | ||||
-rw-r--r-- | lib/gitlab/redis/repository_cache.rb | 13 | ||||
-rw-r--r-- | lib/gitlab/redis/wrapper.rb | 17 |
8 files changed, 107 insertions, 97 deletions
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index 043f14630d5..647573e59fe 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -2,6 +2,16 @@ module Gitlab module Redis + # Match signature in + # https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L59 + ERROR_HANDLER = ->(method:, returning:, exception:) do + Gitlab::ErrorTracking.log_exception( + exception, + method: method, + returning: returning.inspect + ) + end + class Cache < ::Gitlab::Redis::Wrapper CACHE_NAMESPACE = 'cache:gitlab' @@ -12,9 +22,14 @@ module Gitlab redis: pool, compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')), namespace: CACHE_NAMESPACE, - expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i # Cache should not grow forever + expires_in: default_ttl_seconds, + error_handler: ::Gitlab::Redis::ERROR_HANDLER } end + + def self.default_ttl_seconds + ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i + end end end end diff --git a/lib/gitlab/redis/cluster_rate_limiting.rb b/lib/gitlab/redis/cluster_rate_limiting.rb new file mode 100644 index 00000000000..e9d1e4f0c3f --- /dev/null +++ b/lib/gitlab/redis/cluster_rate_limiting.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module Redis + class ClusterRateLimiting < ::Gitlab::Redis::Wrapper + def self.config_fallback + Cache + end + end + end +end diff --git a/lib/gitlab/redis/db_load_balancing.rb b/lib/gitlab/redis/db_load_balancing.rb new file mode 100644 index 00000000000..01276445611 --- /dev/null +++ b/lib/gitlab/redis/db_load_balancing.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Redis + class DbLoadBalancing < ::Gitlab::Redis::Wrapper + class << self + # The data we store on DbLoadBalancing used to be stored on SharedState. + def config_fallback + SharedState + end + + private + + def redis + primary_store = ::Redis.new(params) + secondary_store = ::Redis.new(config_fallback.params) + + MultiStore.new(primary_store, secondary_store, store_name) + end + end + end + end +end diff --git a/lib/gitlab/redis/duplicate_jobs.rb b/lib/gitlab/redis/duplicate_jobs.rb deleted file mode 100644 index c76d298da18..00000000000 --- a/lib/gitlab/redis/duplicate_jobs.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Redis - # Pseudo-store to transition `Gitlab::SidekiqMiddleware::DuplicateJobs` from - # using `Sidekiq.redis` to using the `SharedState` redis store. - class DuplicateJobs < ::Gitlab::Redis::Wrapper - class << self - def store_name - 'SharedState' - end - - private - - def redis - primary_store = ::Redis.new(Gitlab::Redis::SharedState.params) - - # `Sidekiq.redis` is a namespaced redis connection. This means keys are actually being stored under - # "resque:gitlab:resque:gitlab:duplicate:". For backwards compatibility, we make the secondary store - # namespaced in the same way, but omit it from the primary so keys have proper format there. - # rubocop:disable Cop/RedisQueueUsage - secondary_store = ::Redis::Namespace.new( - Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE, redis: ::Redis.new(Gitlab::Redis::Queues.params) - ) - # rubocop:enable Cop/RedisQueueUsage - - MultiStore.new(primary_store, secondary_store, name.demodulize) - end - end - end - end -end diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb index aa8f390ac10..a102267d52b 100644 --- a/lib/gitlab/redis/multi_store.rb +++ b/lib/gitlab/redis/multi_store.rb @@ -46,13 +46,6 @@ module Gitlab # # Ref: https://www.rubydoc.info/github/redis/redis-rb/Redis/Commands # - ENUMERATOR_CACHE_HIT_VALIDATOR = { - scan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, - hscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, - sscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, - zscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? } - }.freeze - READ_CACHE_HIT_VALIDATOR = { exists: ->(val) { val != 0 }, exists?: ->(val) { val }, @@ -62,13 +55,17 @@ module Gitlab hgetall: ->(val) { val.is_a?(Hash) && !val.empty? }, hlen: ->(val) { val != 0 }, hmget: ->(val) { val.is_a?(Array) && !val.compact.empty? }, + hscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, mapped_hmget: ->(val) { val.is_a?(Hash) && !val.compact.empty? }, mget: ->(val) { val.is_a?(Array) && !val.compact.empty? }, + scan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, scard: ->(val) { val != 0 }, sismember: ->(val) { val }, smembers: ->(val) { val.is_a?(Array) && !val.empty? }, sscan: ->(val) { val != ['0', []] }, - ttl: ->(val) { val != 0 && val != -2 } + sscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? }, + ttl: ->(val) { val != 0 && val != -2 }, # ttl returns -2 if the key does not exist. See https://redis.io/commands/ttl/ + zscan_each: ->(val) { val.is_a?(Enumerator) && !val.first.nil? } }.freeze WRITE_COMMANDS = %i[ @@ -134,20 +131,6 @@ module Gitlab end end - ENUMERATOR_CACHE_HIT_VALIDATOR.each_key do |name| - define_method(name) do |*args, **kwargs, &block| - enumerator = if use_primary_and_secondary_stores? - read_command(name, *args, **kwargs) - else - default_store.send(name, *args, **kwargs) - end - - return enumerator if block.nil? - - enumerator.each(&block) - end - end - PIPELINED_COMMANDS.each do |name| define_method(name) do |*args, **kwargs, &block| if use_primary_and_secondary_stores? @@ -186,11 +169,15 @@ module Gitlab end def use_primary_and_secondary_stores? - feature_enabled?("use_primary_and_secondary_stores_for") + feature_table_exists? && + Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}") && # rubocop:disable Cop/FeatureFlagUsage + !same_redis_store? end def use_primary_store_as_default? - feature_enabled?("use_primary_store_as_default_for") + feature_table_exists? && + Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}") && # rubocop:disable Cop/FeatureFlagUsage + !same_redis_store? end def increment_pipelined_command_error_count(command_name) @@ -217,6 +204,14 @@ module Gitlab extra.merge(command_name: command_name, instance_name: instance_name)) end + def default_store + use_primary_store_as_default? ? primary_store : secondary_store + end + + def fallback_store + use_primary_store_as_default? ? secondary_store : primary_store + end + def ping(message = nil) if use_primary_and_secondary_stores? # Both stores have to response success for the ping to be considered success. @@ -231,23 +226,14 @@ module Gitlab private # @return [Boolean] - def feature_enabled?(prefix) - feature_table_exists? && - Feature.enabled?("#{prefix}_#{instance_name.underscore}") && # rubocop:disable Cop/FeatureFlagUsage - !same_redis_store? - end - - # @return [Boolean] def feature_table_exists? + # Use table_exists? (which uses ActiveRecord's schema cache) instead of Feature.feature_flags_available? + # as the latter runs a ';' SQL query which causes a connection to be checked out. Feature::FlipperFeature.table_exists? rescue StandardError false end - def default_store - use_primary_store_as_default? ? primary_store : secondary_store - end - def log_method_missing(command_name, *_args) return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name) @@ -275,26 +261,26 @@ module Gitlab def read_one_with_fallback(command_name, *args, **kwargs, &block) begin - value = send_command(primary_store, command_name, *args, **kwargs, &block) + value = send_command(default_store, command_name, *args, **kwargs, &block) rescue StandardError => e log_error(e, command_name, multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE) end - return value if cache_hit?(command_name, value) + return value if block.nil? && cache_hit?(command_name, value) fallback_read(command_name, *args, **kwargs, &block) end def cache_hit?(command, value) - validator = READ_CACHE_HIT_VALIDATOR[command] || ENUMERATOR_CACHE_HIT_VALIDATOR[command] + validator = READ_CACHE_HIT_VALIDATOR[command] return false unless validator !value.nil? && validator.call(value) end def fallback_read(command_name, *args, **kwargs, &block) - value = send_command(secondary_store, command_name, *args, **kwargs, &block) + value = send_command(fallback_store, command_name, *args, **kwargs, &block) if value log_error(ReadFromPrimaryError.new, command_name) diff --git a/lib/gitlab/redis/rate_limiting.rb b/lib/gitlab/redis/rate_limiting.rb index 4ae1d55e4ce..12710bafbea 100644 --- a/lib/gitlab/redis/rate_limiting.rb +++ b/lib/gitlab/redis/rate_limiting.rb @@ -3,13 +3,28 @@ module Gitlab module Redis class RateLimiting < ::Gitlab::Redis::Wrapper - # The data we store on RateLimiting used to be stored on Cache. - def self.config_fallback - Cache - end + class << self + # The data we store on RateLimiting used to be stored on Cache. + def config_fallback + Cache + end + + def cache_store + @cache_store ||= ActiveSupport::Cache::RedisCacheStore.new( + redis: pool, + namespace: Cache::CACHE_NAMESPACE, + error_handler: ::Gitlab::Redis::ERROR_HANDLER + ) + end + + private + + def redis + primary_store = ::Redis.new(::Gitlab::Redis::ClusterRateLimiting.params) + secondary_store = ::Redis.new(params) - def self.cache_store - @cache_store ||= ActiveSupport::Cache::RedisCacheStore.new(redis: pool, namespace: Cache::CACHE_NAMESPACE) + MultiStore.new(primary_store, secondary_store, name.demodulize) + end end end end diff --git a/lib/gitlab/redis/repository_cache.rb b/lib/gitlab/redis/repository_cache.rb index 8bfbfcfea60..6c7bc8c41d5 100644 --- a/lib/gitlab/redis/repository_cache.rb +++ b/lib/gitlab/redis/repository_cache.rb @@ -14,19 +14,10 @@ module Gitlab redis: pool, compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')), namespace: Cache::CACHE_NAMESPACE, - # Cache should not grow forever - expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i + expires_in: Cache.default_ttl_seconds, + error_handler: ::Gitlab::Redis::ERROR_HANDLER ) end - - private - - def redis - primary_store = ::Redis.new(params) - secondary_store = ::Redis.new(config_fallback.params) - - MultiStore.new(primary_store, secondary_store, store_name) - end end end end diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index e5e1e1d4165..c990655769c 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -59,16 +59,11 @@ module Gitlab config_file_path("redis.#{store_name.underscore}.yml"), # The current Redis instance may have been split off from another one - # (e.g. TraceChunks was split off from SharedState). There are - # installations out there where the lowest priority config source - # (resque.yml) contains bogus values. In those cases, config_file_name - # should resolve to the instance we originated from (the - # "config_fallback") rather than resque.yml. + # (e.g. TraceChunks was split off from SharedState). config_fallback&.config_file_name, # Global config sources: - ENV['GITLAB_REDIS_CONFIG_FILE'], - config_file_path('resque.yml') + ENV['GITLAB_REDIS_CONFIG_FILE'] ].compact.first end @@ -199,11 +194,17 @@ module Gitlab def fetch_config redis_yml = read_yaml(self.class.redis_yml_path).fetch(@rails_env, {}) instance_config_yml = read_yaml(self.class.config_file_name)[@rails_env] + resque_yml = read_yaml(self.class.config_file_path('resque.yml'))[@rails_env] [ redis_yml[self.class.store_name.underscore], + # There are installations out there where the lowest priority config source (resque.yml) contains bogus + # values. In those cases, the configuration should be read for the instance we originated from (the + # "config_fallback"), either from its specific config file or from redis.yml, before falling back to + # resque.yml. instance_config_yml, - self.class.config_fallback && redis_yml[self.class.config_fallback.store_name.underscore] + self.class.config_fallback && redis_yml[self.class.config_fallback.store_name.underscore], + resque_yml ].compact.first end |