Commit 7120fc47 authored by nmilojevic1's avatar nmilojevic1

Improve multi store

- Improve multi store specs
- Remove sessions redis instance from build script
parent c34532e2
...@@ -3,23 +3,20 @@ ...@@ -3,23 +3,20 @@
module Gitlab module Gitlab
module Redis module Redis
class MultiStore class MultiStore
include Gitlab::Utils::StrongMemoize
class ReadFromPrimaryError < StandardError class ReadFromPrimaryError < StandardError
def message def message
'Value not found on the redis primary store. Read from the redis secondary store successful.' 'Value not found on the redis primary store. Read from the redis secondary store successful.'
end end
end end
class MultiReadError < StandardError
def message
'Value not found on both primary and secondary store.'
end
end
class MethodMissingError < StandardError class MethodMissingError < StandardError
def message def message
'Method missing. Falling back to execute method on the redis secondary store.' 'Method missing. Falling back to execute method on the redis secondary store.'
end end
end end
attr_reader :primary_store, :secondary_store attr_reader :primary_store, :secondary_store, :instance_name
FAILED_TO_READ_ERROR_MESSAGE = 'Failed to read from the redis primary_store.' 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.' FAILED_TO_WRITE_ERROR_MESSAGE = 'Failed to write to the redis primary_store.'
...@@ -42,18 +39,13 @@ module Gitlab ...@@ -42,18 +39,13 @@ module Gitlab
flushdb flushdb
).freeze ).freeze
def initialize(primary_store_options, secondary_store_options) def initialize(primary_store, secondary_store, instance_name = nil)
@primary_store = ::Redis::Store.new(primary_store_options) raise ArgumentError, 'primary_store is required' unless primary_store
@secondary_store = ::Redis::Store.new(secondary_store_options) raise ArgumentError, 'secondary_store is required' unless secondary_store
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 == ::Redis::Store
super(klass) @primary_store = primary_store
@secondary_store = secondary_store
@instance_name = instance_name
end end
READ_COMMANDS.each do |name| READ_COMMANDS.each do |name|
...@@ -76,23 +68,43 @@ module Gitlab ...@@ -76,23 +68,43 @@ module Gitlab
end end
end end
def method_missing(command_name, *args, &block) def method_missing(...)
if @instance return @instance.send(...) if @instance
send_command(@instance, command_name, *args, &block)
else
log_error(MethodMissingError.new, command_name)
increment_method_missing_count(command_name)
secondary_store.send(command_name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend log_method_missing(...)
end
secondary_store.send(...) # rubocop:disable GitlabSecurity/PublicSend
end end
def respond_to_missing?(command_name, include_private = false) def respond_to_missing?(command_name, include_private = false)
true true
end 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 == secondary_store.class
super(klass)
end
alias_method :kind_of?, :is_a?
def to_s
if multi_store_enabled?
primary_store.to_s
else
secondary_store.to_s
end
end
private private
def log_method_missing(command_name, *_args)
log_error(MethodMissingError.new, command_name)
increment_method_missing_count(command_name)
end
def read_command(command_name, *args, &block) def read_command(command_name, *args, &block)
if @instance if @instance
send_command(@instance, command_name, *args, &block) send_command(@instance, command_name, *args, &block)
...@@ -128,8 +140,6 @@ module Gitlab ...@@ -128,8 +140,6 @@ module Gitlab
if value if value
log_error(ReadFromPrimaryError.new, command_name) log_error(ReadFromPrimaryError.new, command_name)
increment_read_fallback_count(command_name) increment_read_fallback_count(command_name)
else
log_error(MultiReadError.new, command_name)
end end
value value
...@@ -147,15 +157,22 @@ module Gitlab ...@@ -147,15 +157,22 @@ module Gitlab
end end
def multi_store_enabled? def multi_store_enabled?
Feature.enabled?(:use_multi_store, default_enabled: :yaml) Feature.enabled?(:use_multi_store, default_enabled: :yaml) && !same_redis_store?
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 end
# rubocop:disable GitlabSecurity/PublicSend # rubocop:disable GitlabSecurity/PublicSend
def send_command(redis_instance, command_name, *args, &block) def send_command(redis_instance, command_name, *args, &block)
if block_given? if block_given?
# Make sure that block is wrapped and executed only on the redis instance that is executing the block # 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 |*args| redis_instance.send(command_name, *args) do |*params|
with_instance(redis_instance, *args, &block) with_instance(redis_instance, *params, &block)
end end
else else
redis_instance.send(command_name, *args) redis_instance.send(command_name, *args)
...@@ -163,28 +180,29 @@ module Gitlab ...@@ -163,28 +180,29 @@ module Gitlab
end end
# rubocop:enable GitlabSecurity/PublicSend # rubocop:enable GitlabSecurity/PublicSend
def with_instance(instance, *args) def with_instance(instance, *params)
@instance = instance @instance = instance
yield(*args)
yield(*params)
ensure ensure
@instance = nil @instance = nil
end end
def increment_read_fallback_count(command_name) 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 ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_read_fallback_total, 'Client side Redis MultiStore reading fallback')
@read_fallback_counter.increment(command: command_name) @read_fallback_counter.increment(command: command_name, instance_name: instance_name)
end end
def increment_method_missing_count(command_name) 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 ||= Gitlab::Metrics.counter(:gitlab_redis_multi_store_method_missing_total, 'Client side Redis MultiStore method missing')
@method_missing_counter.increment(command: command_name) @method_missing_counter.increment(command: command_name, innamece_name: instance_name)
end end
def log_error(exception, command_name, extra = {}) def log_error(exception, command_name, extra = {})
Gitlab::ErrorTracking.log_exception( Gitlab::ErrorTracking.log_exception(
exception, exception,
command_name: command_name, command_name: command_name,
extra: extra) extra: extra.merge(instance_name: instance_name))
end end
end end
end end
......
...@@ -38,9 +38,6 @@ sed -i 's|url:.*$|url: redis://redis:6379|g' config/cable.yml ...@@ -38,9 +38,6 @@ sed -i 's|url:.*$|url: redis://redis:6379|g' config/cable.yml
cp config/resque.yml.example config/resque.yml cp config/resque.yml.example config/resque.yml
sed -i 's|url:.*$|url: redis://redis:6379|g' config/resque.yml sed -i 's|url:.*$|url: redis://redis:6379|g' config/resque.yml
cp config/resque.yml.example config/redis.sessions.yml
sed -i 's|url:.*$|url: redis://redis:6379/15|g' config/redis.sessions.yml
if [ "$SETUP_DB" != "false" ]; then if [ "$SETUP_DB" != "false" ]; then
setup_db setup_db
elif getent hosts postgres; then elif getent hosts postgres; then
......
...@@ -5,9 +5,25 @@ require 'spec_helper' ...@@ -5,9 +5,25 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore do RSpec.describe Gitlab::Redis::MultiStore do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:multi_store) { described_class.new(Gitlab::Redis::Sessions.params.merge(serializer: nil), Gitlab::Redis::SharedState.params.merge(serializer: nil))} let_it_be(:redis_store_class) do
let_it_be(:primary_store) { multi_store.primary_store } Class.new(Gitlab::Redis::Wrapper) do
let_it_be(:secondary_store) { multi_store.secondary_store } 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) } subject { multi_store.send(name, *args) }
...@@ -16,6 +32,22 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -16,6 +32,22 @@ RSpec.describe Gitlab::Redis::MultiStore do
secondary_store.flushdb secondary_store.flushdb
end 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 'with READ redis commands' do context 'with READ redis commands' do
let_it_be(:key1) { "redis:{1}:key_a" } let_it_be(:key1) { "redis:{1}:key_a" }
let_it_be(:key2) { "redis:{1}:key_b" } let_it_be(:key2) { "redis:{1}:key_b" }
...@@ -53,7 +85,7 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -53,7 +85,7 @@ RSpec.describe Gitlab::Redis::MultiStore do
RSpec.shared_examples_for 'reads correct value' do RSpec.shared_examples_for 'reads correct value' do
it 'returns the correct value' do it 'returns the correct value' do
if value.is_a?(Array) if value.is_a?(Array)
# :smemebers does not guarantee the order it will return the values (unsorted set) # :smembers does not guarantee the order it will return the values (unsorted set)
is_expected.to match_array(value) is_expected.to match_array(value)
else else
is_expected.to eq(value) is_expected.to eq(value)
...@@ -70,7 +102,7 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -70,7 +102,7 @@ RSpec.describe Gitlab::Redis::MultiStore do
it 'logs the ReadFromPrimaryError' do it 'logs the ReadFromPrimaryError' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError), expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
hash_including(command_name: name)) hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
subject subject
end end
...@@ -83,7 +115,7 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -83,7 +115,7 @@ RSpec.describe Gitlab::Redis::MultiStore do
include_examples 'reads correct value' include_examples 'reads correct value'
context 'when fallback read from the secondary instance raises and exception' do context 'when fallback read from the secondary instance raises an exception' do
before do before do
allow(secondary_store).to receive(name).with(*args).and_raise(StandardError) allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
end end
...@@ -92,24 +124,21 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -92,24 +124,21 @@ RSpec.describe Gitlab::Redis::MultiStore do
expect { subject }.to raise_error(StandardError) expect { subject }.to raise_error(StandardError)
end end
end end
end
context 'when fallback read from the secondary instance returns no value' do RSpec.shared_examples_for 'secondary store' do
before do it 'execute on the secondary instance' do
allow(secondary_store).to receive(name).and_return(nil) expect(secondary_store).to receive(name).with(*args).and_call_original
end
it 'logs the MultiReadError error' do subject
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MultiReadError), end
hash_including(command_name: name))
subject include_examples 'reads correct value'
end
it 'does not increment read fallback count metrics' do it 'does not execute on the primary store' do
expect(multi_store).not_to receive(:increment_read_fallback_count) expect(primary_store).not_to receive(name)
subject subject
end
end end
end end
...@@ -149,7 +178,7 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -149,7 +178,7 @@ RSpec.describe Gitlab::Redis::MultiStore do
it 'logs the exception' do it 'logs the exception' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError), expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(extra: hash_including(:multi_store_error_message), hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
command_name: name)) command_name: name))
subject subject
...@@ -202,19 +231,15 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -202,19 +231,15 @@ RSpec.describe Gitlab::Redis::MultiStore do
stub_feature_flags(use_multi_store: false) stub_feature_flags(use_multi_store: false)
end end
it 'execute on the secondary instance' do it_behaves_like 'secondary store'
expect(secondary_store).to receive(name).with(*args).and_call_original end
subject
end
include_examples 'reads correct value'
it 'does not execute on the primary store' do context 'with both primary and secondary store using same redis instance' do
expect(primary_store).not_to receive(name) 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)}
subject it_behaves_like 'secondary store'
end
end end
end end
end end
...@@ -267,7 +292,7 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -267,7 +292,7 @@ RSpec.describe Gitlab::Redis::MultiStore do
redis_store = multi_store.send(store) redis_store = multi_store.send(store)
if expected_value.is_a?(Array) if expected_value.is_a?(Array)
# :smemebers does not guarantee the order it will return the values # :smembers does not guarantee the order it will return the values
expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value) expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
else else
expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value) expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
...@@ -375,7 +400,8 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -375,7 +400,8 @@ RSpec.describe Gitlab::Redis::MultiStore do
it 'logs MethodMissingError' do it 'logs MethodMissingError' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError), expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
hash_including(command_name: :incr)) hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
subject subject
end end
...@@ -421,4 +447,8 @@ RSpec.describe Gitlab::Redis::MultiStore do ...@@ -421,4 +447,8 @@ RSpec.describe Gitlab::Redis::MultiStore do
end end
end end
end end
def create_redis_store(options, extras = {})
::Redis::Store.new(options.merge(extras))
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