Commit c3ae59ee authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'remove-gitlab-redis-multi-store' into 'master'

Remove redis MultiStore implementation

See merge request gitlab-org/gitlab!78049
parents dbf2aac0 c66fb99d
---
name: use_primary_and_secondary_stores_for_sessions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73660
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1429
milestone: '14.6'
type: development
group: group::memory
default_enabled: false
---
name: use_primary_store_as_default_for_sessions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75258
rollout_issue_url:
milestone: '14.6'
type: development
group: group::memory
default_enabled: false
# frozen_string_literal: true
module Gitlab
module Redis
class MultiStore
include Gitlab::Utils::StrongMemoize
class ReadFromPrimaryError < StandardError
def message
'Value not found on the redis primary store. Read from the redis secondary store successful.'
end
end
class MethodMissingError < StandardError
def message
'Method missing. Falling back to execute method on the redis secondary store.'
end
end
attr_reader :primary_store, :secondary_store, :instance_name
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis primary_store.'
FAILED_TO_WRITE_ERROR_MESSAGE = 'Failed to write to the redis primary_store.'
SKIP_LOG_METHOD_MISSING_FOR_COMMANDS = %i(info).freeze
READ_COMMANDS = %i(
get
mget
smembers
scard
).freeze
WRITE_COMMANDS = %i(
set
setnx
setex
sadd
srem
del
pipelined
flushdb
).freeze
def initialize(primary_store, secondary_store, instance_name)
@primary_store = primary_store
@secondary_store = secondary_store
@instance_name = instance_name
validate_stores!
end
# rubocop:disable GitlabSecurity/PublicSend
READ_COMMANDS.each do |name|
define_method(name) do |*args, &block|
if use_primary_and_secondary_stores?
read_command(name, *args, &block)
else
default_store.send(name, *args, &block)
end
end
end
WRITE_COMMANDS.each do |name|
define_method(name) do |*args, &block|
if use_primary_and_secondary_stores?
write_command(name, *args, &block)
else
default_store.send(name, *args, &block)
end
end
end
def method_missing(...)
return @instance.send(...) if @instance
log_method_missing(...)
default_store.send(...)
end
# rubocop:enable GitlabSecurity/PublicSend
def respond_to_missing?(command_name, include_private = false)
true
end
# This is needed because of Redis::Rack::Connection is requiring Redis::Store
# https://github.com/redis-store/redis-rack/blob/a833086ba494083b6a384a1a4e58b36573a9165d/lib/redis/rack/connection.rb#L15
# Done similarly in https://github.com/lsegal/yard/blob/main/lib/yard/templates/template.rb#L122
def is_a?(klass)
return true if klass == default_store.class
super(klass)
end
alias_method :kind_of?, :is_a?
def to_s
use_primary_and_secondary_stores? ? primary_store.to_s : default_store.to_s
end
def use_primary_and_secondary_stores?
feature_table_exists? && Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
end
def use_primary_store_as_default?
feature_table_exists? && Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
end
private
# @return [Boolean]
def feature_table_exists?
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)
log_error(MethodMissingError.new, command_name)
increment_method_missing_count(command_name)
end
def read_command(command_name, *args, &block)
if @instance
send_command(@instance, command_name, *args, &block)
else
read_one_with_fallback(command_name, *args, &block)
end
end
def write_command(command_name, *args, &block)
if @instance
send_command(@instance, command_name, *args, &block)
else
write_both(command_name, *args, &block)
end
end
def read_one_with_fallback(command_name, *args, &block)
begin
value = send_command(primary_store, command_name, *args, &block)
rescue StandardError => e
log_error(e, command_name,
multi_store_error_message: FAILED_TO_READ_ERROR_MESSAGE)
end
value ||= fallback_read(command_name, *args, &block)
value
end
def fallback_read(command_name, *args, &block)
value = send_command(secondary_store, command_name, *args, &block)
if value
log_error(ReadFromPrimaryError.new, command_name)
increment_read_fallback_count(command_name)
end
value
end
def write_both(command_name, *args, &block)
begin
send_command(primary_store, command_name, *args, &block)
rescue StandardError => e
log_error(e, command_name,
multi_store_error_message: FAILED_TO_WRITE_ERROR_MESSAGE)
end
send_command(secondary_store, command_name, *args, &block)
end
def same_redis_store?
strong_memoize(:same_redis_store) do
# <Redis client v4.4.0 for redis:///path_to/redis/redis.socket/5>"
primary_store.inspect == secondary_store.inspect
end
end
# rubocop:disable GitlabSecurity/PublicSend
def send_command(redis_instance, command_name, *args, &block)
if block_given?
# Make sure that block is wrapped and executed only on the redis instance that is executing the block
redis_instance.send(command_name, *args) do |*params|
with_instance(redis_instance, *params, &block)
end
else
redis_instance.send(command_name, *args)
end
end
# rubocop:enable GitlabSecurity/PublicSend
def with_instance(instance, *params)
@instance = instance
yield(*params)
ensure
@instance = nil
end
def increment_read_fallback_count(command_name)
@read_fallback_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_read_fallback_total, 'Client side Redis MultiStore reading fallback')
@read_fallback_counter.increment(command: command_name, instance_name: instance_name)
end
def increment_method_missing_count(command_name)
@method_missing_counter ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_method_missing_total, 'Client side Redis MultiStore method missing')
@method_missing_counter.increment(command: command_name, instance_name: instance_name)
end
def validate_stores!
raise ArgumentError, 'primary_store is required' unless primary_store
raise ArgumentError, 'secondary_store is required' unless secondary_store
raise ArgumentError, 'instance_name is required' unless instance_name
raise ArgumentError, 'invalid primary_store' unless primary_store.is_a?(::Redis)
raise ArgumentError, 'invalid secondary_store' unless secondary_store.is_a?(::Redis)
end
def log_error(exception, command_name, extra = {})
Gitlab::ErrorTracking.log_exception(
exception,
command_name: command_name,
extra: extra.merge(instance_name: instance_name))
end
end
end
end
......@@ -9,40 +9,10 @@ module Gitlab
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2'
OTP_SESSIONS_NAMESPACE = 'session:otp'
class << self
# The data we store on Sessions used to be stored on SharedState.
def config_fallback
def self.config_fallback
SharedState
end
private
def redis
# Don't use multistore if redis.sessions configuration is not provided
return super if config_fallback?
primary_store = ::Redis.new(params)
secondary_store = ::Redis.new(config_fallback.params)
MultiStore.new(primary_store, secondary_store, store_name)
end
end
def store(extras = {})
# Don't use multistore if redis.sessions configuration is not provided
return super if self.class.config_fallback?
primary_store = create_redis_store(redis_store_options, extras)
secondary_store = create_redis_store(self.class.config_fallback.params, extras)
MultiStore.new(primary_store, secondary_store, self.class.store_name)
end
private
def create_redis_store(options, extras)
::Redis::Store.new(options.merge(extras))
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore do
using RSpec::Parameterized::TableSyntax
let_it_be(:redis_store_class) do
Class.new(Gitlab::Redis::Wrapper) do
def config_file_name
config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
Rails.root.join(config_file_name).to_s
end
def self.name
'Sessions'
end
end
end
let_it_be(:primary_db) { 1 }
let_it_be(:secondary_db) { 2 }
let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
let_it_be(:instance_name) { 'TestStore' }
let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
subject { multi_store.send(name, *args) }
before do
skip_feature_flags_yaml_validation
skip_default_enabled_yaml_check
end
after(:all) do
primary_store.flushdb
secondary_store.flushdb
end
context 'when primary_store is nil' do
let(:multi_store) { described_class.new(nil, secondary_store, instance_name)}
it 'fails with exception' do
expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/)
end
end
context 'when secondary_store is nil' do
let(:multi_store) { described_class.new(primary_store, nil, instance_name)}
it 'fails with exception' do
expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/)
end
end
context 'when instance_name is nil' do
let(:instance_name) { nil }
let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
it 'fails with exception' do
expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/)
end
end
context 'when primary_store is not a ::Redis instance' do
before do
allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
end
it 'fails with exception' do
expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/)
end
end
context 'when secondary_store is not a ::Redis instance' do
before do
allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
end
it 'fails with exception' do
expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/)
end
end
context 'with READ redis commands' do
let_it_be(:key1) { "redis:{1}:key_a" }
let_it_be(:key2) { "redis:{1}:key_b" }
let_it_be(:value1) { "redis_value1"}
let_it_be(:value2) { "redis_value2"}
let_it_be(:skey) { "redis:set:key" }
let_it_be(:keys) { [key1, key2] }
let_it_be(:values) { [value1, value2] }
let_it_be(:svalues) { [value2, value1] }
where(:case_name, :name, :args, :value, :block) do
'execute :get command' | :get | ref(:key1) | ref(:value1) | nil
'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil
'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value }
'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil
'execute :scard command' | :scard | ref(:skey) | 2 | nil
end
before(:all) do
primary_store.multi do |multi|
multi.set(key1, value1)
multi.set(key2, value2)
multi.sadd(skey, value1)
multi.sadd(skey, value2)
end
secondary_store.multi do |multi|
multi.set(key1, value1)
multi.set(key2, value2)
multi.sadd(skey, value1)
multi.sadd(skey, value2)
end
end
RSpec.shared_examples_for 'reads correct value' do
it 'returns the correct value' do
if value.is_a?(Array)
# :smembers does not guarantee the order it will return the values (unsorted set)
is_expected.to match_array(value)
else
is_expected.to eq(value)
end
end
end
RSpec.shared_examples_for 'fallback read from the secondary store' do
let(:counter) { Gitlab::Metrics::NullMetric.instance }
before do
allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
end
it 'fallback and execute on secondary instance' do
expect(secondary_store).to receive(name).with(*args).and_call_original
subject
end
it 'logs the ReadFromPrimaryError' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
subject
end
it 'increment read fallback count metrics' do
expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
subject
end
include_examples 'reads correct value'
context 'when fallback read from the secondary instance raises an exception' do
before do
allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
end
it 'fails with exception' do
expect { subject }.to raise_error(StandardError)
end
end
end
RSpec.shared_examples_for 'secondary store' do
it 'execute on the secondary instance' do
expect(secondary_store).to receive(name).with(*args).and_call_original
subject
end
include_examples 'reads correct value'
it 'does not execute on the primary store' do
expect(primary_store).not_to receive(name)
subject
end
end
with_them do
describe "#{name}" do
before do
allow(primary_store).to receive(name).and_call_original
allow(secondary_store).to receive(name).and_call_original
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
end
context 'when reading from the primary is successful' do
it 'returns the correct value' do
expect(primary_store).to receive(name).with(*args).and_call_original
subject
end
it 'does not execute on the secondary store' do
expect(secondary_store).not_to receive(name)
subject
end
include_examples 'reads correct value'
end
context 'when reading from primary instance is raising an exception' do
before do
allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'logs the exception' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
command_name: name))
subject
end
include_examples 'fallback read from the secondary store'
end
context 'when reading from primary instance return no value' do
before do
allow(primary_store).to receive(name).and_return(nil)
end
include_examples 'fallback read from the secondary store'
end
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do
multi_store.send(name, *args)
end
end
it 'is executed only 1 time on primary instance' do
expect(primary_store).to receive(name).with(*args).once
subject
end
end
if params[:block]
subject do
multi_store.send(name, *args, &block)
end
context 'when block is provided' do
it 'yields to the block' do
expect(primary_store).to receive(name).and_yield(value)
subject
end
include_examples 'reads correct value'
end
end
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
end
context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it_behaves_like 'secondary store'
end
context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it 'execute on the primary instance' do
expect(primary_store).to receive(name).with(*args).and_call_original
subject
end
include_examples 'reads correct value'
it 'does not execute on the secondary store' do
expect(secondary_store).not_to receive(name)
subject
end
end
end
context 'with both primary and secondary store using same redis instance' do
let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
it_behaves_like 'secondary store'
end
end
end
end
context 'with WRITE redis commands' do
let_it_be(:key1) { "redis:{1}:key_a" }
let_it_be(:key2) { "redis:{1}:key_b" }
let_it_be(:value1) { "redis_value1"}
let_it_be(:value2) { "redis_value2"}
let_it_be(:key1_value1) { [key1, value1] }
let_it_be(:key1_value2) { [key1, value2] }
let_it_be(:ttl) { 10 }
let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
let_it_be(:skey) { "redis:set:key" }
let_it_be(:svalues1) { [value2, value1] }
let_it_be(:svalues2) { [value1] }
let_it_be(:skey_value1) { [skey, value1] }
let_it_be(:skey_value2) { [skey, value2] }
where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
end
before do
primary_store.flushdb
secondary_store.flushdb
primary_store.multi do |multi|
multi.set(key2, value1)
multi.sadd(skey, value1)
end
secondary_store.multi do |multi|
multi.set(key2, value1)
multi.sadd(skey, value1)
end
end
RSpec.shared_examples_for 'verify that store contains values' do |store|
it "#{store} redis store contains correct values", :aggregate_errors do
subject
redis_store = multi_store.send(store)
if expected_value.is_a?(Array)
# :smembers does not guarantee the order it will return the values
expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
else
expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
end
end
end
with_them do
describe "#{name}" do
let(:expected_args) {args || no_args }
before do
allow(primary_store).to receive(name).and_call_original
allow(secondary_store).to receive(name).and_call_original
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
end
context 'when executing on primary instance is successful' do
it 'executes on both primary and secondary redis store', :aggregate_errors do
expect(primary_store).to receive(name).with(*expected_args).and_call_original
expect(secondary_store).to receive(name).with(*expected_args).and_call_original
subject
end
include_examples 'verify that store contains values', :primary_store
include_examples 'verify that store contains values', :secondary_store
end
context 'when executing on the primary instance is raising an exception' do
before do
allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'logs the exception and execute on secondary instance', :aggregate_errors do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(extra: hash_including(:multi_store_error_message), command_name: name))
expect(secondary_store).to receive(name).with(*expected_args).and_call_original
subject
end
include_examples 'verify that store contains values', :secondary_store
end
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do
multi_store.send(name, *args)
end
end
it 'is executed only 1 time on each instance', :aggregate_errors do
expect(primary_store).to receive(name).with(*expected_args).once
expect(secondary_store).to receive(name).with(*expected_args).once
subject
end
include_examples 'verify that store contains values', :primary_store
include_examples 'verify that store contains values', :secondary_store
end
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
end
context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it 'executes only on the secondary redis store', :aggregate_errors do
expect(secondary_store).to receive(name).with(*expected_args)
expect(primary_store).not_to receive(name).with(*expected_args)
subject
end
include_examples 'verify that store contains values', :secondary_store
end
context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it 'executes only on the primary_redis redis store', :aggregate_errors do
expect(primary_store).to receive(name).with(*expected_args)
expect(secondary_store).not_to receive(name).with(*expected_args)
subject
end
include_examples 'verify that store contains values', :primary_store
end
end
end
end
end
context 'with unsupported command' do
let(:counter) { Gitlab::Metrics::NullMetric.instance }
before do
primary_store.flushdb
secondary_store.flushdb
allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
end
let_it_be(:key) { "redis:counter" }
subject { multi_store.incr(key) }
it 'executes method missing' do
expect(multi_store).to receive(:method_missing)
subject
end
context 'when command is not in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
it 'logs MethodMissingError' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
subject
end
it 'increments method missing counter' do
expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name)
subject
end
end
context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
subject { multi_store.info }
it 'does not log MethodMissingError' do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
subject
end
it 'does not increment method missing counter' do
expect(counter).not_to receive(:increment)
subject
end
end
context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it 'fallback and executes only on the secondary store', :aggregate_errors do
expect(primary_store).to receive(:incr).with(key).and_call_original
expect(secondary_store).not_to receive(:incr)
subject
end
it 'correct value is stored on the secondary store', :aggregate_errors do
subject
expect(secondary_store.get(key)).to be_nil
expect(primary_store.get(key)).to eq('1')
end
end
context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it 'fallback and executes only on the secondary store', :aggregate_errors do
expect(secondary_store).to receive(:incr).with(key).and_call_original
expect(primary_store).not_to receive(:incr)
subject
end
it 'correct value is stored on the secondary store', :aggregate_errors do
subject
expect(primary_store.get(key)).to be_nil
expect(secondary_store.get(key)).to eq('1')
end
end
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do
multi_store.incr(key)
end
end
it 'is executed only 1 time on each instance', :aggregate_errors do
expect(primary_store).to receive(:incr).with(key).once
expect(secondary_store).to receive(:incr).with(key).once
subject
end
it "both redis stores are containing correct values", :aggregate_errors do
subject
expect(primary_store.get(key)).to eq('1')
expect(secondary_store.get(key)).to eq('1')
end
end
end
describe '#to_s' do
subject { multi_store.to_s }
context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
end
it 'returns same value as primary_store' do
is_expected.to eq(primary_store.to_s)
end
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
end
context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it 'returns same value as primary_store' do
is_expected.to eq(primary_store.to_s)
end
end
context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it 'returns same value as primary_store' do
is_expected.to eq(secondary_store.to_s)
end
end
end
end
describe '#is_a?' do
it 'returns true for ::Redis::Store' do
expect(multi_store.is_a?(::Redis::Store)).to be true
end
end
describe '#use_primary_and_secondary_stores?' do
context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be true
end
end
context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be false
end
end
context 'with empty DB' do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be false
end
end
context 'when FF table guard raises' do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be false
end
end
end
describe '#use_primary_store_as_default?' do
context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_store_as_default?).to be true
end
end
context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_store_as_default?).to be false
end
end
context 'with empty DB' do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be false
end
end
context 'when FF table guard raises' do
before do
allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
end
it 'multi store is disabled' do
expect(multi_store.use_primary_and_secondary_stores?).to be false
end
end
end
def create_redis_store(options, extras = {})
::Redis::Store.new(options.merge(extras))
end
end
......@@ -6,33 +6,18 @@ RSpec.describe Gitlab::Redis::Sessions do
it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
before do
around do |example|
clear_pool
end
after do
example.run
ensure
clear_pool
end
context 'when redis.sessions configuration is not provided' do
it 'uses ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Redis)
end
end
end
context 'when redis.sessions configuration is provided' do
it 'instantiates an instance of MultiStore' do
expect(described_class).to receive(:config_fallback?).and_return(false)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
end
end
end
def clear_pool
described_class.remove_instance_variable(:@pool)
......@@ -44,49 +29,9 @@ RSpec.describe Gitlab::Redis::Sessions do
describe '#store' do
subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
context 'when redis.sessions configuration is NOT provided' do
it 'instantiates ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
# Check that Gitlab::Redis::Sessions is configured as RedisStore.
it 'instantiates an instance of Redis::Store' do
expect(store).to be_instance_of(::Redis::Store)
end
end
context 'when redis.sessions configuration is provided' do
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
before do
redis_clear_raw_config!(Gitlab::Redis::Sessions)
redis_clear_raw_config!(Gitlab::Redis::SharedState)
allow(described_class).to receive(:config_fallback?).and_return(false)
end
after do
redis_clear_raw_config!(Gitlab::Redis::Sessions)
redis_clear_raw_config!(Gitlab::Redis::SharedState)
end
# Check that Gitlab::Redis::Sessions is configured as MultiStore with proper attrs.
it 'instantiates an instance of MultiStore', :aggregate_failures do
expect(described_class).to receive(:config_file_name).and_return(config_new_format_host)
expect(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
expect(store).to be_instance_of(::Gitlab::Redis::MultiStore)
expect(store.primary_store.to_s).to eq("Redis Client connected to test-host:6379 against DB 99 with namespace session:gitlab")
expect(store.secondary_store.to_s).to eq("Redis Client connected to /path/to/redis.sock against DB 0 with namespace session:gitlab")
expect(store.instance_name).to eq('Sessions')
end
context 'when MultiStore correctly configured' do
before do
allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
allow(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
end
it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
before do
stub_feature_flags(use_primary_and_secondary_stores => true)
end
it 'multi store is enabled' do
expect(subject.use_primary_and_secondary_stores?).to be true
end
end
context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
before do
stub_feature_flags(use_primary_and_secondary_stores => false)
end
it 'multi store is disabled' do
expect(subject.use_primary_and_secondary_stores?).to be false
end
end
context "with feature flag :#{use_primary_store_as_default} is enabled" do
before do
stub_feature_flags(use_primary_store_as_default => true)
end
it 'primary store is enabled' do
expect(subject.use_primary_store_as_default?).to be true
end
end
context "with feature flag :#{use_primary_store_as_default} is disabled" do
before do
stub_feature_flags(use_primary_store_as_default => false)
end
it 'primary store is disabled' do
expect(subject.use_primary_store_as_default?).to be false
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment