Commit 28ea4ecc authored by Kamil Trzciński's avatar Kamil Trzciński

Use `GITLAB_LOAD_BALANCING_REUSE_PRIMARY_*` to allow primary re-use

The purpose of this feature is to allow Phase 3 of decomposition rollout
as described in https://gitlab.com/groups/gitlab-org/-/epics/6160#progress.

The idea is that we configure a separate DB (CI) with replicas, but due
to transaction visibility we need to share primary connection to `main`
database.

This adds a temporary feature enabled via `FF` to allow this type
of behavior change.
parent 06945b48
...@@ -5,8 +5,6 @@ module Gitlab ...@@ -5,8 +5,6 @@ module Gitlab
module LoadBalancing module LoadBalancing
# Configuration settings for a single LoadBalancer instance. # Configuration settings for a single LoadBalancer instance.
class Configuration class Configuration
attr_reader :connection_specification_name, :connection_db_config
attr_accessor :hosts, :max_replication_difference, attr_accessor :hosts, :max_replication_difference,
:max_replication_lag_time, :replica_check_interval, :max_replication_lag_time, :replica_check_interval,
:service_discovery :service_discovery
...@@ -43,12 +41,12 @@ module Gitlab ...@@ -43,12 +41,12 @@ module Gitlab
end end
end end
config.reuse_primary_connection!
config config
end end
def initialize(model, hosts = []) def initialize(model, hosts = [])
@connection_specification_name = model.connection_specification_name
@connection_db_config = model.connection_db_config
@max_replication_difference = 8.megabytes @max_replication_difference = 8.megabytes
@max_replication_lag_time = 60.0 @max_replication_lag_time = 60.0
@replica_check_interval = 60.0 @replica_check_interval = 60.0
...@@ -63,6 +61,24 @@ module Gitlab ...@@ -63,6 +61,24 @@ module Gitlab
disconnect_timeout: 120, disconnect_timeout: 120,
use_tcp: false use_tcp: false
} }
# Temporary model for GITLAB_LOAD_BALANCING_REUSE_PRIMARY_
# To be removed with FF
@primary_model = nil
end
def db_config_name
@model.connection_db_config.name.to_sym
end
# With connection re-use the primary connection can be overwritten
# to be used from different model
def primary_connection_specification_name
(@primary_model || @model).connection_specification_name
end
def replica_db_config
@model.connection_db_config
end end
def pool_size def pool_size
...@@ -90,6 +106,30 @@ module Gitlab ...@@ -90,6 +106,30 @@ module Gitlab
def service_discovery_enabled? def service_discovery_enabled?
service_discovery[:record].present? service_discovery[:record].present?
end end
# TODO: This is temporary code to allow re-use of primary connection
# if the two connections are pointing to the same host. This is needed
# to properly support transaction visibility.
#
# This behavior is required to support [Phase 3](https://gitlab.com/groups/gitlab-org/-/epics/6160#progress).
# This method is meant to be removed as soon as it is finished.
#
# The remapping is done as-is:
# export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_<name-of-connection>=<new-name-of-connection>
#
# Ex.:
# export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main
#
def reuse_primary_connection!
new_connection = ENV["GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}"]
return unless new_connection.present?
@primary_model = Gitlab::Database.database_base_models[new_connection.to_sym]
unless @primary_model
raise "Invalid value for 'GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}=#{new_connection}'"
end
end
end end
end end
end end
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
REPLICA_SUFFIX = '_replica' REPLICA_SUFFIX = '_replica'
attr_reader :name, :host_list, :configuration attr_reader :host_list, :configuration
# configuration - An instance of `LoadBalancing::Configuration` that # configuration - An instance of `LoadBalancing::Configuration` that
# contains the configuration details (such as the hosts) # contains the configuration details (such as the hosts)
...@@ -26,8 +26,10 @@ module Gitlab ...@@ -26,8 +26,10 @@ module Gitlab
else else
HostList.new(configuration.hosts.map { |addr| Host.new(addr, self) }) HostList.new(configuration.hosts.map { |addr| Host.new(addr, self) })
end end
end
@name = @configuration.connection_db_config.name.to_sym def name
@configuration.db_config_name
end end
def primary_only? def primary_only?
...@@ -227,7 +229,7 @@ module Gitlab ...@@ -227,7 +229,7 @@ module Gitlab
# host - An optional host name to use instead of the default one. # host - An optional host name to use instead of the default one.
# port - An optional port to connect to. # port - An optional port to connect to.
def create_replica_connection_pool(pool_size, host = nil, port = nil) def create_replica_connection_pool(pool_size, host = nil, port = nil)
db_config = pool.db_config db_config = @configuration.replica_db_config
env_config = db_config.configuration_hash.dup env_config = db_config.configuration_hash.dup
env_config[:pool] = pool_size env_config[:pool] = pool_size
...@@ -252,7 +254,7 @@ module Gitlab ...@@ -252,7 +254,7 @@ module Gitlab
# leverage that. # leverage that.
def pool def pool
ActiveRecord::Base.connection_handler.retrieve_connection_pool( ActiveRecord::Base.connection_handler.retrieve_connection_pool(
@configuration.connection_specification_name, @configuration.primary_connection_specification_name,
role: ActiveRecord::Base.writing_role, role: ActiveRecord::Base.writing_role,
shard: ActiveRecord::Base.default_shard shard: ActiveRecord::Base.default_shard
) || raise(::ActiveRecord::ConnectionNotEstablished) ) || raise(::ActiveRecord::ConnectionNotEstablished)
......
...@@ -3,20 +3,12 @@ ...@@ -3,20 +3,12 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Configuration do RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
let(:model) do let(:configuration_hash) { {} }
config = ActiveRecord::DatabaseConfigurations::HashConfig let(:db_config) { ActiveRecord::DatabaseConfigurations::HashConfig.new('test', 'ci', configuration_hash) }
.new('main', 'test', configuration_hash) let(:model) { double(:model, connection_db_config: db_config) }
double(:model,
connection_db_config: config,
connection_specification_name: ActiveRecord::Base.connection_specification_name
)
end
describe '.for_model' do describe '.for_model' do
context 'when load balancing is not configured' do context 'when load balancing is not configured' do
let(:configuration_hash) { {} }
it 'uses the default settings' do it 'uses the default settings' do
config = described_class.for_model(model) config = described_class.for_model(model)
...@@ -108,6 +100,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do ...@@ -108,6 +100,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
expect(config.pool_size).to eq(4) expect(config.pool_size).to eq(4)
end end
end end
it 'calls reuse_primary_connection!' do
expect_next_instance_of(described_class) do |subject|
expect(subject).to receive(:reuse_primary_connection!).and_call_original
end
described_class.for_model(model)
end
end end
describe '#load_balancing_enabled?' do describe '#load_balancing_enabled?' do
...@@ -183,4 +183,58 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do ...@@ -183,4 +183,58 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
end end
end end
end end
describe '#db_config_name' do
let(:config) { described_class.new(model) }
subject { config.db_config_name }
it 'returns connection name as symbol' do
is_expected.to eq(:ci)
end
end
describe '#replica_db_config' do
let(:model) { double(:model, connection_db_config: db_config, connection_specification_name: 'Ci::CiDatabaseRecord') }
let(:config) { described_class.for_model(model) }
it 'returns exactly db_config' do
expect(config.replica_db_config).to eq(db_config)
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
it 'does not change replica_db_config' do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
expect(config.replica_db_config).to eq(db_config)
end
end
end
describe 'reuse_primary_connection!' do
let(:model) { double(:model, connection_db_config: db_config, connection_specification_name: 'Ci::CiDatabaseRecord') }
let(:config) { described_class.for_model(model) }
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_* not configured' do
it 'the primary connection uses default specification' do
expect(config.primary_connection_specification_name).to eq('Ci::CiDatabaseRecord')
end
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
it 'the primary connection uses main connection' do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
expect(config.primary_connection_specification_name).to eq('ActiveRecord::Base')
end
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=unknown' do
it 'raises exception' do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'unknown')
expect { config.reuse_primary_connection! }.to raise_error /Invalid value for/
end
end
end
end end
...@@ -4,10 +4,11 @@ require 'spec_helper' ...@@ -4,10 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
let(:conflict_error) { Class.new(RuntimeError) } let(:conflict_error) { Class.new(RuntimeError) }
let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host } let(:model) { ActiveRecord::Base }
let(:db_host) { model.connection_pool.db_config.host }
let(:config) do let(:config) do
Gitlab::Database::LoadBalancing::Configuration Gitlab::Database::LoadBalancing::Configuration
.new(ActiveRecord::Base, [db_host, db_host]) .new(model, [db_host, db_host])
end end
let(:lb) { described_class.new(config) } let(:lb) { described_class.new(config) }
...@@ -452,4 +453,51 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do ...@@ -452,4 +453,51 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect(lb.send(:get_write_location, double(select_all: []))).to be_nil expect(lb.send(:get_write_location, double(select_all: []))).to be_nil
end end
end end
describe 'primary connection re-use', :reestablished_active_record_base do
let(:model) { Ci::CiDatabaseRecord }
before do
# fake additional Database
model.establish_connection(
ActiveRecord::DatabaseConfigurations::HashConfig.new(Rails.env, 'ci', ActiveRecord::Base.connection_db_config.configuration_hash)
)
end
describe '#read' do
it 'returns ci replica connection' do
expect { |b| lb.read(&b) }.to yield_with_args do |args|
expect(args.pool.db_config.name).to eq('ci_replica')
end
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
it 'returns ci replica connection' do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
expect { |b| lb.read(&b) }.to yield_with_args do |args|
expect(args.pool.db_config.name).to eq('ci_replica')
end
end
end
end
describe '#read_write' do
it 'returns Ci::CiDatabaseRecord connection' do
expect { |b| lb.read_write(&b) }.to yield_with_args do |args|
expect(args.pool.db_config.name).to eq('ci')
end
end
context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
it 'returns ActiveRecord::Base connection' do
stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
expect { |b| lb.read_write(&b) }.to yield_with_args do |args|
expect(args.pool.db_config.name).to eq('main')
end
end
end
end
end
end end
...@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do ...@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
it 'returns the warden user if present' do it 'returns the warden user if present' do
env = { 'warden' => warden_user } env = { 'warden' => warden_user }
ids = Gitlab::Database::LoadBalancing.base_models.map do |model| ids = Gitlab::Database::LoadBalancing.base_models.map do |model|
[model, :user, 42] [model.sticking, :user, 42]
end end
expect(middleware.sticking_namespaces(env)).to eq(ids) expect(middleware.sticking_namespaces(env)).to eq(ids)
...@@ -181,9 +181,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do ...@@ -181,9 +181,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
env = { described_class::STICK_OBJECT => multiple_sticking_objects } env = { described_class::STICK_OBJECT => multiple_sticking_objects }
expect(middleware.sticking_namespaces(env)).to eq([ expect(middleware.sticking_namespaces(env)).to eq([
[ActiveRecord::Base, :user, 42], [ActiveRecord::Base.sticking, :user, 42],
[ActiveRecord::Base, :runner, '123456789'], [ActiveRecord::Base.sticking, :runner, '123456789'],
[ActiveRecord::Base, :runner, '1234'] [ActiveRecord::Base.sticking, :runner, '1234']
]) ])
end end
end end
......
...@@ -23,10 +23,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do ...@@ -23,10 +23,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
env_name: 'test', env_name: 'test',
name: 'main' name: 'main'
) )
model = double(:model, model = double(:model, connection_db_config: config)
connection_db_config: config,
connection_specification_name: ActiveRecord::Base.connection_specification_name
)
expect(ActiveRecord::DatabaseConfigurations::HashConfig) expect(ActiveRecord::DatabaseConfigurations::HashConfig)
.to receive(:new) .to receive(:new)
......
...@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do ...@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
sticking.stick_or_unstick_request(env, :user, 42) sticking.stick_or_unstick_request(env, :user, 42)
expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a) expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a)
.to eq([[ActiveRecord::Base, :user, 42]]) .to eq([[sticking, :user, 42]])
end end
it 'sticks or unsticks multiple objects and updates the Rack environment' do it 'sticks or unsticks multiple objects and updates the Rack environment' do
...@@ -42,8 +42,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do ...@@ -42,8 +42,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
sticking.stick_or_unstick_request(env, :runner, '123456789') sticking.stick_or_unstick_request(env, :runner, '123456789')
expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a).to eq([ expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a).to eq([
[ActiveRecord::Base, :user, 42], [sticking, :user, 42],
[ActiveRecord::Base, :runner, '123456789'] [sticking, :runner, '123456789']
]) ])
end 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