Commit 2c7a9c35 authored by Stan Hu's avatar Stan Hu

Merge branch '6097-turn-gitlab-geo-fdw-into-a-class' into 'master'

Geo - Turn Gitlab::Geo::Fdw into a class

Closes #6097

See merge request gitlab-org/gitlab-ee!9263
parents 7e2145e0 b9f8569a
...@@ -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
......
...@@ -2,114 +2,115 @@ ...@@ -2,114 +2,115 @@
module Gitlab module Gitlab
module Geo module Geo
module Fdw class Fdw
DEFAULT_SCHEMA = 'public'.freeze DEFAULT_SCHEMA = 'public'
FDW_SCHEMA = 'gitlab_secondary'.freeze FOREIGN_SERVER = 'gitlab_secondary'
FOREIGN_SCHEMA = 'gitlab_secondary'
# Return full table name with FDW schema
# class << self
# @param [String] table_name # Return if FDW is enabled for this instance
def self.table(table_name) #
FDW_SCHEMA + ".#{table_name}" # @return [Boolean] whether FDW is enabled
end def enabled?
return false unless fdw_capable?
# FDW is enabled by default, disable it by setting `fdw: false` in config/database_geo.yml
value = Rails.configuration.geo_database['fdw']
value.nil? ? true : value
end
# Return if FDW is enabled for this instance # Return full table name with foreign schema
# #
# @return [Boolean] whether FDW is enabled # @param [String] table_name
def self.enabled? def foreign_table_name(table_name)
return false unless fdw_capable? FOREIGN_SCHEMA + ".#{table_name}"
end
# FDW is enabled by default, disable it by setting `fdw: false` in config/database_geo.yml def foreign_tables_up_to_date?
value = Rails.configuration.geo_database['fdw'] has_foreign_schema? && foreign_schema_tables_match?
value.nil? ? true : value end
end
def self.fdw_capable? # Number of existing tables
has_foreign_schema? && connection_exist? && count_tables.positive? #
end # @return [Integer] number of tables
def foreign_schema_tables_count
Gitlab::Geo.cache_value(:geo_fdw_count_tables) do
sql = <<~SQL
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = '#{FOREIGN_SCHEMA}'
AND table_type = 'FOREIGN TABLE'
AND table_name NOT LIKE 'pg_%'
SQL
::Geo::TrackingBase.connection.execute(sql).first.fetch('count').to_i
end
end
def self.fdw_up_to_date? def gitlab_schema_tables_count
has_foreign_schema? && foreign_schema_tables_match? ActiveRecord::Schema.tables.reject { |table| table.start_with?('pg_') }.count
end end
def self.has_foreign_schema? private
Gitlab::Geo.cache_value(:geo_fdw_schema_exist) do
sql = <<~SQL
SELECT 1
FROM information_schema.schemata
WHERE schema_name='#{FDW_SCHEMA}'
SQL
::Geo::TrackingBase.connection.execute(sql).count.positive? def fdw_capable?
has_foreign_server? && has_foreign_schema? && foreign_schema_tables_count.positive?
end end
end
# Check if there is at least one FDW connection configured # Check if there is at least one foreign server configured
# #
# @return [Boolean] whether any FDW connection exists # @return [Boolean] whether any foreign server exists
def self.connection_exist? def has_foreign_server?
::Geo::TrackingBase.connection.execute( ::Geo::TrackingBase.connection.execute(
"SELECT 1 FROM pg_foreign_server" "SELECT 1 FROM pg_foreign_server"
).count.positive? ).count.positive?
end end
# Number of existing tables def has_foreign_schema?
# Gitlab::Geo.cache_value(:geo_FOREIGN_SCHEMA_exist) do
# @return [Integer] number of tables sql = <<~SQL
def self.count_tables SELECT 1
Gitlab::Geo.cache_value(:geo_fdw_count_tables) do FROM information_schema.schemata
sql = <<~SQL WHERE schema_name='#{FOREIGN_SCHEMA}'
SELECT COUNT(*) SQL
FROM information_schema.tables
WHERE table_schema = '#{FDW_SCHEMA}'
AND table_type = 'FOREIGN TABLE'
AND table_name NOT LIKE 'pg_%'
SQL
::Geo::TrackingBase.connection.execute(sql).first.fetch('count').to_i ::Geo::TrackingBase.connection.execute(sql).count.positive?
end
end end
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 self.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 self.count_tables_match? def retrieve_foreign_schema_tables
gitlab_tables.count == count_tables retrieve_schema_tables(::Geo::TrackingBase, Rails.configuration.geo_database['database'], FOREIGN_SCHEMA).to_a
end end
def self.gitlab_tables
ActiveRecord::Schema.tables.reject { |table| table.start_with?('pg_') }
end
def self.gitlab_schema def retrieve_gitlab_schema_tables
retrieve_schema_tables(ActiveRecord::Base, ActiveRecord::Base.connection_config[:database], DEFAULT_SCHEMA).to_a retrieve_schema_tables(ActiveRecord::Base, ActiveRecord::Base.connection_config[:database], DEFAULT_SCHEMA).to_a
end end
def self.fdw_schema def retrieve_schema_tables(adapter, database, schema)
retrieve_schema_tables(::Geo::TrackingBase, Rails.configuration.geo_database['database'], FDW_SCHEMA).to_a sql = <<~SQL
end SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_catalog = '#{database}'
AND table_schema = '#{schema}'
AND table_name NOT LIKE 'pg_%'
ORDER BY table_name, column_name, data_type
SQL
def self.retrieve_schema_tables(adapter, database, schema) adapter.connection.select_all(sql)
sql = <<~SQL end
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_catalog = '#{database}'
AND table_schema = '#{schema}'
AND table_name NOT LIKE 'pg_%'
ORDER BY table_name, column_name, data_type
SQL
adapter.connection.select_all(sql)
end end
private_class_method :retrieve_schema_tables
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
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
end end
context 'with fdw capable' do it 'returns false when foreign server exists but foreign schema does not exist' do
before do drop_foreign_schema
allow(described_class).to receive(:fdw_capable?).and_return(true)
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
expect(described_class.enabled?).to be_truthy
end
it 'returns false if configured in `config/database_geo.yml`' do it 'returns false when foreign server and schema exists but foreign tables are empty' do
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => false) drop_foreign_schema
create_foreign_schema
expect(described_class.enabled?).to be_falsey expect(described_class.enabled?).to eq false
end end
it 'returns true if configured in `config/database_geo.yml`' do it 'returns false when fdw is disabled in `config/database_geo.yml`' do
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => true) allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => false)
expect(described_class.enabled?).to be_truthy expect(described_class.enabled?).to be_falsey
end
end end
end
describe '.gitlab_tables' do it 'returns true when fdw is set in `config/database_geo.yml`' do
it 'excludes pg_ tables' do allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => true)
tables = described_class.gitlab_tables
ActiveRecord::Base.connection.create_table(:pg_gitlab_test) expect(described_class.enabled?).to be_truthy
end
expect(described_class.gitlab_tables).to eq(tables) it 'returns true when fdw is nil in `config/database_geo.yml`' do
allow(Rails.configuration).to receive(:geo_database).and_return('fdw' => nil)
ActiveRecord::Base.connection.drop_table(:pg_gitlab_test) expect(described_class.enabled?).to be_truthy
end end
end
describe 'fdw_capable?' do it 'returns true with a functional fdw environment' do
context 'with mocked FDW environment' do expect(described_class.enabled?).to be_truthy
it 'returns true when PostgreSQL FDW is enabled' do end
expect(described_class).to receive(:has_foreign_schema?).and_return(true) end
expect(described_class).to receive(:count_tables).and_return(1)
expect(described_class.fdw_capable?).to be_truthy
end
it 'returns false when PostgreSQL FDW is not enabled' do describe '.foreign_tables_up_to_date?' do
expect(described_class).to receive(:has_foreign_schema?).and_return(false) it 'returns false when foreign schema does not exist' do
drop_foreign_schema
expect(described_class.fdw_capable?).to be_falsey expect(described_class.foreign_tables_up_to_date?).to eq false
end end
it 'returns false when PostgreSQL FDW is enabled but remote tables are empty' do it 'returns false when foreign schema exists but tables in schema doesnt match' do
expect(described_class).to receive(:has_foreign_schema?).and_return(true) create_foreign_table(:gitlab_test)
expect(described_class).to receive(:count_tables).and_return(0)
expect(described_class.fdw_capable?).to be_falsey expect(described_class.foreign_tables_up_to_date?).to eq false
end end
it 'returns false when PostgreSQL FDW is enabled but no remote connection is defined' do it 'returns true when foreign schema exists and foreign schema has same tables as secondary database' do
expect(described_class).to receive(:has_foreign_schema?).and_return(true) expect(described_class.foreign_tables_up_to_date?).to eq true
expect(described_class).to receive(:connection_exist?).and_return(false) end
end
expect(described_class.fdw_capable?).to be_falsey describe '.foreign_schema_tables_count' do
end before do
drop_foreign_schema
create_foreign_schema
end end
context 'with functional FDW environment' do it 'returns the number of tables in the foreign schema' do
it 'returns true' do create_foreign_table(:gitlab_test)
expect(described_class.fdw_capable?).to be_truthy
end
context 'with a pg_ table' do expect(described_class.foreign_schema_tables_count).to eq(1)
before do end
ActiveRecord::Base.connection.create_table(:pg_gitlab_test)
end
after do it 'excludes tables that start with `pg_`' do
ActiveRecord::Base.connection.drop_table(:pg_gitlab_test) create_foreign_table(:pg_gitlab_test)
end
it 'returns true' do expect(described_class.foreign_schema_tables_count).to eq(0)
expect(described_class.fdw_capable?).to be_truthy
end
end
end end
end end
describe 'fdw_up_to_date?' do describe '.gitlab_schema_tables_count' do
context 'with mocked FDW environment' do it 'returns the same number of tables as defined in the database' do
it 'returns true when FDW is enabled and foreign schema has same tables as secondary database' do expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count)
expect(described_class).to receive(:has_foreign_schema?).and_return(true) end
expect(described_class).to receive(:foreign_schema_tables_match?).and_return(true)
expect(described_class.fdw_up_to_date?).to be_truthy
end
it 'returns false when FDW is enabled but tables in schema doesnt match' do
expect(described_class).to receive(:has_foreign_schema?).and_return(true)
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 ActiveRecord::Base.connection.create_table(:pg_gitlab_test)
it 'returns false when FDW is disabled' do expect(described_class.gitlab_schema_tables_count).to eq(ActiveRecord::Schema.tables.count - 1)
expect(described_class).to receive(:has_foreign_schema?).and_return(false)
expect(described_class.fdw_up_to_date?).to be_falsey ActiveRecord::Base.connection.drop_table(:pg_gitlab_test)
end
end end
end
context 'with functional FDW environment' do def with_foreign_connection
it 'returns true' do Geo::TrackingBase.connection
expect(described_class.fdw_up_to_date?).to be_truthy
end
end
end end
describe 'has_foreign_schema?' do def drop_foreign_server
context 'with functional FDW environment' do with_foreign_connection.execute <<-SQL
it 'returns true' do DROP SERVER IF EXISTS #{described_class::FOREIGN_SERVER} CASCADE
# When testing it locally, make sure you have FDW set up correctly. SQL
# If you are using GDK, you can run, from GDK root folder:
#
# make postgresql/geo-fdw/test
expect(described_class.has_foreign_schema?).to be_truthy
end
end
end end
describe 'count_tables' do def drop_foreign_schema
context 'with functional FDW environment' do with_foreign_connection.execute <<-SQL
it 'returns same amount as defined in schema migration' do DROP SCHEMA IF EXISTS #{described_class::FOREIGN_SCHEMA} CASCADE
# When testing it locally, you may need to refresh FDW with: SQL
#
# rake geo:db:test:refresh_foreign_tables
expect(described_class.count_tables).to eq(ActiveRecord::Schema.tables.count)
end
end
end end
describe 'connection_exist?' do def create_foreign_schema
context 'with functional FDW environment' do with_foreign_connection.execute <<-SQL
it 'returns true' do CREATE SCHEMA IF NOT EXISTS #{described_class::FOREIGN_SCHEMA}
# When testing it locally, make sure you have FDW set up correctly. SQL
# If you are using GDK, you can run, from GDK root folder:
# with_foreign_connection.execute <<-SQL
# make postgresql/geo-fdw/test GRANT USAGE ON FOREIGN SERVER #{described_class::FOREIGN_SERVER} TO current_user
expect(described_class.connection_exist?).to be_truthy SQL
end
end
end end
describe 'foreign_schema_tables_match?' do def create_foreign_table(table_name)
context 'with functional FDW environment' do with_foreign_connection.execute <<-SQL
it 'returns true' do CREATE FOREIGN TABLE IF NOT EXISTS #{described_class::FOREIGN_SCHEMA}.#{table_name} (
# When testing it locally, you may need to refresh FDW with: id int
# ) SERVER #{described_class::FOREIGN_SERVER}
# rake geo:db:test:refresh_foreign_tables SQL
expect(described_class.foreign_schema_tables_match?).to be_truthy
end
it 'returns true if order is different' do
one_schema = [
{ "table_name" => "events", "column_name" => "target_type", "data_type" => "character varying" },
{ "table_name" => "ci_job_artifacts", "column_name" => "id", "data_type" => "integer" }
]
second_schema = one_schema.reverse
allow(described_class).to receive(:gitlab_schema).and_return(one_schema)
allow(described_class).to receive(:fdw_schema).and_return(second_schema)
expect(described_class.foreign_schema_tables_match?).to be_truthy
end
end
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