diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index ef2e380ea950a1e43ad71f7d5ecb7457b1c3693f..d81f3c5004e5741b7d1977a347b046ebc13e993b 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -920,7 +920,6 @@ Layout/LineLength: - 'ee/app/models/ee/upload.rb' - 'ee/app/models/ee/user.rb' - 'ee/app/models/ee/vulnerability.rb' - - 'ee/app/models/elastic/migration_record.rb' - 'ee/app/models/elastic/reindexing_slice.rb' - 'ee/app/models/epic_issue.rb' - 'ee/app/models/geo/project_registry.rb' diff --git a/.rubocop_todo/style/missing_respond_to_missing.yml b/.rubocop_todo/style/missing_respond_to_missing.yml index 9387ae72d0011e1273a3731c55139f7acb5b3a3f..4c61c7fc1a662ecf09c1f02af832cfea6400327b 100644 --- a/.rubocop_todo/style/missing_respond_to_missing.yml +++ b/.rubocop_todo/style/missing_respond_to_missing.yml @@ -5,7 +5,6 @@ Style/MissingRespondToMissing: - 'app/models/network/commit.rb' - 'app/services/notification_service.rb' - 'ee/app/controllers/ee/groups/application_controller.rb' - - 'ee/app/models/elastic/migration_record.rb' - 'ee/app/services/ee/audit_event_service.rb' - 'lib/declarative_enum.rb' - 'lib/gitlab/auth/ldap/dn.rb' diff --git a/ee/app/models/elastic/migration_record.rb b/ee/app/models/elastic/migration_record.rb index ea9984590266e7e1043520873ffdf7965ab85961..7e9bc63efefe15802ae35a86b34e13c8ebce5d58 100644 --- a/ee/app/models/elastic/migration_record.rb +++ b/ee/app/models/elastic/migration_record.rb @@ -20,13 +20,18 @@ def save!(completed:) data = { completed: completed, state: load_state, name: name }.merge(timestamps(completed: completed)) - client.index index: index_name, type: '_doc', id: version, body: data + client.index index: index_name, refresh: true, type: '_doc', id: version, body: data end def save_state!(state) - completed = load_completed_from_index + source = load_from_index&.dig('_source')&.with_indifferent_access || {} + current_state = source['state']&.with_indifferent_access || {} + completed = source['completed'] - client.index index: index_name, refresh: true, type: '_doc', id: version, body: { completed: completed, state: load_state.merge(state) } + source.delete(:state) + body = source.merge(state: current_state.merge(state), completed: completed) + + client.index index: index_name, refresh: true, type: '_doc', id: version, body: body end def started? @@ -107,12 +112,18 @@ def method_missing(method, *args, &block) end end + def respond_to_missing?(method, include_private = false) + migration.respond_to?(method) || super + end + def self.load_versions(completed:) helper = Gitlab::Elastic::Helper.default + body = { query: { term: { completed: completed } }, size: ELASTICSEARCH_SIZE } helper.client - .search(index: helper.migrations_index_name, body: { query: { term: { completed: completed } }, size: ELASTICSEARCH_SIZE }) + .search(index: helper.migrations_index_name, body: body) .dig('hits', 'hits') .map { |v| v['_id'].to_i } + .sort end def self.completed_versions diff --git a/ee/lib/gitlab/elastic/helper.rb b/ee/lib/gitlab/elastic/helper.rb index 0a8847369c1ae6bba6d75190ef36dbae81e6d4dd..affd57ced6280bbf57a469967c81fa9ab92dc870 100644 --- a/ee/lib/gitlab/elastic/helper.rb +++ b/ee/lib/gitlab/elastic/helper.rb @@ -114,6 +114,9 @@ def create_migrations_index }, completed_at: { type: 'date' + }, + name: { + type: 'keyword' } } } diff --git a/ee/spec/models/elastic/migration_record_spec.rb b/ee/spec/models/elastic/migration_record_spec.rb index b0c7955c48c7beafcb1f2db42f00667dcef367db..3648455271f45d793f46e39ca5d30a7384be19c9 100644 --- a/ee/spec/models/elastic/migration_record_spec.rb +++ b/ee/spec/models/elastic/migration_record_spec.rb @@ -2,14 +2,22 @@ require 'spec_helper' -RSpec.describe Elastic::MigrationRecord, :elastic_clean, feature_category: :global_search do +RSpec.describe Elastic::MigrationRecord, :elastic_delete_by_query, feature_category: :global_search do using RSpec::Parameterized::TableSyntax + let_it_be(:helper) { Gitlab::Elastic::Helper.default } + + let(:migration) { Class.new } let(:record) { described_class.new(version: Time.now.to_i, name: 'ExampleMigration', filename: nil) } + before do + allow(record).to receive(:load_migration).and_return(migration) + allow(Gitlab::Elastic::Helper).to receive(:default).and_return(helper) + end + describe '#save!' do - it 'creates an index if it is not found' do - es_helper.delete_migrations_index + it 'raises an error if the migrations index is not found' do + allow(helper).to receive(:index_exists?).with(index_name: 'gitlab-test-migrations').and_return(false) expect { record.save!(completed: true) }.to raise_error(/index is not found/) end @@ -51,13 +59,13 @@ describe '#load_from_index' do it 'does not raise an exception when connection refused' do - allow(Gitlab::Elastic::Helper.default).to receive(:get).and_raise(Faraday::ConnectionFailed) + allow(helper.client).to receive(:get).and_raise(Faraday::ConnectionFailed) expect(record.load_from_index).to be_nil end it 'does not raise an exception when record does not exist' do - allow(Gitlab::Elastic::Helper.default).to receive(:get).and_raise(Elasticsearch::Transport::Transport::Errors::NotFound) + allow(helper.client).to receive(:get).and_raise(Elasticsearch::Transport::Transport::Errors::NotFound) expect(record.load_from_index).to be_nil end @@ -88,7 +96,7 @@ end describe '#started?' do - it 'changes on object save' do + it 'changes on first save to the index' do expect { record.save!(completed: true) }.to change { record.started? }.from(false).to(true) end end @@ -98,12 +106,12 @@ let(:in_progress_migration) { described_class.new(version: 10, name: 10, filename: nil) } before do - es_helper.delete_migrations_index - es_helper.create_migrations_index + helper.delete_migrations_index + helper.create_migrations_index completed_versions.each { |migration| migration.save!(completed: true) } in_progress_migration.save!(completed: false) - es_helper.refresh_index(index_name: es_helper.migrations_index_name) + helper.refresh_index(index_name: helper.migrations_index_name) end it 'loads all records' do @@ -112,7 +120,7 @@ end it 'raises an exception if no index present' do - es_helper.delete_migrations_index + allow(Gitlab::Elastic::Helper.default.client).to receive(:search).and_raise(Elasticsearch::Transport::Transport::Errors::NotFound) expect { described_class.load_versions(completed: true) }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) expect { described_class.load_versions(completed: false) }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) @@ -276,4 +284,42 @@ it { is_expected.to eq(true) } end end + + describe '#save_state!' do + it 'only modifies the state field' do + record.save!(completed: false) + + original_source = record.load_from_index['_source'] + + record.save_state!({ projects: [1], remaining_documents: 500 }) + + source = record.load_from_index['_source'] + expected_state = original_source['state'].merge({ 'projects' => [1], 'remaining_documents' => 500 }) + + expect(source['state']).to eq(expected_state) + + source.delete('state') + source.each_key do |key| + expect(source[key]).to eq(original_source[key]) + end + end + + it 'only overwrites the state fields provided to the method' do + record.save_state!({ remaining_documents: 5_000, halted: false }) + + original_source = record.load_from_index['_source'] + + record.save_state!({ projects: [1], remaining_documents: 100 }) + + source = record.load_from_index['_source'] + expected_state = { 'halted' => false, 'projects' => [1], 'remaining_documents' => 100 } + + expect(source['state']).to eq(expected_state) + + source.delete('state') + source.each_key do |key| + expect(source[key]).to eq(original_source[key]) + end + end + end end