Refactor Gitlab::Geo::Fdw class

These changes rename the method names to something more meaningful.
It also changes the spec to test all the internal details without
stubbing the private methods.
parent ee4a5f3c
...@@ -8,7 +8,7 @@ module Geo ...@@ -8,7 +8,7 @@ module Geo
STORE_COLUMN = :file_store STORE_COLUMN = :file_store
self.table_name = Gitlab::Geo::Fdw.table('ci_job_artifacts') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('ci_job_artifacts')
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :geo_syncable, -> { with_files_stored_locally.not_expired } scope :geo_syncable, -> { with_files_stored_locally.not_expired }
......
...@@ -7,7 +7,7 @@ module Geo ...@@ -7,7 +7,7 @@ module Geo
STORE_COLUMN = :file_store STORE_COLUMN = :file_store
self.table_name = Gitlab::Geo::Fdw.table('lfs_objects') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('lfs_objects')
scope :geo_syncable, -> { with_files_stored_locally } scope :geo_syncable, -> { with_files_stored_locally }
end end
......
...@@ -5,7 +5,7 @@ module Geo ...@@ -5,7 +5,7 @@ module Geo
class Project < ::Geo::BaseFdw class Project < ::Geo::BaseFdw
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
self.table_name = Gitlab::Geo::Fdw.table('projects') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('projects')
class << self class << self
# Searches for a list of projects based on the query given in `query`. # Searches for a list of projects based on the query given in `query`.
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Geo module Geo
module Fdw module Fdw
class ProjectFeature < ::Geo::BaseFdw class ProjectFeature < ::Geo::BaseFdw
self.table_name = Gitlab::Geo::Fdw.table('project_features') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('project_features')
end end
end end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Geo module Geo
module Fdw module Fdw
class ProjectRepositoryState < ::Geo::BaseFdw class ProjectRepositoryState < ::Geo::BaseFdw
self.table_name = Gitlab::Geo::Fdw.table('project_repository_states') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('project_repository_states')
end end
end end
end end
...@@ -7,7 +7,7 @@ module Geo ...@@ -7,7 +7,7 @@ module Geo
STORE_COLUMN = :store STORE_COLUMN = :store
self.table_name = Gitlab::Geo::Fdw.table('uploads') self.table_name = Gitlab::Geo::Fdw.foreign_table_name('uploads')
scope :geo_syncable, -> { with_files_stored_locally } scope :geo_syncable, -> { with_files_stored_locally }
end end
......
...@@ -3,17 +3,11 @@ ...@@ -3,17 +3,11 @@
module Gitlab module Gitlab
module Geo module Geo
class Fdw class Fdw
DEFAULT_SCHEMA = 'public'.freeze DEFAULT_SCHEMA = 'public'
FDW_SCHEMA = 'gitlab_secondary'.freeze FOREIGN_SERVER = 'gitlab_secondary'
FOREIGN_SCHEMA = 'gitlab_secondary'
class << self class << self
# Return full table name with FDW schema
#
# @param [String] table_name
def table(table_name)
FDW_SCHEMA + ".#{table_name}"
end
# Return if FDW is enabled for this instance # Return if FDW is enabled for this instance
# #
# @return [Boolean] whether FDW is enabled # @return [Boolean] whether FDW is enabled
...@@ -25,19 +19,26 @@ module Gitlab ...@@ -25,19 +19,26 @@ module Gitlab
value.nil? ? true : value value.nil? ? true : value
end end
def fdw_up_to_date? # Return full table name with foreign schema
#
# @param [String] table_name
def foreign_table_name(table_name)
FOREIGN_SCHEMA + ".#{table_name}"
end
def foreign_tables_up_to_date?
has_foreign_schema? && foreign_schema_tables_match? has_foreign_schema? && foreign_schema_tables_match?
end end
# Number of existing tables # Number of existing tables
# #
# @return [Integer] number of tables # @return [Integer] number of tables
def count_tables def foreign_schema_tables_count
Gitlab::Geo.cache_value(:geo_fdw_count_tables) do Gitlab::Geo.cache_value(:geo_fdw_count_tables) do
sql = <<~SQL sql = <<~SQL
SELECT COUNT(*) SELECT COUNT(*)
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = '#{FDW_SCHEMA}' WHERE table_schema = '#{FOREIGN_SCHEMA}'
AND table_type = 'FOREIGN TABLE' AND table_type = 'FOREIGN TABLE'
AND table_name NOT LIKE 'pg_%' AND table_name NOT LIKE 'pg_%'
SQL SQL
...@@ -46,58 +47,55 @@ module Gitlab ...@@ -46,58 +47,55 @@ module Gitlab
end end
end end
def count_tables_match? def gitlab_schema_tables_count
gitlab_tables.count == count_tables ActiveRecord::Schema.tables.reject { |table| table.start_with?('pg_') }.count
end
def gitlab_tables
ActiveRecord::Schema.tables.reject { |table| table.start_with?('pg_') }
end end
private private
def fdw_capable? def fdw_capable?
has_foreign_schema? && connection_exist? && count_tables.positive? has_foreign_server? && has_foreign_schema? && foreign_schema_tables_count.positive?
end
# Check if there is at least one foreign server configured
#
# @return [Boolean] whether any foreign server exists
def has_foreign_server?
::Geo::TrackingBase.connection.execute(
"SELECT 1 FROM pg_foreign_server"
).count.positive?
end end
def has_foreign_schema? def has_foreign_schema?
Gitlab::Geo.cache_value(:geo_fdw_schema_exist) do Gitlab::Geo.cache_value(:geo_FOREIGN_SCHEMA_exist) do
sql = <<~SQL sql = <<~SQL
SELECT 1 SELECT 1
FROM information_schema.schemata FROM information_schema.schemata
WHERE schema_name='#{FDW_SCHEMA}' WHERE schema_name='#{FOREIGN_SCHEMA}'
SQL SQL
::Geo::TrackingBase.connection.execute(sql).count.positive? ::Geo::TrackingBase.connection.execute(sql).count.positive?
end end
end end
# Check if there is at least one FDW connection configured
#
# @return [Boolean] whether any FDW connection exists
def connection_exist?
::Geo::TrackingBase.connection.execute(
"SELECT 1 FROM pg_foreign_server"
).count.positive?
end
# Check if foreign schema has exact the same tables and fields defined on secondary database # Check if foreign schema has exact the same tables and fields defined on secondary database
# #
# @return [Boolean] whether schemas match and are not empty # @return [Boolean] whether schemas match and are not empty
def foreign_schema_tables_match? def foreign_schema_tables_match?
Gitlab::Geo.cache_value(:geo_fdw_schema_tables_match) do Gitlab::Geo.cache_value(:geo_foreign_schema_tables_match) do
schema = gitlab_schema gitlab_schema_tables = retrieve_gitlab_schema_tables.to_set
foreign_schema_tables = retrieve_foreign_schema_tables.to_set
schema.present? && (schema.to_set == fdw_schema.to_set) gitlab_schema_tables.present? && (gitlab_schema_tables == foreign_schema_tables)
end end
end end
def gitlab_schema def retrieve_foreign_schema_tables
retrieve_schema_tables(ActiveRecord::Base, ActiveRecord::Base.connection_config[:database], DEFAULT_SCHEMA).to_a retrieve_schema_tables(::Geo::TrackingBase, Rails.configuration.geo_database['database'], FOREIGN_SCHEMA).to_a
end end
def fdw_schema def retrieve_gitlab_schema_tables
retrieve_schema_tables(::Geo::TrackingBase, Rails.configuration.geo_database['database'], FDW_SCHEMA).to_a retrieve_schema_tables(ActiveRecord::Base, ActiveRecord::Base.connection_config[:database], DEFAULT_SCHEMA).to_a
end end
def retrieve_schema_tables(adapter, database, schema) def retrieve_schema_tables(adapter, database, schema)
...@@ -113,7 +111,6 @@ module Gitlab ...@@ -113,7 +111,6 @@ module Gitlab
adapter.connection.select_all(sql) adapter.connection.select_all(sql)
end end
end end
end end
end end
end end
...@@ -55,7 +55,7 @@ module Gitlab ...@@ -55,7 +55,7 @@ module Gitlab
sql = <<~SQL sql = <<~SQL
SELECT count(1) SELECT count(1)
FROM pg_foreign_server FROM pg_foreign_server
WHERE srvname = '#{Gitlab::Geo::Fdw::FDW_SCHEMA}'; WHERE srvname = '#{Gitlab::Geo::Fdw::FOREIGN_SERVER}';
SQL SQL
Gitlab::Geo::DatabaseTasks.with_geo_db do Gitlab::Geo::DatabaseTasks.with_geo_db do
......
...@@ -21,11 +21,14 @@ module Gitlab ...@@ -21,11 +21,14 @@ module Gitlab
return 'The Geo database is not configured to use Foreign Data Wrapper.' unless Gitlab::Geo::Fdw.enabled? return 'The Geo database is not configured to use Foreign Data Wrapper.' unless Gitlab::Geo::Fdw.enabled?
unless Gitlab::Geo::Fdw.fdw_up_to_date? unless Gitlab::Geo::Fdw.foreign_tables_up_to_date?
output = "The Geo database has an outdated FDW remote schema." output = "The Geo database has an outdated FDW remote schema."
unless Gitlab::Geo::Fdw.count_tables_match? foreign_schema_tables_count = Gitlab::Geo::Fdw.foreign_schema_tables_count
output = "#{output} It contains #{Gitlab::Geo::Fdw.count_tables} of #{Gitlab::Geo::Fdw.gitlab_tables.count} expected tables." gitlab_schema_tables_count = Gitlab::Geo::Fdw.gitlab_schema_tables_count
unless gitlab_schema_tables_count == foreign_schema_tables_count
output = "#{output} It contains #{foreign_schema_tables_count} of #{gitlab_schema_tables_count} expected tables."
end end
return output return output
......
...@@ -25,7 +25,7 @@ module SystemCheck ...@@ -25,7 +25,7 @@ module SystemCheck
end end
def check? def check?
Gitlab::Geo::Fdw.fdw_up_to_date? Gitlab::Geo::Fdw.foreign_tables_up_to_date?
end end
def show_error def show_error
......
...@@ -58,7 +58,7 @@ namespace :geo do ...@@ -58,7 +58,7 @@ namespace :geo do
desc 'Refresh Foreign Tables definition in Geo Secondary node' desc 'Refresh Foreign Tables definition in Geo Secondary node'
task refresh_foreign_tables: [:environment] do task refresh_foreign_tables: [:environment] do
if Gitlab::Geo::GeoTasks.foreign_server_configured? if Gitlab::Geo::GeoTasks.foreign_server_configured?
print "\nRefreshing foreign tables for FDW: #{Gitlab::Geo::Fdw::FDW_SCHEMA} ... " print "\nRefreshing foreign tables for FDW: #{Gitlab::Geo::Fdw::FOREIGN_SCHEMA} ... "
Gitlab::Geo::GeoTasks.refresh_foreign_tables! Gitlab::Geo::GeoTasks.refresh_foreign_tables!
puts 'Done!' puts 'Done!'
else else
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Geo::Fdw, :geo do describe Gitlab::Geo::Fdw, :geo do
include ::EE::GeoHelpers describe '.enabled?' do
it 'returns false when foreign server does not exist' do
drop_foreign_server
describe 'enabled?' do expect(described_class.enabled?).to eq false
it 'returns false when PostgreSQL FDW is not enabled' do end
expect(described_class).to receive(:count_tables).and_return(0)
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => true)
expect(described_class.enabled?).to be_falsey it 'returns false when foreign server exists but foreign schema does not exist' do
drop_foreign_schema
expect(described_class.enabled?).to eq false
end end
context 'with fdw capable' do it 'returns false when foreign server and schema exists but foreign tables are empty' do
before do drop_foreign_schema
allow(described_class).to receive(:fdw_capable?).and_return(true) create_foreign_schema
end
it 'returns true by default' do expect(described_class.enabled?).to eq false
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => nil) end
it 'returns false when fdw is disabled in `config/database_geo.yml`' do
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => false)
expect(described_class.enabled?).to be_falsey
end
expect(described_class.enabled?).to be_truthy it 'returns true when fdw is set in `config/database_geo.yml`' do
end allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => true)
it 'returns false if disabled in `config/database_geo.yml`' do expect(described_class.enabled?).to be_truthy
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => false) end
expect(described_class.enabled?).to be_falsey it 'returns true when fdw is nil in `config/database_geo.yml`' do
end allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => nil)
it 'returns true if configured in `config/database_geo.yml`' do expect(described_class.enabled?).to be_truthy
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => true) end
expect(described_class.enabled?).to be_truthy it 'returns true with a functional fdw environment' do
end expect(described_class.enabled?).to be_truthy
end end
end end
describe '.gitlab_tables' do describe '.foreign_tables_up_to_date?' do
it 'excludes pg_ tables' do it 'returns false when foreign schema does not exist' do
ActiveRecord::Base.connection.create_table(:pg_gitlab_test) drop_foreign_schema
expect(described_class.gitlab_tables).not_to include('pg_gitlab_test') expect(described_class.foreign_tables_up_to_date?).to eq false
end
ActiveRecord::Base.connection.drop_table(:pg_gitlab_test) it 'returns false when foreign schema exists but tables in schema doesnt match' do
create_foreign_table(:gitlab_test)
expect(described_class.foreign_tables_up_to_date?).to eq false
end
it 'returns true when foreign schema exists and foreign schema has same tables as secondary database' do
expect(described_class.foreign_tables_up_to_date?).to eq true
end end
end end
describe 'fdw_up_to_date?' do describe '.foreign_schema_tables_count' do
context 'with mocked FDW environment' do before do
it 'returns true when FDW is enabled and foreign schema has same tables as secondary database' do drop_foreign_schema
expect(described_class).to receive(:has_foreign_schema?).and_return(true) create_foreign_schema
expect(described_class).to receive(:foreign_schema_tables_match?).and_return(true) end
expect(described_class.fdw_up_to_date?).to be_truthy it 'returns the number of tables in the foreign schema' do
end create_foreign_table(:gitlab_test)
it 'returns false when FDW is enabled but tables in schema doesnt match' do expect(described_class.foreign_schema_tables_count).to eq(1)
expect(described_class).to receive(:has_foreign_schema?).and_return(true) end
expect(described_class).to receive(:foreign_schema_tables_match?).and_return(false)
expect(described_class.fdw_up_to_date?).to be_falsey it 'excludes tables that start with `pg_`' do
end create_foreign_table(:pg_gitlab_test)
it 'returns false when FDW is disabled' do expect(described_class.foreign_schema_tables_count).to eq(0)
expect(described_class).to receive(:has_foreign_schema?).and_return(false) end
end
expect(described_class.fdw_up_to_date?).to be_falsey describe '.gitlab_schema_tables_count' do
end it 'returns the same number of tables as defined in the database' do
expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count)
end end
context 'with functional FDW environment' do it 'excludes tables that start with `pg_`' do
it 'returns true' do ActiveRecord::Base.connection.create_table(:pg_gitlab_test)
expect(described_class.fdw_up_to_date?).to be_truthy
end expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count - 1)
ActiveRecord::Base.connection.drop_table(:pg_gitlab_test)
end end
end end
describe 'count_tables' do def with_foreign_connection
context 'with functional FDW environment' do Geo::TrackingBase.connection
it 'returns same amount as defined in schema migration' do end
# When testing it locally, you may need to refresh FDW with:
# def drop_foreign_server
# rake geo:db:test:refresh_foreign_tables with_foreign_connection.execute <<-SQL
expect(described_class.count_tables).to eq(ActiveRecord::Schema.tables.count) DROP SERVER IF EXISTS #{described_class::FOREIGN_SERVER} CASCADE
end SQL
end end
def drop_foreign_schema
with_foreign_connection.execute <<-SQL
DROP SCHEMA IF EXISTS #{described_class::FOREIGN_SCHEMA} CASCADE
SQL
end
def create_foreign_schema
with_foreign_connection.execute <<-SQL
CREATE SCHEMA IF NOT EXISTS #{described_class::FOREIGN_SCHEMA}
SQL
with_foreign_connection.execute <<-SQL
GRANT USAGE ON FOREIGN SERVER #{described_class::FOREIGN_SERVER} TO current_user
SQL
end
def create_foreign_table(table_name)
with_foreign_connection.execute <<-SQL
CREATE FOREIGN TABLE IF NOT EXISTS #{described_class::FOREIGN_SCHEMA}.#{table_name} (
id int
) SERVER #{described_class::FOREIGN_SERVER}
SQL
end end
end end
...@@ -96,8 +96,9 @@ describe Gitlab::Geo::HealthCheck, :geo do ...@@ -96,8 +96,9 @@ describe Gitlab::Geo::HealthCheck, :geo do
allow(described_class).to receive(:database_secondary?) { true } allow(described_class).to receive(:database_secondary?) { true }
allow(described_class).to receive(:streaming_active?) { true } allow(described_class).to receive(:streaming_active?) { true }
allow(Gitlab::Geo::Fdw).to receive(:fdw_up_to_date?) { false } allow(Gitlab::Geo::Fdw).to receive(:foreign_tables_up_to_date?) { false }
allow(Gitlab::Geo::Fdw).to receive(:count_tables_match?) { true } allow(Gitlab::Geo::Fdw).to receive(:foreign_schema_tables_count) { 1 }
allow(Gitlab::Geo::Fdw).to receive(:gitlab_schema_tables_count) { 1 }
expect(subject.perform_checks).to match(/The Geo database has an outdated FDW remote schema\./) expect(subject.perform_checks).to match(/The Geo database has an outdated FDW remote schema\./)
end end
...@@ -106,8 +107,9 @@ describe Gitlab::Geo::HealthCheck, :geo do ...@@ -106,8 +107,9 @@ describe Gitlab::Geo::HealthCheck, :geo do
allow(described_class).to receive(:database_secondary?) { true } allow(described_class).to receive(:database_secondary?) { true }
allow(described_class).to receive(:streaming_active?) { true } allow(described_class).to receive(:streaming_active?) { true }
allow(Gitlab::Geo::Fdw).to receive(:fdw_up_to_date?) { false } allow(Gitlab::Geo::Fdw).to receive(:foreign_tables_up_to_date?) { false }
allow(Gitlab::Geo::Fdw).to receive(:count_tables_match?) { false } allow(Gitlab::Geo::Fdw).to receive(:foreign_schema_tables_count) { 1 }
allow(Gitlab::Geo::Fdw).to receive(:gitlab_schema_tables_count) { 2 }
expect(subject.perform_checks).to match(/The Geo database has an outdated FDW remote schema\. It contains [0-9]+ of [0-9]+ expected tables/) expect(subject.perform_checks).to match(/The Geo database has an outdated FDW remote schema\. It contains [0-9]+ of [0-9]+ expected tables/)
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