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