Merge branch '54670-external-diffs-when-outdated' into 'master'
Allow external diffs to be used conditionally Closes #54670 See merge request gitlab-org/gitlab-ce!25432
This commit is contained in:
commit
1ab4d12aae
17 changed files with 563 additions and 24 deletions
|
@ -12,6 +12,10 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
# Don't display more than 100 commits at once
|
# Don't display more than 100 commits at once
|
||||||
COMMITS_SAFE_SIZE = 100
|
COMMITS_SAFE_SIZE = 100
|
||||||
|
|
||||||
|
# Applies to closed or merged MRs when determining whether to migrate their
|
||||||
|
# diffs to external storage
|
||||||
|
EXTERNAL_DIFF_CUTOFF = 7.days.freeze
|
||||||
|
|
||||||
belongs_to :merge_request
|
belongs_to :merge_request
|
||||||
|
|
||||||
manual_inverse_association :merge_request, :merge_request_diff
|
manual_inverse_association :merge_request, :merge_request_diff
|
||||||
|
@ -48,6 +52,81 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :recent, -> { order(id: :desc).limit(100) }
|
scope :recent, -> { order(id: :desc).limit(100) }
|
||||||
|
scope :files_in_database, -> { where(stored_externally: [false, nil]) }
|
||||||
|
|
||||||
|
scope :not_latest_diffs, -> do
|
||||||
|
merge_requests = MergeRequest.arel_table
|
||||||
|
mr_diffs = arel_table
|
||||||
|
|
||||||
|
join_condition = merge_requests[:id].eq(mr_diffs[:merge_request_id])
|
||||||
|
.and(mr_diffs[:id].not_eq(merge_requests[:latest_merge_request_diff_id]))
|
||||||
|
|
||||||
|
arel_join = mr_diffs.join(merge_requests).on(join_condition)
|
||||||
|
joins(arel_join.join_sources)
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :old_merged_diffs, -> (before) do
|
||||||
|
merge_requests = MergeRequest.arel_table
|
||||||
|
mr_metrics = MergeRequest::Metrics.arel_table
|
||||||
|
mr_diffs = arel_table
|
||||||
|
|
||||||
|
mr_join = mr_diffs
|
||||||
|
.join(merge_requests)
|
||||||
|
.on(mr_diffs[:merge_request_id].eq(merge_requests[:id]))
|
||||||
|
|
||||||
|
metrics_join_condition = mr_diffs[:merge_request_id]
|
||||||
|
.eq(mr_metrics[:merge_request_id])
|
||||||
|
.and(mr_metrics[:merged_at].not_eq(nil))
|
||||||
|
|
||||||
|
metrics_join = mr_diffs.join(mr_metrics).on(metrics_join_condition)
|
||||||
|
|
||||||
|
condition = MergeRequest.arel_table[:state].eq(:merged)
|
||||||
|
.and(MergeRequest::Metrics.arel_table[:merged_at].lteq(before))
|
||||||
|
.and(MergeRequest::Metrics.arel_table[:merged_at].not_eq(nil))
|
||||||
|
|
||||||
|
joins(metrics_join.join_sources, mr_join.join_sources).where(condition)
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :old_closed_diffs, -> (before) do
|
||||||
|
condition = MergeRequest.arel_table[:state].eq(:closed)
|
||||||
|
.and(MergeRequest::Metrics.arel_table[:latest_closed_at].lteq(before))
|
||||||
|
|
||||||
|
joins(merge_request: :metrics).where(condition)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.ids_for_external_storage_migration(limit:)
|
||||||
|
# No point doing any work unless the feature is enabled
|
||||||
|
return [] unless Gitlab.config.external_diffs.enabled
|
||||||
|
|
||||||
|
case Gitlab.config.external_diffs.when
|
||||||
|
when 'always'
|
||||||
|
files_in_database.limit(limit).pluck(:id)
|
||||||
|
when 'outdated'
|
||||||
|
# Outdated is too complex to be a single SQL query, so split into three
|
||||||
|
before = EXTERNAL_DIFF_CUTOFF.ago
|
||||||
|
|
||||||
|
ids = files_in_database
|
||||||
|
.old_merged_diffs(before)
|
||||||
|
.limit(limit)
|
||||||
|
.pluck(:id)
|
||||||
|
|
||||||
|
return ids if ids.size >= limit
|
||||||
|
|
||||||
|
ids += files_in_database
|
||||||
|
.old_closed_diffs(before)
|
||||||
|
.limit(limit - ids.size)
|
||||||
|
.pluck(:id)
|
||||||
|
|
||||||
|
return ids if ids.size >= limit
|
||||||
|
|
||||||
|
ids + files_in_database
|
||||||
|
.not_latest_diffs
|
||||||
|
.limit(limit - ids.size)
|
||||||
|
.pluck(:id)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
mount_uploader :external_diff, ExternalDiffUploader
|
mount_uploader :external_diff, ExternalDiffUploader
|
||||||
|
|
||||||
|
@ -55,7 +134,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
# It allows you to override variables like head_commit_sha before getting diff.
|
# It allows you to override variables like head_commit_sha before getting diff.
|
||||||
after_create :save_git_content, unless: :importing?
|
after_create :save_git_content, unless: :importing?
|
||||||
|
|
||||||
after_save :update_external_diff_store, if: :external_diff_changed?
|
after_save :update_external_diff_store, if: -> { !importing? && external_diff_changed? }
|
||||||
|
|
||||||
def self.find_by_diff_refs(diff_refs)
|
def self.find_by_diff_refs(diff_refs)
|
||||||
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
|
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
|
||||||
|
@ -294,6 +373,23 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Transactionally migrate the current merge_request_diff_files entries to
|
||||||
|
# external storage. If external storage isn't an option for this diff, the
|
||||||
|
# method is a no-op.
|
||||||
|
def migrate_files_to_external_storage!
|
||||||
|
return if stored_externally? || !use_external_diff? || merge_request_diff_files.count == 0
|
||||||
|
|
||||||
|
rows = build_merge_request_diff_files(merge_request_diff_files)
|
||||||
|
|
||||||
|
transaction do
|
||||||
|
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
|
||||||
|
create_merge_request_diff_files(rows)
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
merge_request_diff_files.reload
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def encode_in_base64?(diff_text)
|
def encode_in_base64?(diff_text)
|
||||||
|
@ -301,20 +397,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
diff_text.include?("\0")
|
diff_text.include?("\0")
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_merge_request_diff_files(diffs)
|
def build_external_merge_request_diff_files(rows)
|
||||||
rows =
|
|
||||||
if has_attribute?(:external_diff) && Gitlab.config.external_diffs.enabled
|
|
||||||
build_external_merge_request_diff_files(diffs)
|
|
||||||
else
|
|
||||||
build_merge_request_diff_files(diffs)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Faster inserts
|
|
||||||
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_external_merge_request_diff_files(diffs)
|
|
||||||
rows = build_merge_request_diff_files(diffs)
|
|
||||||
tempfile = build_external_diff_tempfile(rows)
|
tempfile = build_external_diff_tempfile(rows)
|
||||||
|
|
||||||
self.external_diff = tempfile
|
self.external_diff = tempfile
|
||||||
|
@ -325,16 +408,21 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
tempfile&.unlink
|
tempfile&.unlink
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_merge_request_diff_files(rows)
|
||||||
|
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
|
||||||
|
|
||||||
|
# Faster inserts
|
||||||
|
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
||||||
|
end
|
||||||
|
|
||||||
def build_external_diff_tempfile(rows)
|
def build_external_diff_tempfile(rows)
|
||||||
Tempfile.open(external_diff.filename) do |file|
|
Tempfile.open(external_diff.filename) do |file|
|
||||||
rows.inject(0) do |offset, row|
|
rows.each do |row|
|
||||||
data = row.delete(:diff)
|
data = row.delete(:diff)
|
||||||
row[:external_diff_offset] = offset
|
row[:external_diff_offset] = file.pos
|
||||||
row[:external_diff_size] = data.size
|
row[:external_diff_size] = data.bytesize
|
||||||
|
|
||||||
file.write(data)
|
file.write(data)
|
||||||
|
|
||||||
offset + data.size
|
|
||||||
end
|
end
|
||||||
|
|
||||||
file
|
file
|
||||||
|
@ -361,6 +449,47 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_external_diff?
|
||||||
|
return false unless has_attribute?(:external_diff)
|
||||||
|
return false unless Gitlab.config.external_diffs.enabled
|
||||||
|
|
||||||
|
case Gitlab.config.external_diffs.when
|
||||||
|
when 'always'
|
||||||
|
true
|
||||||
|
when 'outdated'
|
||||||
|
outdated_by_merge? || outdated_by_closure? || old_version?
|
||||||
|
else
|
||||||
|
false # Disable external diffs if misconfigured
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def outdated_by_merge?
|
||||||
|
return false unless merge_request&.metrics&.merged_at
|
||||||
|
|
||||||
|
merge_request.merged? && merge_request.metrics.merged_at < EXTERNAL_DIFF_CUTOFF.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
def outdated_by_closure?
|
||||||
|
return false unless merge_request&.metrics&.latest_closed_at
|
||||||
|
|
||||||
|
merge_request.closed? && merge_request.metrics.latest_closed_at < EXTERNAL_DIFF_CUTOFF.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
# We can't rely on `merge_request.latest_merge_request_diff_id` because that
|
||||||
|
# may have been changed in `save_git_content` without being reflected in
|
||||||
|
# the association's instance. This query is always subject to races, but
|
||||||
|
# the worst case is that we *don't* make a diff external when we could. The
|
||||||
|
# background worker will make it external at a later date.
|
||||||
|
def old_version?
|
||||||
|
latest_id = MergeRequest
|
||||||
|
.where(id: merge_request_id)
|
||||||
|
.limit(1)
|
||||||
|
.pluck(:latest_merge_request_diff_id)
|
||||||
|
.first
|
||||||
|
|
||||||
|
self.id != latest_id
|
||||||
|
end
|
||||||
|
|
||||||
def load_diffs(options)
|
def load_diffs(options)
|
||||||
# Ensure all diff files operate on the same external diff file instance if
|
# Ensure all diff files operate on the same external diff file instance if
|
||||||
# present. This reduces file open/close overhead.
|
# present. This reduces file open/close overhead.
|
||||||
|
@ -394,7 +523,8 @@ class MergeRequestDiff < ActiveRecord::Base
|
||||||
if diff_collection.any?
|
if diff_collection.any?
|
||||||
new_attributes[:state] = :collected
|
new_attributes[:state] = :collected
|
||||||
|
|
||||||
create_merge_request_diff_files(diff_collection)
|
rows = build_merge_request_diff_files(diff_collection)
|
||||||
|
create_merge_request_diff_files(rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set our state to 'overflow' to make the #empty? and #collected?
|
# Set our state to 'overflow' to make the #empty? and #collected?
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module MergeRequests
|
||||||
|
class MigrateExternalDiffsService < ::BaseService
|
||||||
|
MAX_JOBS = 1000.freeze
|
||||||
|
|
||||||
|
attr_reader :diff
|
||||||
|
|
||||||
|
def self.enqueue!
|
||||||
|
ids = MergeRequestDiff.ids_for_external_storage_migration(limit: MAX_JOBS)
|
||||||
|
|
||||||
|
MigrateExternalDiffsWorker.bulk_perform_async(ids.map { |id| [id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(merge_request_diff)
|
||||||
|
@diff = merge_request_diff
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
diff.migrate_files_to_external_storage!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,7 @@
|
||||||
- cronjob:trending_projects
|
- cronjob:trending_projects
|
||||||
- cronjob:issue_due_scheduler
|
- cronjob:issue_due_scheduler
|
||||||
- cronjob:prune_web_hook_logs
|
- cronjob:prune_web_hook_logs
|
||||||
|
- cronjob:schedule_migrate_external_diffs
|
||||||
|
|
||||||
- gcp_cluster:cluster_install_app
|
- gcp_cluster:cluster_install_app
|
||||||
- gcp_cluster:cluster_patch_app
|
- gcp_cluster:cluster_patch_app
|
||||||
|
@ -119,6 +120,7 @@
|
||||||
- invalid_gpg_signature_update
|
- invalid_gpg_signature_update
|
||||||
- irker
|
- irker
|
||||||
- merge
|
- merge
|
||||||
|
- migrate_external_diffs
|
||||||
- namespaceless_project_destroy
|
- namespaceless_project_destroy
|
||||||
- new_issue
|
- new_issue
|
||||||
- new_merge_request
|
- new_merge_request
|
||||||
|
|
12
app/workers/migrate_external_diffs_worker.rb
Normal file
12
app/workers/migrate_external_diffs_worker.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: false
|
||||||
|
|
||||||
|
class MigrateExternalDiffsWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
|
||||||
|
def perform(merge_request_diff_id)
|
||||||
|
diff = MergeRequestDiff.find_by_id(merge_request_diff_id)
|
||||||
|
return unless diff
|
||||||
|
|
||||||
|
MergeRequests::MigrateExternalDiffsService.new(diff).execute
|
||||||
|
end
|
||||||
|
end
|
14
app/workers/schedule_migrate_external_diffs_worker.rb
Normal file
14
app/workers/schedule_migrate_external_diffs_worker.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: false
|
||||||
|
|
||||||
|
class ScheduleMigrateExternalDiffsWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include CronjobQueue
|
||||||
|
include Gitlab::ExclusiveLeaseHelpers
|
||||||
|
|
||||||
|
def perform
|
||||||
|
in_lock(self.class.name.underscore, ttl: 2.hours, retries: 0) do
|
||||||
|
MergeRequests::MigrateExternalDiffsService.enqueue!
|
||||||
|
end
|
||||||
|
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: 'Allow external diffs to be used conditionally'
|
||||||
|
merge_request: 25432
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -301,6 +301,10 @@ production: &base
|
||||||
pages_domain_verification_cron_worker:
|
pages_domain_verification_cron_worker:
|
||||||
cron: "*/15 * * * *"
|
cron: "*/15 * * * *"
|
||||||
|
|
||||||
|
# Periodically migrate diffs from the database to external storage
|
||||||
|
schedule_migrate_external_diffs_worker:
|
||||||
|
cron: "15 * * * *"
|
||||||
|
|
||||||
registry:
|
registry:
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# host: registry.example.com
|
# host: registry.example.com
|
||||||
|
@ -787,6 +791,10 @@ test:
|
||||||
enabled: true
|
enabled: true
|
||||||
external_diffs:
|
external_diffs:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# Diffs may be `always` external (the default), or they can be made external
|
||||||
|
# after they have become `outdated` (i.e., the MR is closed or a new version
|
||||||
|
# has been pushed).
|
||||||
|
# when: always
|
||||||
# The location where external diffs are stored (default: shared/external-diffs).
|
# The location where external diffs are stored (default: shared/external-diffs).
|
||||||
# storage_path: shared/external-diffs
|
# storage_path: shared/external-diffs
|
||||||
object_store:
|
object_store:
|
||||||
|
|
|
@ -238,6 +238,7 @@ Settings.pages.admin['certificate'] ||= ''
|
||||||
#
|
#
|
||||||
Settings['external_diffs'] ||= Settingslogic.new({})
|
Settings['external_diffs'] ||= Settingslogic.new({})
|
||||||
Settings.external_diffs['enabled'] = false if Settings.external_diffs['enabled'].nil?
|
Settings.external_diffs['enabled'] = false if Settings.external_diffs['enabled'].nil?
|
||||||
|
Settings.external_diffs['when'] = 'always' if Settings.external_diffs['when'].nil?
|
||||||
Settings.external_diffs['storage_path'] = Settings.absolute(Settings.external_diffs['storage_path'] || File.join(Settings.shared['path'], 'external-diffs'))
|
Settings.external_diffs['storage_path'] = Settings.absolute(Settings.external_diffs['storage_path'] || File.join(Settings.shared['path'], 'external-diffs'))
|
||||||
Settings.external_diffs['object_store'] = ObjectStoreSettings.parse(Settings.external_diffs['object_store'])
|
Settings.external_diffs['object_store'] = ObjectStoreSettings.parse(Settings.external_diffs['object_store'])
|
||||||
|
|
||||||
|
@ -344,6 +345,10 @@ Settings.cron_jobs['prune_web_hook_logs_worker'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['prune_web_hook_logs_worker']['cron'] ||= '0 */1 * * *'
|
Settings.cron_jobs['prune_web_hook_logs_worker']['cron'] ||= '0 */1 * * *'
|
||||||
Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLogsWorker'
|
Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLogsWorker'
|
||||||
|
|
||||||
|
Settings.cron_jobs['schedule_migrate_external_diffs_worker'] ||= Settingslogic.new({})
|
||||||
|
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *'
|
||||||
|
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker'
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sidekiq
|
# Sidekiq
|
||||||
#
|
#
|
||||||
|
|
|
@ -89,3 +89,4 @@
|
||||||
- [project_daily_statistics, 1]
|
- [project_daily_statistics, 1]
|
||||||
- [import_issues_csv, 2]
|
- [import_issues_csv, 2]
|
||||||
- [chat_notification, 2]
|
- [chat_notification, 2]
|
||||||
|
- [migrate_external_diffs, 1]
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexesForMergeRequestDiffsQuery < ActiveRecord::Migration[5.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
INDEX_SPECS = [
|
||||||
|
[
|
||||||
|
:merge_request_metrics,
|
||||||
|
:latest_closed_at,
|
||||||
|
{ where: 'latest_closed_at IS NOT NULL' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
:merge_request_metrics,
|
||||||
|
[:merge_request_id, :merged_at],
|
||||||
|
{ where: 'merged_at IS NOT NULL' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
:merge_request_diffs,
|
||||||
|
[:merge_request_id, :id],
|
||||||
|
{
|
||||||
|
name: 'index_merge_request_diffs_on_merge_request_id_and_id_partial',
|
||||||
|
where: 'NOT stored_externally OR stored_externally IS NULL'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
INDEX_SPECS.each do |spec|
|
||||||
|
add_concurrent_index(*spec)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
INDEX_SPECS.reverse.each do |spec|
|
||||||
|
remove_concurrent_index(*spec)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1256,6 +1256,7 @@ ActiveRecord::Schema.define(version: 20190322132835) do
|
||||||
t.integer "external_diff_store"
|
t.integer "external_diff_store"
|
||||||
t.boolean "stored_externally"
|
t.boolean "stored_externally"
|
||||||
t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
|
t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
|
||||||
|
t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id_partial", where: "((NOT stored_externally) OR (stored_externally IS NULL))", using: :btree
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "merge_request_metrics", force: :cascade do |t|
|
create_table "merge_request_metrics", force: :cascade do |t|
|
||||||
|
@ -1271,7 +1272,9 @@ ActiveRecord::Schema.define(version: 20190322132835) do
|
||||||
t.integer "latest_closed_by_id"
|
t.integer "latest_closed_by_id"
|
||||||
t.datetime_with_timezone "latest_closed_at"
|
t.datetime_with_timezone "latest_closed_at"
|
||||||
t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
|
t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
|
||||||
|
t.index ["latest_closed_at"], name: "index_merge_request_metrics_on_latest_closed_at", where: "(latest_closed_at IS NOT NULL)", using: :btree
|
||||||
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id", using: :btree
|
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id", using: :btree
|
||||||
|
t.index ["merge_request_id", "merged_at"], name: "index_merge_request_metrics_on_merge_request_id_and_merged_at", where: "(merged_at IS NOT NULL)", using: :btree
|
||||||
t.index ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
|
t.index ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
|
||||||
t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id", using: :btree
|
t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id", using: :btree
|
||||||
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
|
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
|
||||||
|
|
|
@ -145,3 +145,47 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
|
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
|
||||||
|
|
||||||
|
### Alternative in-database storage
|
||||||
|
|
||||||
|
Enabling external diffs may reduce the performance of merge requests, as they
|
||||||
|
must be retrieved in a separate operation to other data. A compromise may be
|
||||||
|
reached by only storing outdated diffs externally, while keeping current diffs
|
||||||
|
in the database.
|
||||||
|
|
||||||
|
To enable this feature, perform the following steps:
|
||||||
|
|
||||||
|
**In Omnibus installations:**
|
||||||
|
|
||||||
|
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab_rails['external_diffs_when'] = 'outdated'
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
|
||||||
|
|
||||||
|
**In installations from source:**
|
||||||
|
|
||||||
|
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
|
||||||
|
lines:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
external_diffs:
|
||||||
|
enabled: true
|
||||||
|
when: outdated
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
|
||||||
|
|
||||||
|
With this feature enabled, diffs will initially stored in the database, rather
|
||||||
|
than externally. They will be moved to external storage once any of these
|
||||||
|
conditions become true:
|
||||||
|
|
||||||
|
- A newer version of the merge request diff exists
|
||||||
|
- The merge request was merged more than seven days ago
|
||||||
|
- The merge request was closed more than seven day ago
|
||||||
|
|
||||||
|
These rules strike a balance between space and performance by only storing
|
||||||
|
frequently-accessed diffs in the database. Diffs that are less likely to be
|
||||||
|
accessed are moved to external storage instead.
|
||||||
|
|
|
@ -46,10 +46,26 @@ FactoryBot.define do
|
||||||
target_branch "improve/awesome"
|
target_branch "improve/awesome"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :merged_last_month do
|
||||||
|
merged
|
||||||
|
|
||||||
|
after(:build) do |merge_request|
|
||||||
|
merge_request.build_metrics.merged_at = 1.month.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
trait :closed do
|
trait :closed do
|
||||||
state :closed
|
state :closed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :closed_last_month do
|
||||||
|
closed
|
||||||
|
|
||||||
|
after(:build) do |merge_request|
|
||||||
|
merge_request.build_metrics.latest_closed_at = 1.month.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
trait :opened do
|
trait :opened do
|
||||||
state :opened
|
state :opened
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,7 +51,104 @@ describe MergeRequestDiff do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#latest' do
|
describe '.ids_for_external_storage_migration' do
|
||||||
|
set(:merge_request) { create(:merge_request) }
|
||||||
|
set(:outdated) { merge_request.merge_request_diff }
|
||||||
|
set(:latest) { merge_request.create_merge_request_diff }
|
||||||
|
|
||||||
|
set(:closed_mr) { create(:merge_request, :closed_last_month) }
|
||||||
|
let(:closed) { closed_mr.merge_request_diff }
|
||||||
|
|
||||||
|
set(:merged_mr) { create(:merge_request, :merged_last_month) }
|
||||||
|
let(:merged) { merged_mr.merge_request_diff }
|
||||||
|
|
||||||
|
set(:recently_closed_mr) { create(:merge_request, :closed) }
|
||||||
|
let(:closed_recently) { recently_closed_mr.merge_request_diff }
|
||||||
|
|
||||||
|
set(:recently_merged_mr) { create(:merge_request, :merged) }
|
||||||
|
let(:merged_recently) { recently_merged_mr.merge_request_diff }
|
||||||
|
|
||||||
|
before do
|
||||||
|
merge_request.update!(latest_merge_request_diff: latest)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.ids_for_external_storage_migration(limit: 1000) }
|
||||||
|
|
||||||
|
context 'external diffs are disabled' do
|
||||||
|
before do
|
||||||
|
stub_external_diffs_setting(enabled: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'external diffs are misconfigured' do
|
||||||
|
before do
|
||||||
|
stub_external_diffs_setting(enabled: true, when: 'every second tuesday')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_empty }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'external diffs are enabled unconditionally' do
|
||||||
|
before do
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'external diffs are enabled for outdated diffs' do
|
||||||
|
before do
|
||||||
|
stub_external_diffs_setting(enabled: true, when: 'outdated')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns records for outdated merge request versions' do
|
||||||
|
is_expected.to contain_exactly(outdated.id, closed.id, merged.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with limit' do
|
||||||
|
it 'respects the limit' do
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
|
||||||
|
expect(described_class.ids_for_external_storage_migration(limit: 3).count).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#migrate_files_to_external_storage!' do
|
||||||
|
let(:diff) { create(:merge_request).merge_request_diff }
|
||||||
|
|
||||||
|
it 'converts from in-database to external storage' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
expect(diff).to receive(:save!)
|
||||||
|
|
||||||
|
diff.migrate_files_to_external_storage!
|
||||||
|
|
||||||
|
expect(diff).to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing with an external diff' do
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
|
||||||
|
expect(diff).to be_stored_externally
|
||||||
|
expect(diff).not_to receive(:save!)
|
||||||
|
|
||||||
|
diff.migrate_files_to_external_storage!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing if external diffs are disabled' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
expect(diff).not_to receive(:save!)
|
||||||
|
|
||||||
|
diff.migrate_files_to_external_storage!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#latest?' do
|
||||||
let!(:mr) { create(:merge_request, :with_diffs) }
|
let!(:mr) { create(:merge_request, :with_diffs) }
|
||||||
let!(:first_diff) { mr.merge_request_diff }
|
let!(:first_diff) { mr.merge_request_diff }
|
||||||
let!(:last_diff) { mr.create_merge_request_diff }
|
let!(:last_diff) { mr.create_merge_request_diff }
|
||||||
|
@ -222,14 +319,58 @@ describe MergeRequestDiff do
|
||||||
include_examples 'merge request diffs'
|
include_examples 'merge request diffs'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'external diffs configured' do
|
describe 'external diffs always enabled' do
|
||||||
before do
|
before do
|
||||||
stub_external_diffs_setting(enabled: true)
|
stub_external_diffs_setting(enabled: true, when: 'always')
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'merge request diffs'
|
include_examples 'merge request diffs'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'exernal diffs enabled for outdated diffs' do
|
||||||
|
before do
|
||||||
|
stub_external_diffs_setting(enabled: true, when: 'outdated')
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'merge request diffs'
|
||||||
|
|
||||||
|
it 'stores up-to-date diffs in the database' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores diffs for recently closed MRs in the database' do
|
||||||
|
mr = create(:merge_request, :closed)
|
||||||
|
|
||||||
|
expect(mr.merge_request_diff).not_to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores diffs for recently merged MRs in the database' do
|
||||||
|
mr = create(:merge_request, :merged)
|
||||||
|
|
||||||
|
expect(mr.merge_request_diff).not_to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores diffs for old MR versions in external storage' do
|
||||||
|
old_diff = diff
|
||||||
|
merge_request.create_merge_request_diff
|
||||||
|
old_diff.migrate_files_to_external_storage!
|
||||||
|
|
||||||
|
expect(old_diff).to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores diffs for old closed MRs in external storage' do
|
||||||
|
mr = create(:merge_request, :closed_last_month)
|
||||||
|
|
||||||
|
expect(mr.merge_request_diff).to be_stored_externally
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores diffs for old merged MRs in external storage' do
|
||||||
|
mr = create(:merge_request, :merged_last_month)
|
||||||
|
|
||||||
|
expect(mr.merge_request_diff).to be_stored_externally
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#commit_shas' do
|
describe '#commit_shas' do
|
||||||
it 'returns all commit SHAs using commits from the DB' do
|
it 'returns all commit SHAs using commits from the DB' do
|
||||||
expect(diff_with_commits.commit_shas).not_to be_empty
|
expect(diff_with_commits.commit_shas).not_to be_empty
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe MergeRequests::MigrateExternalDiffsService do
|
||||||
|
let(:merge_request) { create(:merge_request) }
|
||||||
|
let(:diff) { merge_request.merge_request_diff }
|
||||||
|
|
||||||
|
describe '.enqueue!', :sidekiq do
|
||||||
|
around do |example|
|
||||||
|
Sidekiq::Testing.fake! { example.run }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'enqueues nothing if external diffs are disabled' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
|
||||||
|
expect { described_class.enqueue! }
|
||||||
|
.not_to change { MigrateExternalDiffsWorker.jobs.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'enqueues eligible in-database diffs if external diffs are enabled' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
|
||||||
|
expect { described_class.enqueue! }
|
||||||
|
.to change { MigrateExternalDiffsWorker.jobs.count }
|
||||||
|
.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
it 'migrates an in-database diff to the external store' do
|
||||||
|
expect(diff).not_to be_stored_externally
|
||||||
|
|
||||||
|
stub_external_diffs_setting(enabled: true)
|
||||||
|
|
||||||
|
described_class.new(diff).execute
|
||||||
|
|
||||||
|
expect(diff).to be_stored_externally
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
spec/workers/migrate_external_diffs_worker_spec.rb
Normal file
25
spec/workers/migrate_external_diffs_worker_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe MigrateExternalDiffsWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:diff) { create(:merge_request).merge_request_diff }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'migrates the listed diff' do
|
||||||
|
expect_next_instance_of(MergeRequests::MigrateExternalDiffsService) do |instance|
|
||||||
|
expect(instance.diff).to eq(diff)
|
||||||
|
expect(instance).to receive(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
worker.perform(diff.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing if the diff is missing' do
|
||||||
|
diff.destroy
|
||||||
|
|
||||||
|
worker.perform(diff.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
spec/workers/schedule_migrate_external_diffs_worker_spec.rb
Normal file
25
spec/workers/schedule_migrate_external_diffs_worker_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ScheduleMigrateExternalDiffsWorker do
|
||||||
|
include ExclusiveLeaseHelpers
|
||||||
|
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
it 'triggers a scan for diffs to migrate' do
|
||||||
|
expect(MergeRequests::MigrateExternalDiffsService).to receive(:enqueue!)
|
||||||
|
|
||||||
|
worker.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'will not run if the lease is already taken' do
|
||||||
|
stub_exclusive_lease_taken('schedule_migrate_external_diffs_worker', timeout: 2.hours)
|
||||||
|
|
||||||
|
expect(MergeRequests::MigrateExternalDiffsService).not_to receive(:enqueue!)
|
||||||
|
|
||||||
|
worker.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue