Commit 8d1e97fc authored by Kamil Trzciński's avatar Kamil Trzciński

Optimise import performance

- Fix `O(n)` complexity of `append_or_update_attribute`,
  we append objects to an array and re-save project
- Remove the usage of `keys.include?` as it performs `O(n)`
  search, instead use `.has_key?`
- Remove the usage of `.keys.first` as it performs a copy
  of all keys, instead use `.first.first`
parent 5e102f17
......@@ -1854,16 +1854,24 @@ class Project < ApplicationRecord
end
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
if Project.reflect_on_association(name).try(:macro) == :has_many
# if this is 1-to-N relation, update the parent object
value.each do |item|
item.update!(
Project.reflect_on_association(name).foreign_key => id)
end
# force to drop relation cache
public_send(name).reset # rubocop:disable GitlabSecurity/PublicSend
if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
update_attribute(name, old_values + value)
# succeeded
true
else
# if this is another relation or attribute, update just object
update_attribute(name, value)
end
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
rescue ActiveRecord::RecordInvalid => e
raise e, "Failed to set #{name}: #{e.message}"
end
# Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
......@@ -2252,18 +2260,6 @@ class Project < ApplicationRecord
ContainerRepository.build_root_repository(self).has_tags?
end
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
end
def fetch_branch_allows_collaboration(user, branch_name = nil)
return false unless user
......
---
title: Optimise import performance
merge_request: 31045
author:
type: performance
......@@ -45,7 +45,7 @@ module Gitlab
end
def key_from_hash(value)
value.is_a?(Hash) ? value.keys.first : value
value.is_a?(Hash) ? value.first.first : value
end
end
end
......
......@@ -27,7 +27,7 @@ module Gitlab
# {:merge_requests=>[:merge_request_diff, :notes]}
def process_model_objects(model_object_hash)
json_config_hash = {}
current_key = model_object_hash.keys.first
current_key = model_object_hash.first.first
model_object_hash.values.flatten.each do |model_object|
@attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash }
......
......@@ -35,7 +35,7 @@ module Gitlab
end
def include?(old_author_id)
map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
map.has_key?(old_author_id) && map[old_author_id] != default_user_id
end
private
......
......@@ -185,7 +185,7 @@ module Gitlab
return unless EXISTING_OBJECT_CHECK.include?(@relation_name)
return unless @relation_hash['group_id']
@relation_hash['group_id'] = @project.group&.id
@relation_hash['group_id'] = @project.namespace_id
end
def reset_tokens!
......
......@@ -13,7 +13,7 @@ describe UserCalloutsController do
subject { post :create, params: { feature_name: feature_name }, format: :json }
context 'with valid feature name' do
let(:feature_name) { UserCallout.feature_names.keys.first }
let(:feature_name) { UserCallout.feature_names.first.first }
context 'when callout entry does not exist' do
it 'creates a callout entry with dismissed state' do
......@@ -28,7 +28,7 @@ describe UserCalloutsController do
end
context 'when callout entry already exists' do
let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) }
let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.first.first, user: user) }
it 'returns success' do
subject
......
......@@ -19,7 +19,7 @@
"labels": [
{
"id": 2,
"title": "project label",
"title": "A project label",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
......@@ -105,7 +105,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
"title": "project label",
"title": "A project label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
......@@ -162,7 +162,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 2,
"title": "project label",
"title": "A project label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
......
......@@ -272,7 +272,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'has label priorities' do
expect(project.labels.first.priorities).not_to be_empty
expect(project.labels.find_by(title: 'A project label').priorities).not_to be_empty
end
it 'has milestones' do
......@@ -325,7 +325,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 1,
labels: 1,
labels: 2,
milestones: 1,
first_issue_labels: 1,
services: 1
......@@ -402,7 +402,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project successfully'
it_behaves_like 'restores project correctly',
issues: 2,
labels: 1,
labels: 2,
milestones: 2,
first_issue_labels: 1
......
......@@ -3097,11 +3097,8 @@ describe Project do
let(:project) { create(:project) }
it 'shows full error updating an invalid MR' do
error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\
' Validate fork Source project is not a fork of the target project'
expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }
.to raise_error(ActiveRecord::RecordNotSaved, error_message)
.to raise_error(ActiveRecord::RecordInvalid, /Failed to set merge_requests:/)
end
it 'updates the project successfully' do
......
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