diff options
Diffstat (limited to 'spec/lib/gitlab/redis/multi_store_spec.rb')
-rw-r--r-- | spec/lib/gitlab/redis/multi_store_spec.rb | 667 |
1 files changed, 562 insertions, 105 deletions
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb index 0e7eedf66b1..f198ba90d0a 100644 --- a/spec/lib/gitlab/redis/multi_store_spec.rb +++ b/spec/lib/gitlab/redis/multi_store_spec.rb @@ -25,7 +25,9 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 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) } + subject do + multi_store.send(name, *args) + end before do skip_feature_flags_yaml_validation @@ -108,34 +110,93 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end + # rubocop:disable RSpec/MultipleMemoizedHelpers context 'with READ redis commands' do + subject do + multi_store.send(name, *args, **kwargs) + end + 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(:skey2) { "redis:set:key2" } + let_it_be(:smemberargs) { [skey, value1] } + let_it_be(:hkey) { "redis:hash:key" } + let_it_be(:hkey2) { "redis:hash:key2" } + let_it_be(:zkey) { "redis:sortedset:key" } + let_it_be(:zkey2) { "redis:sortedset:key2" } + let_it_be(:hitem1) { "item1" } + let_it_be(:hitem2) { "item2" } 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 + let_it_be(:hgetargs) { [hkey, hitem1] } + let_it_be(:hmgetval) { [value1] } + let_it_be(:mhmgetargs) { [hkey, hitem1] } + let_it_be(:hvalmapped) { { "item1" => value1 } } + let_it_be(:sscanargs) { [skey2, 0] } + let_it_be(:sscanval) { ["0", [value1]] } + let_it_be(:sscan_eachval) { [value1] } + let_it_be(:sscan_each_arg) { { match: '*1*' } } + let_it_be(:hscan_eachval) { [[hitem1, value1]] } + let_it_be(:zscan_eachval) { [[value1, 1.0]] } + let_it_be(:scan_each_arg) { { match: 'redis*' } } + let_it_be(:scan_each_val) { [key1, key2, skey, skey2, hkey, hkey2, zkey, zkey2] } + + # rubocop:disable Layout/LineLength + where(:case_name, :name, :args, :value, :kwargs, :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 + 'execute :sismember command' | :sismember | ref(:smemberargs) | true | {} | nil + 'execute :exists command' | :exists | ref(:key1) | 1 | {} | nil + 'execute :exists? command' | :exists? | ref(:key1) | true | {} | nil + 'execute :hget command' | :hget | ref(:hgetargs) | ref(:value1) | {} | nil + 'execute :hlen command' | :hlen | ref(:hkey) | 1 | {} | nil + 'execute :hgetall command' | :hgetall | ref(:hkey) | ref(:hvalmapped) | {} | nil + 'execute :hexists command' | :hexists | ref(:hgetargs) | true | {} | nil + 'execute :hmget command' | :hmget | ref(:hgetargs) | ref(:hmgetval) | {} | nil + 'execute :mapped_hmget command' | :mapped_hmget | ref(:mhmgetargs) | ref(:hvalmapped) | {} | nil + 'execute :sscan command' | :sscan | ref(:sscanargs) | ref(:sscanval) | {} | nil + + # we run *scan_each here as they are reads too + 'execute :scan_each command' | :scan_each | nil | ref(:scan_each_val) | ref(:scan_each_arg) | nil + 'execute :sscan_each command' | :sscan_each | ref(:skey2) | ref(:sscan_eachval) | {} | nil + 'execute :sscan_each w block' | :sscan_each | ref(:skey) | ref(:sscan_eachval) | ref(:sscan_each_arg) | nil + 'execute :hscan_each command' | :hscan_each | ref(:hkey) | ref(:hscan_eachval) | {} | nil + 'execute :hscan_each w block' | :hscan_each | ref(:hkey2) | ref(:hscan_eachval) | ref(:sscan_each_arg) | nil + 'execute :zscan_each command' | :zscan_each | ref(:zkey) | ref(:zscan_eachval) | {} | nil + 'execute :zscan_each w block' | :zscan_each | ref(:zkey2) | ref(:zscan_eachval) | ref(:sscan_each_arg) | nil end + # rubocop:enable Layout/LineLength - before(:all) do + before do primary_store.set(key1, value1) primary_store.set(key2, value2) - primary_store.sadd?(skey, value1) - primary_store.sadd?(skey, value2) + primary_store.sadd?(skey, [value1, value2]) + primary_store.sadd?(skey2, [value1]) + primary_store.hset(hkey, hitem1, value1) + primary_store.hset(hkey2, hitem1, value1, hitem2, value2) + primary_store.zadd(zkey, 1, value1) + primary_store.zadd(zkey2, [[1, value1], [2, value2]]) secondary_store.set(key1, value1) secondary_store.set(key2, value2) - secondary_store.sadd?(skey, value1) - secondary_store.sadd?(skey, value2) + secondary_store.sadd?(skey, [value1, value2]) + secondary_store.sadd?(skey2, [value1]) + secondary_store.hset(hkey, hitem1, value1) + secondary_store.hset(hkey2, hitem1, value1, hitem2, value2) + secondary_store.zadd(zkey, 1, value1) + secondary_store.zadd(zkey2, [[1, value1], [2, value2]]) + end + + after do + primary_store.flushdb + secondary_store.flushdb end RSpec.shared_examples_for 'reads correct value' do @@ -157,7 +218,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end it 'fallback and execute on secondary instance' do - expect(secondary_store).to receive(name).with(*args).and_call_original + expect(secondary_store).to receive(name).with(*expected_args).and_call_original subject end @@ -181,7 +242,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 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) + allow(secondary_store).to receive(name).with(*expected_args).and_raise(StandardError) end it 'fails with exception' do @@ -192,7 +253,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 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 + expect(secondary_store).to receive(name).with(*expected_args).and_call_original subject end @@ -208,6 +269,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do with_them do describe name.to_s do + let(:expected_args) { kwargs&.present? ? [*args, { **kwargs }] : Array(args) } + before do allow(primary_store).to receive(name).and_call_original allow(secondary_store).to receive(name).and_call_original @@ -215,7 +278,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 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 + expect(primary_store).to receive(name).with(*expected_args).and_call_original subject end @@ -231,7 +294,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 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(primary_store).to receive(name).with(*expected_args).and_raise(StandardError) allow(Gitlab::ErrorTracking).to receive(:log_exception) end @@ -245,9 +308,10 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do include_examples 'fallback read from the secondary store' end - context 'when reading from primary instance return no value' do + context 'when reading from empty primary instance' do before do - allow(primary_store).to receive(name).and_return(nil) + # this ensures a cache miss without having to stub primary store + primary_store.flushdb end include_examples 'fallback read from the secondary store' @@ -256,7 +320,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do context 'when the command is executed within pipelined block' do subject do multi_store.pipelined do |pipeline| - pipeline.send(name, *args) + pipeline.send(name, *args, **kwargs) end end @@ -266,7 +330,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do 2.times do expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| - expect(pipeline).to receive(name).with(*args).once.and_call_original + expect(pipeline).to receive(name).with(*expected_args).once.and_call_original end end @@ -276,7 +340,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do if params[:block] subject do - multi_store.send(name, *args, &block) + multi_store.send(name, *expected_args, &block) end context 'when block is provided' do @@ -297,6 +361,115 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do it_behaves_like 'secondary store' end + + context 'when use_primary_and_secondary_stores feature flag is disabled' do + before do + stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) + end + + context 'when using secondary store as default' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it 'executes only on secondary redis store', :aggregate_errors do + expect(secondary_store).to receive(name).with(*expected_args).and_call_original + expect(primary_store).not_to receive(name).with(*expected_args).and_call_original + + subject + end + end + + context 'when using primary store as default' do + it 'executes only on primary redis store', :aggregate_errors do + expect(primary_store).to receive(name).with(*expected_args).and_call_original + expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original + + subject + end + end + end + end + end + end + # rubocop:enable RSpec/MultipleMemoizedHelpers + + context 'with nested command in block' do + let(:skey) { "test_set" } + let(:values) { %w[{x}a {x}b {x}c] } + + before do + primary_store.set('{x}a', 1) + primary_store.set('{x}b', 2) + primary_store.set('{x}c', 3) + + secondary_store.set('{x}a', 10) + secondary_store.set('{x}b', 20) + secondary_store.set('{x}c', 30) + end + + subject do + multi_store.mget(values) do |v| + multi_store.sadd(skey, v) + multi_store.scard(skey) + v # mget receiving block returns the last line of the block for cache-hit check + end + end + + RSpec.shared_examples_for 'primary instance executes block' do + it 'ensures primary instance is executing the block' do + expect(primary_store).to receive(:send).with(:mget, values).and_call_original + expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original + expect(primary_store).to receive(:send).with(:scard, skey).and_call_original + + expect(secondary_store).not_to receive(:send).with(:mget, values).and_call_original + expect(secondary_store).not_to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original + expect(secondary_store).not_to receive(:send).with(:scard, skey).and_call_original + + subject + end + end + + context 'when using both stores' do + context 'when primary instance is default store' do + it_behaves_like 'primary instance executes block' + end + + context 'when secondary instance is default store' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + # multistore read still favours the primary store + it_behaves_like 'primary instance executes block' + end + end + + context 'when using 1 store only' do + before do + stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) + end + + context 'when primary instance is default store' do + it_behaves_like 'primary instance executes block' + end + + context 'when secondary instance is default store' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it 'ensures only secondary instance is executing the block' do + expect(secondary_store).to receive(:send).with(:mget, values).and_call_original + expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original + expect(secondary_store).to receive(:send).with(:scard, skey).and_call_original + + expect(primary_store).not_to receive(:send).with(:mget, values).and_call_original + expect(primary_store).not_to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original + expect(primary_store).not_to receive(:send).with(:scard, skey).and_call_original + + subject + end end end end @@ -316,9 +489,17 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end + # rubocop:disable RSpec/MultipleMemoizedHelpers context 'with WRITE redis commands' do + let_it_be(:ikey1) { "counter1" } + let_it_be(:ikey2) { "counter2" } + let_it_be(:iargs) { [ikey2, 3] } + let_it_be(:ivalue1) { "1" } + let_it_be(:ivalue2) { "3" } let_it_be(:key1) { "redis:{1}:key_a" } let_it_be(:key2) { "redis:{1}:key_b" } + let_it_be(:key3) { "redis:{1}:key_c" } + let_it_be(:key4) { "redis:{1}:key_d" } let_it_be(:value1) { "redis_value1" } let_it_be(:value2) { "redis_value2" } let_it_be(:key1_value1) { [key1, value1] } @@ -331,27 +512,50 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do let_it_be(:skey_value1) { [skey, [value1]] } let_it_be(:skey_value2) { [skey, [value2]] } let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) } - + let_it_be(:hkey1) { "redis:{1}:hash_a" } + let_it_be(:hkey2) { "redis:{1}:hash_b" } + let_it_be(:item) { "item" } + let_it_be(:hdelarg) { [hkey1, item] } + let_it_be(:hsetarg) { [hkey2, item, value1] } + let_it_be(:mhsetarg) { [hkey2, { "item" => value1 }] } + let_it_be(:hgetarg) { [hkey2, item] } + let_it_be(:expireargs) { [key3, ttl] } + + # rubocop:disable Layout/LineLength 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 - 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1) + '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 :unlink command' | :unlink | ref(:key3) | nil | :get | ref(:key3) + 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil + 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1) + 'execute :incr command' | :incr | ref(:ikey1) | ref(:ivalue1) | :get | ref(:ikey1) + 'execute :incrby command' | :incrby | ref(:iargs) | ref(:ivalue2) | :get | ref(:ikey2) + 'execute :hset command' | :hset | ref(:hsetarg) | ref(:value1) | :hget | ref(:hgetarg) + 'execute :hdel command' | :hdel | ref(:hdelarg) | nil | :hget | ref(:hdelarg) + 'execute :expire command' | :expire | ref(:expireargs) | ref(:ttl) | :ttl | ref(:key3) + 'execute :mapped_hmset command' | :mapped_hmset | ref(:mhsetarg) | ref(:value1) | :hget | ref(:hgetarg) end + # rubocop:enable Layout/LineLength before do primary_store.flushdb secondary_store.flushdb primary_store.set(key2, value1) + primary_store.set(key3, value1) + primary_store.set(key4, value1) primary_store.sadd?(skey, value1) + primary_store.hset(hkey2, item, value1) secondary_store.set(key2, value1) + secondary_store.set(key3, value1) + secondary_store.set(key4, value1) secondary_store.sadd?(skey, value1) + secondary_store.hset(hkey2, item, value1) end with_them do @@ -375,6 +579,34 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do include_examples 'verify that store contains values', :secondary_store end + context 'when use_primary_and_secondary_stores feature flag is disabled' do + before do + stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) + end + + context 'when using secondary store as default' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it 'executes only on secondary redis store', :aggregate_errors do + expect(secondary_store).to receive(name).with(*expected_args).and_call_original + expect(primary_store).not_to receive(name).with(*expected_args).and_call_original + + subject + end + end + + context 'when using primary store as default' do + it 'executes only on primary redis store', :aggregate_errors do + expect(primary_store).to receive(name).with(*expected_args).and_call_original + expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original + + subject + end + end + 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) @@ -419,6 +651,121 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end end + # rubocop:enable RSpec/MultipleMemoizedHelpers + + context 'with ENUMERATOR_COMMANDS redis commands' do + let_it_be(:hkey) { "redis:hash" } + let_it_be(:skey) { "redis:set" } + let_it_be(:zkey) { "redis:sortedset" } + let_it_be(:rvalue) { "value1" } + let_it_be(:scan_kwargs) { { match: 'redis:hash' } } + + where(:case_name, :name, :args, :kwargs) do + 'execute :scan_each command' | :scan_each | nil | ref(:scan_kwargs) + 'execute :sscan_each command' | :sscan_each | ref(:skey) | {} + 'execute :hscan_each command' | :hscan_each | ref(:hkey) | {} + 'execute :zscan_each command' | :zscan_each | ref(:zkey) | {} + end + + before(:all) do + primary_store.hset(hkey, rvalue, 1) + primary_store.sadd?(skey, rvalue) + primary_store.zadd(zkey, 1, rvalue) + + secondary_store.hset(hkey, rvalue, 1) + secondary_store.sadd?(skey, rvalue) + secondary_store.zadd(zkey, 1, rvalue) + end + + RSpec.shared_examples_for 'enumerator commands execution' do |both_stores, default_primary| + context 'without block passed in' do + subject do + multi_store.send(name, *args, **kwargs) + end + + it 'returns an enumerator' do + expect(subject).to be_instance_of(Enumerator) + end + end + + context 'with block passed in' do + subject do + multi_store.send(name, *args, **kwargs) { |key| multi_store.incr(rvalue) } + end + + it 'returns nil' do + expect(subject).to eq(nil) + end + + it 'runs block on correct Redis instance' do + if both_stores + expect(primary_store).to receive(name).with(*expected_args).and_call_original + expect(secondary_store).not_to receive(name) + + expect(primary_store).to receive(:incr).with(rvalue) + expect(secondary_store).to receive(:incr).with(rvalue) + elsif default_primary + expect(primary_store).to receive(name).with(*expected_args).and_call_original + expect(primary_store).to receive(:incr).with(rvalue) + + expect(secondary_store).not_to receive(name) + expect(secondary_store).not_to receive(:incr).with(rvalue) + else + expect(secondary_store).to receive(name).with(*expected_args).and_call_original + expect(secondary_store).to receive(:incr).with(rvalue) + + expect(primary_store).not_to receive(name) + expect(primary_store).not_to receive(:incr).with(rvalue) + end + + subject + end + end + end + + with_them do + describe name.to_s do + let(:expected_args) { kwargs.present? ? [*args, { **kwargs }] : Array(args) } + + before do + allow(primary_store).to receive(name).and_call_original + allow(secondary_store).to receive(name).and_call_original + end + + context 'when only using 1 store' do + before do + stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) + end + + context 'when using secondary store as default' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it_behaves_like 'enumerator commands execution', false, false + end + + context 'when using primary store as default' do + it_behaves_like 'enumerator commands execution', false, true + end + end + + context 'when using both stores' do + context 'when using secondary store as default' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it_behaves_like 'enumerator commands execution', true, false + end + + context 'when using primary store as default' do + it_behaves_like 'enumerator commands execution', true, true + end + end + end + end + end RSpec.shared_examples_for 'pipelined command' do |name| let_it_be(:key1) { "redis:{1}:key_a" } @@ -554,6 +901,34 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do end end end + + context 'when use_primary_and_secondary_stores feature flag is disabled' do + before do + stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false) + end + + context 'when using secondary store as default' do + before do + stub_feature_flags(use_primary_store_as_default_for_test_store: false) + end + + it 'executes on secondary store', :aggregate_errors do + expect(primary_store).not_to receive(:send).and_call_original + expect(secondary_store).to receive(:send).and_call_original + + subject + end + end + + context 'when using primary store as default' do + it 'executes on primary store', :aggregate_errors do + expect(secondary_store).not_to receive(:send).and_call_original + expect(primary_store).to receive(:send).and_call_original + + subject + end + end + end end end @@ -565,129 +940,211 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do include_examples 'pipelined command', :pipelined 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" } + describe '#ping' do + subject { multi_store.ping } - subject { multi_store.incr(key) } + context 'when using both stores' do + before do + allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(true) + end - it 'responds to missing method' do - expect(multi_store).to receive(:respond_to_missing?).and_call_original + context 'without message' do + it 'returns PONG' do + expect(subject).to eq('PONG') + end + end - expect(multi_store.respond_to?(:incr)).to be(true) - end + context 'with message' do + it 'returns the same message' do + expect(multi_store.ping('hello world')).to eq('hello world') + end + end - it 'executes method missing' do - expect(multi_store).to receive(:method_missing) + shared_examples 'returns an error' do + before do + allow(store).to receive(:ping).and_raise('boom') + end - subject - end + it 'returns the error' do + expect { subject }.to raise_error('boom') + end + 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, instance_name: instance_name) - ) + context 'when primary store returns an error' do + let(:store) { primary_store } - subject + it_behaves_like 'returns an error' end - it 'increments method missing counter' do - expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name) + context 'when secondary store returns an error' do + let(:store) { secondary_store } - subject + it_behaves_like 'returns an error' end end - context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do - subject { multi_store.info } + shared_examples 'single store as default store' do + context 'when the store retuns success' do + it 'returns response from the respective store' do + expect(store).to receive(:ping).and_return('PONG') - it 'does not log MethodMissingError' do - expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + subject - subject + expect(subject).to eq('PONG') + end end - it 'does not increment method missing counter' do - expect(counter).not_to receive(:increment) + context 'when the store returns an error' do + before do + allow(store).to receive(:ping).and_raise('boom') + end - subject + it 'returns the error' do + expect { subject }.to raise_error('boom') + end end end - context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do + context 'when using only one store' do before do - stub_feature_flags(use_primary_store_as_default_for_test_store: true) + allow(multi_store).to receive(:use_primary_and_secondary_stores?).and_return(false) 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) + context 'when using primary_store as default store' do + let(:store) { primary_store } - subject + before do + allow(multi_store).to receive(:use_primary_store_as_default?).and_return(true) + end + + it_behaves_like 'single store as default store' end - it 'correct value is stored on the secondary store', :aggregate_errors do - subject + context 'when using secondary_store as default store' do + let(:store) { secondary_store } - expect(secondary_store.get(key)).to be_nil - expect(primary_store.get(key)).to eq('1') + before do + allow(multi_store).to receive(:use_primary_store_as_default?).and_return(false) + end + + it_behaves_like 'single store as default store' end end + end - context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do + 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 + + subject { multi_store.command } + + context 'when in test environment' do + it 'raises error' do + expect { subject }.to raise_error(instance_of(Gitlab::Redis::MultiStore::MethodMissingError)) + end + end + + context 'when not in test environment' do before do - stub_feature_flags(use_primary_store_as_default_for_test_store: false) + stub_rails_env('production') 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) + it 'responds to missing method' do + expect(multi_store).to receive(:respond_to_missing?).and_call_original - subject + expect(multi_store.respond_to?(:command)).to be(true) end - it 'correct value is stored on the secondary store', :aggregate_errors do + 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: :command, instance_name: instance_name) + ) + + subject + end + + it 'increments method missing counter' do + expect(counter).to receive(:increment).with(command: :command, instance_name: instance_name) + + subject + end - expect(primary_store.get(key)).to be_nil - expect(secondary_store.get(key)).to eq('1') + it 'fallback and executes only on the secondary store', :aggregate_errors do + expect(primary_store).to receive(:command).and_call_original + expect(secondary_store).not_to receive(:command) + + subject + end end - end - context 'when the command is executed within pipelined block' do - subject do - multi_store.pipelined do |pipeline| - pipeline.incr(key) + 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 - it 'is executed only 1 time on each instance', :aggregate_errors do - expect(primary_store).to receive(:pipelined).once.and_call_original - expect(secondary_store).to receive(:pipelined).once.and_call_original + context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do + it 'fallback and executes only on the secondary store', :aggregate_errors do + expect(primary_store).to receive(:command).and_call_original + expect(secondary_store).not_to receive(:command) - 2.times do - expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| - expect(pipeline).to receive(:incr).with(key).once - end + subject end + end - subject + 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(:command).and_call_original + expect(primary_store).not_to receive(:command) + + subject + end end - it "both redis stores are containing correct values", :aggregate_errors do - subject + context 'when the command is executed within pipelined block' do + subject do + multi_store.pipelined(&:command) + end + + it 'is executed only 1 time on each instance', :aggregate_errors do + expect(primary_store).to receive(:pipelined).once.and_call_original + expect(secondary_store).to receive(:pipelined).once.and_call_original + + 2.times do + expect_next_instance_of(Redis::PipelinedConnection) do |pipeline| + expect(pipeline).to receive(:command).once + end + end - expect(primary_store.get(key)).to eq('1') - expect(secondary_store.get(key)).to eq('1') + subject + end end end end |