# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing::Setup do describe '#setup' do it 'sets up the load balancer' do setup = described_class.new(ActiveRecord::Base) expect(setup).to receive(:configure_connection) expect(setup).to receive(:setup_connection_proxy) expect(setup).to receive(:setup_service_discovery) expect(setup).to receive(:setup_feature_flag_to_model_load_balancing) setup.setup end end describe '#configure_connection' do it 'configures pool, prepared statements and reconnects to the database' do config = double( :config, configuration_hash: { host: 'localhost', pool: 2, prepared_statements: true }, env_name: 'test', name: 'main' ) model = double(:model, connection_db_config: config) expect(ActiveRecord::DatabaseConfigurations::HashConfig) .to receive(:new) .with('test', 'main', { host: 'localhost', prepared_statements: false, pool: Gitlab::Database.default_pool_size }) .and_call_original # HashConfig doesn't implement its own #==, so we can't directly compare # the expected value with a pre-defined one. expect(model) .to receive(:establish_connection) .with(an_instance_of(ActiveRecord::DatabaseConfigurations::HashConfig)) described_class.new(model).configure_connection end end describe '#setup_connection_proxy' do it 'sets up the load balancer' do model = Class.new(ActiveRecord::Base) setup = described_class.new(model) config = Gitlab::Database::LoadBalancing::Configuration.new(model) lb = instance_spy(Gitlab::Database::LoadBalancing::LoadBalancer) allow(lb).to receive(:configuration).and_return(config) expect(Gitlab::Database::LoadBalancing::LoadBalancer) .to receive(:new) .with(setup.configuration) .and_return(lb) setup.setup_connection_proxy expect(model.load_balancer).to eq(lb) expect(model.sticking) .to be_an_instance_of(Gitlab::Database::LoadBalancing::Sticking) end end describe '#setup_service_discovery' do context 'when service discovery is disabled' do it 'does nothing' do expect(Gitlab::Database::LoadBalancing::ServiceDiscovery) .not_to receive(:new) described_class.new(ActiveRecord::Base).setup_service_discovery end end context 'when service discovery is enabled' do it 'immediately performs service discovery' do model = ActiveRecord::Base setup = described_class.new(model) sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery) allow(setup.configuration) .to receive(:service_discovery_enabled?) .and_return(true) allow(Gitlab::Database::LoadBalancing::ServiceDiscovery) .to receive(:new) .with(setup.load_balancer, setup.configuration.service_discovery) .and_return(sv) expect(sv).to receive(:perform_service_discovery) expect(sv).not_to receive(:start) setup.setup_service_discovery end it 'starts service discovery if needed' do model = ActiveRecord::Base setup = described_class.new(model, start_service_discovery: true) sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery) allow(setup.configuration) .to receive(:service_discovery_enabled?) .and_return(true) allow(Gitlab::Database::LoadBalancing::ServiceDiscovery) .to receive(:new) .with(setup.load_balancer, setup.configuration.service_discovery) .and_return(sv) expect(sv).to receive(:perform_service_discovery) expect(sv).to receive(:start) setup.setup_service_discovery end end end describe '#setup_feature_flag_to_model_load_balancing', :reestablished_active_record_base do using RSpec::Parameterized::TableSyntax where do { "with model LB enabled it picks a dedicated CI connection" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: false, ff_use_model_load_balancing: nil, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'ci_replica', write: 'ci' } } }, "with model LB enabled and re-use of primary connection it uses CI connection for reads" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', request_store_active: false, ff_use_model_load_balancing: nil, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'ci_replica', write: 'main' } } }, "with model LB disabled it fallbacks to use main" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: false, ff_use_model_load_balancing: nil, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'main_replica', write: 'main' } } }, "with model LB disabled, but re-use configured it fallbacks to use main" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', request_store_active: false, ff_use_model_load_balancing: nil, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'main_replica', write: 'main' } } }, "with FF disabled without RequestStore it uses main" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: false, ff_use_model_load_balancing: false, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'main_replica', write: 'main' } } }, "with FF enabled without RequestStore sticking of FF does not work, so it fallbacks to use main" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: false, ff_use_model_load_balancing: true, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'main_replica', write: 'main' } } }, "with FF disabled with RequestStore it uses main" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: true, ff_use_model_load_balancing: false, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'main_replica', write: 'main' } } }, "with FF enabled with RequestStore it sticks FF and uses CI connection" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil, request_store_active: true, ff_use_model_load_balancing: true, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'ci_replica', write: 'ci' } } }, "with re-use and FF enabled with RequestStore it sticks FF and uses CI connection for reads" => { env_GITLAB_USE_MODEL_LOAD_BALANCING: nil, env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main', request_store_active: true, ff_use_model_load_balancing: true, expectations: { main: { read: 'main_replica', write: 'main' }, ci: { read: 'ci_replica', write: 'main' } } } } end with_them do let(:ci_class) do Class.new(ActiveRecord::Base) do def self.name 'Ci::ApplicationRecordTemporary' end establish_connection ActiveRecord::DatabaseConfigurations::HashConfig.new( Rails.env, 'ci', ActiveRecord::Base.connection_db_config.configuration_hash ) end end let(:models) do { main: ActiveRecord::Base, ci: ci_class } end around do |example| if request_store_active Gitlab::WithRequestStore.with_request_store do RequestStore.clear! example.run end else example.run end end before do # Rewrite `class_attribute` to use rspec mocking and prevent modifying the objects allow_next_instance_of(described_class) do |setup| allow(setup).to receive(:configure_connection) allow(setup).to receive(:setup_class_attribute) do |attribute, value| allow(setup.model).to receive(attribute) { value } end end stub_env('GITLAB_USE_MODEL_LOAD_BALANCING', env_GITLAB_USE_MODEL_LOAD_BALANCING) stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci) stub_feature_flags(use_model_load_balancing: ff_use_model_load_balancing) # Make load balancer to force init with a dedicated replicas connections models.each do |_, model| described_class.new(model).tap do |subject| subject.configuration.hosts = [subject.configuration.replica_db_config.host] subject.setup end end end it 'results match expectations' do result = models.transform_values do |model| load_balancer = model.connection.instance_variable_get(:@load_balancer) { read: load_balancer.read { |connection| connection.pool.db_config.name }, write: load_balancer.read_write { |connection| connection.pool.db_config.name } } end expect(result).to eq(expectations) end it 'does return load_balancer assigned to a given connection' do models.each do |name, model| expect(model.load_balancer.name).to eq(name) expect(model.sticking.instance_variable_get(:@load_balancer)).to eq(model.load_balancer) end end end end end