debian-mirror-gitlab/spec/lib/gitlab/database/load_balancing/host_spec.rb
2021-11-18 22:05:49 +05:30

457 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Host do
let(:load_balancer) do
Gitlab::Database::LoadBalancing::LoadBalancer
.new(Gitlab::Database::LoadBalancing::Configuration.new(ActiveRecord::Base))
end
let(:host) do
Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
end
before do
allow(load_balancer).to receive(:create_replica_connection_pool) do
ActiveRecord::Base.connection_pool
end
end
def raise_and_wrap(wrapper, original)
raise original
rescue original.class
raise wrapper, 'boom'
end
def wrapped_exception(wrapper, original)
raise_and_wrap(wrapper, original.new)
rescue wrapper => error
error
end
describe '#connection' do
it 'returns a connection from the pool' do
expect(host.pool).to receive(:connection)
host.connection
end
end
describe '#disconnect!' do
it 'disconnects the pool' do
connection = double(:connection, in_use?: false)
pool = double(:pool, connections: [connection])
allow(host)
.to receive(:pool)
.and_return(pool)
expect(host)
.not_to receive(:sleep)
expect(host.pool)
.to receive(:disconnect!)
host.disconnect!
end
it 'disconnects the pool when waiting for connections takes too long' do
connection = double(:connection, in_use?: true)
pool = double(:pool, connections: [connection])
allow(host)
.to receive(:pool)
.and_return(pool)
expect(host.pool)
.to receive(:disconnect!)
host.disconnect!(timeout: 1)
end
end
describe '#release_connection' do
it 'releases the current connection from the pool' do
expect(host.pool).to receive(:release_connection)
host.release_connection
end
end
describe '#offline!' do
it 'marks the host as offline' do
expect(host.pool).to receive(:disconnect!)
expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
.with(hash_including(event: :host_offline))
.and_call_original
host.offline!
end
end
describe '#online?' do
context 'when the replica status is recent enough' do
before do
expect(host).to receive(:check_replica_status?).and_return(false)
end
it 'returns the latest status' do
expect(host).not_to receive(:refresh_status)
expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:warn)
expect(host).to be_online
end
it 'returns an offline status' do
host.offline!
expect(host).not_to receive(:refresh_status)
expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:warn)
expect(host).not_to be_online
end
end
context 'when the replica status is outdated' do
before do
expect(host)
.to receive(:check_replica_status?)
.and_return(true)
end
it 'refreshes the status' do
expect(Gitlab::Database::LoadBalancing::Logger).to receive(:info)
.with(hash_including(event: :host_online))
.and_call_original
expect(host).to be_online
end
context 'and replica is not up to date' do
before do
expect(host).to receive(:replica_is_up_to_date?).and_return(false)
end
it 'marks the host offline' do
expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
.with(hash_including(event: :host_offline))
.and_call_original
expect(host).not_to be_online
end
end
end
context 'when the replica is not online' do
it 'returns false when ActionView::Template::Error is raised' do
wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
allow(host)
.to receive(:check_replica_status?)
.and_raise(wrapped_error)
expect(host).not_to be_online
end
it 'returns false when ActiveRecord::StatementInvalid is raised' do
allow(host)
.to receive(:check_replica_status?)
.and_raise(ActiveRecord::StatementInvalid.new('foo'))
expect(host).not_to be_online
end
it 'returns false when PG::Error is raised' do
allow(host)
.to receive(:check_replica_status?)
.and_raise(PG::Error)
expect(host).not_to be_online
end
it 'returns false when ActiveRecord::ConnectionNotEstablished is raised' do
allow(host)
.to receive(:check_replica_status?)
.and_raise(ActiveRecord::ConnectionNotEstablished)
expect(host).not_to be_online
end
end
end
describe '#refresh_status' do
it 'refreshes the status' do
host.offline!
expect(host)
.to receive(:replica_is_up_to_date?)
.and_call_original
host.refresh_status
expect(host).to be_online
end
end
describe '#check_replica_status?' do
it 'returns true when we need to check the replica status' do
allow(host)
.to receive(:last_checked_at)
.and_return(1.year.ago)
expect(host.check_replica_status?).to eq(true)
end
it 'returns false when we do not need to check the replica status' do
freeze_time do
allow(host)
.to receive(:last_checked_at)
.and_return(Time.zone.now)
expect(host.check_replica_status?).to eq(false)
end
end
end
describe '#replica_is_up_to_date?' do
context 'when the lag time is below the threshold' do
it 'returns true' do
expect(host)
.to receive(:replication_lag_below_threshold?)
.and_return(true)
expect(host.replica_is_up_to_date?).to eq(true)
end
end
context 'when the lag time exceeds the threshold' do
before do
allow(host)
.to receive(:replication_lag_below_threshold?)
.and_return(false)
end
it 'returns true if the data is recent enough' do
expect(host)
.to receive(:data_is_recent_enough?)
.and_return(true)
expect(host.replica_is_up_to_date?).to eq(true)
end
it 'returns false when the data is not recent enough' do
expect(host)
.to receive(:data_is_recent_enough?)
.and_return(false)
expect(host.replica_is_up_to_date?).to eq(false)
end
end
end
describe '#replication_lag_below_threshold' do
it 'returns true when the lag time is below the threshold' do
expect(host)
.to receive(:replication_lag_time)
.and_return(1)
expect(host.replication_lag_below_threshold?).to eq(true)
end
it 'returns false when the lag time exceeds the threshold' do
expect(host)
.to receive(:replication_lag_time)
.and_return(9000)
expect(host.replication_lag_below_threshold?).to eq(false)
end
it 'returns false when no lag time could be calculated' do
expect(host)
.to receive(:replication_lag_time)
.and_return(nil)
expect(host.replication_lag_below_threshold?).to eq(false)
end
end
describe '#data_is_recent_enough?' do
it 'returns true when the data is recent enough' do
expect(host.data_is_recent_enough?).to eq(true)
end
it 'returns false when the data is not recent enough' do
diff = load_balancer.configuration.max_replication_difference * 2
expect(host)
.to receive(:query_and_release)
.and_return({ 'diff' => diff })
expect(host.data_is_recent_enough?).to eq(false)
end
it 'returns false when no lag size could be calculated' do
expect(host)
.to receive(:replication_lag_size)
.and_return(nil)
expect(host.data_is_recent_enough?).to eq(false)
end
end
describe '#replication_lag_time' do
it 'returns the lag time as a Float' do
expect(host.replication_lag_time).to be_an_instance_of(Float)
end
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
.and_return({})
expect(host.replication_lag_time).to be_nil
end
end
describe '#replication_lag_size' do
it 'returns the lag size as an Integer' do
expect(host.replication_lag_size).to be_an_instance_of(Integer)
end
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
.and_return({})
expect(host.replication_lag_size).to be_nil
end
it 'returns nil when the database connection fails' do
wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
allow(host)
.to receive(:connection)
.and_raise(wrapped_error)
expect(host.replication_lag_size).to be_nil
end
end
describe '#primary_write_location' do
it 'returns the write location of the primary' do
expect(host.primary_write_location).to be_an_instance_of(String)
expect(host.primary_write_location).not_to be_empty
end
end
describe '#caught_up?' do
let(:connection) { double(:connection) }
before do
allow(connection).to receive(:quote).and_return('foo')
end
it 'returns true when a host has caught up' do
allow(host).to receive(:connection).and_return(connection)
expect(connection).to receive(:select_all).and_return([{ 'result' => 't' }])
expect(host.caught_up?('foo')).to eq(true)
end
it 'returns true when a host has caught up' do
allow(host).to receive(:connection).and_return(connection)
expect(connection).to receive(:select_all).and_return([{ 'result' => true }])
expect(host.caught_up?('foo')).to eq(true)
end
it 'returns false when a host has not caught up' do
allow(host).to receive(:connection).and_return(connection)
expect(connection).to receive(:select_all).and_return([{ 'result' => 'f' }])
expect(host.caught_up?('foo')).to eq(false)
end
it 'returns false when a host has not caught up' do
allow(host).to receive(:connection).and_return(connection)
expect(connection).to receive(:select_all).and_return([{ 'result' => false }])
expect(host.caught_up?('foo')).to eq(false)
end
it 'returns false when the connection fails' do
wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
allow(host)
.to receive(:connection)
.and_raise(wrapped_error)
expect(host.caught_up?('foo')).to eq(false)
end
end
describe '#database_replica_location' do
let(:connection) { double(:connection) }
it 'returns the write ahead location of the replica', :aggregate_failures do
expect(host)
.to receive(:query_and_release)
.and_return({ 'location' => '0/D525E3A8' })
expect(host.database_replica_location).to be_an_instance_of(String)
end
it 'returns nil when the database query returned no rows' do
expect(host)
.to receive(:query_and_release)
.and_return({})
expect(host.database_replica_location).to be_nil
end
it 'returns nil when the database connection fails' do
wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
allow(host)
.to receive(:connection)
.and_raise(wrapped_error)
expect(host.database_replica_location).to be_nil
end
end
describe '#query_and_release' do
it 'executes a SQL query' do
results = host.query_and_release('SELECT 10 AS number')
expect(results).to be_an_instance_of(Hash)
expect(results['number'].to_i).to eq(10)
end
it 'releases the connection after running the query' do
expect(host)
.to receive(:release_connection)
.once
host.query_and_release('SELECT 10 AS number')
end
it 'returns an empty Hash in the event of an error' do
expect(host.connection)
.to receive(:select_all)
.and_raise(RuntimeError, 'kittens')
expect(host.query_and_release('SELECT 10 AS number')).to eq({})
end
end
describe '#host' do
it 'returns the hostname' do
expect(host.host).to eq('localhost')
end
end
end