Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-20 06:08:37 +00:00
parent 93c966ae2a
commit 1ad2f1981f
13 changed files with 222 additions and 21 deletions

View file

@ -111,7 +111,7 @@ module Spammable
end end
# Override in Spammable if further checks are necessary # Override in Spammable if further checks are necessary
def check_for_spam? def check_for_spam?(user:)
true true
end end

View file

@ -437,10 +437,10 @@ class Issue < ApplicationRecord
user, project.external_authorization_classification_label) user, project.external_authorization_classification_label)
end end
def check_for_spam? def check_for_spam?(user:)
# content created via support bots is always checked for spam, EVEN if # content created via support bots is always checked for spam, EVEN if
# the issue is not publicly visible and/or confidential # the issue is not publicly visible and/or confidential
return true if author.support_bot? && spammable_attribute_changed? return true if user.support_bot? && spammable_attribute_changed?
# Only check for spam on issues which are publicly visible (and thus indexed in search engines) # Only check for spam on issues which are publicly visible (and thus indexed in search engines)
return false unless publicly_visible? return false unless publicly_visible?

View file

@ -246,7 +246,7 @@ class Snippet < ApplicationRecord
notes.includes(:author) notes.includes(:author)
end end
def check_for_spam? def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) || visibility_level_changed?(to: Snippet::PUBLIC) ||
(public? && (title_changed? || content_changed?)) (public? && (title_changed? || content_changed?))
end end

View file

@ -28,7 +28,7 @@ module Spam
ServiceResponse.success(message: "CAPTCHA successfully verified") ServiceResponse.success(message: "CAPTCHA successfully verified")
else else
return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user) return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user)
return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam? return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?(user: user)
perform_spam_service_check perform_spam_service_check
ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement") ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement")
@ -94,7 +94,7 @@ module Spam
def create_spam_log def create_spam_log
@spam_log = SpamLog.create!( @spam_log = SpamLog.create!(
{ {
user_id: target.author_id, user_id: user.id,
title: target.spam_title, title: target.spam_title,
description: target.spam_description, description: target.spam_description,
source_ip: spam_params.ip_address, source_ip: spam_params.ip_address,

View file

@ -174,13 +174,18 @@ roughly be as follows:
1. Release B: 1. Release B:
1. Deploy code so that the application starts using the new column and stops 1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data. scheduling jobs for newly created data.
1. In a post-deployment migration you'll need to ensure no jobs remain. 1. In a post-deployment migration use `finalize_background_migration` from
`BackgroundMigrationHelpers` to ensure no jobs remain. This helper will:
1. Use `Gitlab::BackgroundMigration.steal` to process any remaining 1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs in Sidekiq. jobs in Sidekiq.
1. Reschedule the migration to be run directly (i.e. not through Sidekiq) 1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
on any rows that weren't migrated by Sidekiq. This can happen if, for on any rows that weren't migrated by Sidekiq. This can happen if, for
instance, Sidekiq received a SIGKILL, or if a particular batch failed instance, Sidekiq received a SIGKILL, or if a particular batch failed
enough times to be marked as dead. enough times to be marked as dead.
1. Remove `Gitlab::Database::BackgroundMigrationJob` rows where
`status = succeeded`. To retain diagnostic information that may
help with future bug tracking you can skip this step by specifying
the `delete_tracking_jobs: false` parameter.
1. Remove the old column. 1. Remove the old column.
This may also require a bump to the [import/export version](../user/project/settings/import_export.md), if This may also require a bump to the [import/export version](../user/project/settings/import_export.md), if

View file

@ -264,6 +264,34 @@ module Gitlab
migration migration
end end
# Force a background migration to complete.
#
# WARNING: This method will block the caller and move the background migration from an
# asynchronous migration to a synchronous migration.
#
# 1. Steal work from sidekiq and perform immediately (avoid duplicates generated by step 2).
# 2. Process any pending tracked jobs.
# 3. Steal work from sidekiq and perform immediately (clear anything left from step 2).
# 4. Optionally remove job tracking information.
#
# This method does not garauntee that all jobs completed successfully.
def finalize_background_migration(class_name, delete_tracking_jobs: ['succeeded'])
# Empty the sidekiq queue.
Gitlab::BackgroundMigration.steal(class_name)
# Process pending tracked jobs.
jobs = Gitlab::Database::BackgroundMigrationJob.pending.for_migration_class(class_name)
jobs.find_each do |job|
BackgroundMigrationWorker.new.perform(job.class_name, job.arguments)
end
# Empty the sidekiq queue.
Gitlab::BackgroundMigration.steal(class_name)
# Delete job tracking rows.
delete_job_tracking(class_name, status: delete_tracking_jobs) if delete_tracking_jobs
end
def perform_background_migration_inline? def perform_background_migration_inline?
Rails.env.test? || Rails.env.development? Rails.env.test? || Rails.env.development?
end end
@ -304,6 +332,12 @@ module Gitlab
end end
end end
def delete_job_tracking(class_name, status: 'succeeded')
status = Array(status).map { |s| Gitlab::Database::BackgroundMigrationJob.statuses[s] }
jobs = Gitlab::Database::BackgroundMigrationJob.where(status: status).for_migration_class(class_name)
jobs.each_batch { |batch| batch.delete_all }
end
private private
def track_in_database(class_name, arguments) def track_in_database(class_name, arguments)

View file

@ -581,4 +581,101 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
model.delete_queued_jobs('BackgroundMigrationClassName') model.delete_queued_jobs('BackgroundMigrationClassName')
end end
end end
describe '#finalized_background_migration' do
include_context 'background migration job class'
let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
before do
Sidekiq::Testing.disable! do
BackgroundMigrationWorker.perform_async(job_class_name, [1, 2])
BackgroundMigrationWorker.perform_async(job_class_name, [3, 4])
BackgroundMigrationWorker.perform_in(10, job_class_name, [5, 6])
BackgroundMigrationWorker.perform_in(20, job_class_name, [7, 8])
end
end
it_behaves_like 'finalized tracked background migration' do
before do
model.finalize_background_migration(job_class_name)
end
end
context 'when removing all tracked job records' do
# Force pending jobs to remain pending.
let!(:job_perform_method) { ->(*arguments) { } }
before do
model.finalize_background_migration(job_class_name, delete_tracking_jobs: %w[pending succeeded])
end
it_behaves_like 'finalized tracked background migration'
it_behaves_like 'removed tracked jobs', 'pending'
it_behaves_like 'removed tracked jobs', 'succeeded'
end
context 'when retaining all tracked job records' do
before do
model.finalize_background_migration(job_class_name, delete_tracking_jobs: false)
end
it_behaves_like 'finalized background migration'
include_examples 'retained tracked jobs', 'succeeded'
end
context 'during retry race condition' do
let(:queue_items_added) { [] }
let!(:job_perform_method) do
->(*arguments) do
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
RSpec.current_example.example_group_instance.job_class_name,
arguments
)
# Mock another process pushing queue jobs.
queue_items_added = RSpec.current_example.example_group_instance.queue_items_added
if queue_items_added.count < 10
Sidekiq::Testing.disable! do
job_class_name = RSpec.current_example.example_group_instance.job_class_name
queue_items_added << BackgroundMigrationWorker.perform_async(job_class_name, [Time.current])
queue_items_added << BackgroundMigrationWorker.perform_in(10, job_class_name, [Time.current])
end
end
end
end
it_behaves_like 'finalized tracked background migration' do
before do
model.finalize_background_migration(job_class_name, delete_tracking_jobs: ['succeeded'])
end
end
end
end
describe '#delete_job_tracking' do
let!(:job_class_name) { 'TestJob' }
let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
context 'with default status' do
before do
model.delete_job_tracking(job_class_name)
end
include_examples 'retained tracked jobs', 'pending'
include_examples 'removed tracked jobs', 'succeeded'
end
context 'with explicit status' do
before do
model.delete_job_tracking(job_class_name, status: %w[pending succeeded])
end
include_examples 'removed tracked jobs', 'pending'
include_examples 'removed tracked jobs', 'succeeded'
end
end
end end

View file

@ -28,11 +28,11 @@ RSpec.describe Spammable do
it 'returns true for public project' do it 'returns true for public project' do
issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
expect(issue.check_for_spam?).to eq(true) expect(issue.check_for_spam?(user: issue.author)).to eq(true)
end end
it 'returns false for other visibility levels' do it 'returns false for other visibility levels' do
expect(issue.check_for_spam?).to eq(false) expect(issue.check_for_spam?(user: issue.author)).to eq(false)
end end
end end

View file

@ -1112,14 +1112,14 @@ RSpec.describe Issue do
with_them do with_them do
it 'checks for spam when necessary' do it 'checks for spam when necessary' do
author = support_bot? ? support_bot : user active_user = support_bot? ? support_bot : user
project = reusable_project project = reusable_project
project.update!(visibility_level: visibility_level) project.update!(visibility_level: visibility_level)
issue = create(:issue, project: project, confidential: confidential, description: 'original description', author: author) issue = create(:issue, project: project, confidential: confidential, description: 'original description', author: support_bot)
issue.assign_attributes(new_attributes) issue.assign_attributes(new_attributes)
expect(issue.check_for_spam?).to eq(check_for_spam?) expect(issue.check_for_spam?(user: active_user)).to eq(check_for_spam?)
end end
end end
end end

View file

@ -431,7 +431,7 @@ RSpec.describe Snippet do
subject do subject do
snippet.assign_attributes(title: title) snippet.assign_attributes(title: title)
snippet.check_for_spam? snippet.check_for_spam?(user: snippet.author)
end end
context 'when public and spammable attributes changed' do context 'when public and spammable attributes changed' do
@ -455,7 +455,7 @@ RSpec.describe Snippet do
snippet.save! snippet.save!
snippet.visibility_level = Snippet::PUBLIC snippet.visibility_level = Snippet::PUBLIC
expect(snippet.check_for_spam?).to be_truthy expect(snippet.check_for_spam?(user: snippet.author)).to be_truthy
end end
end end

View file

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Spam::SpamActionService do RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants' include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: user) } let(:issue) { create(:issue, project: project, author: author) }
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' } let(:fake_referer) { 'fake-http-referer' }
@ -23,6 +23,7 @@ RSpec.describe Spam::SpamActionService do
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
before do before do
issue.spam = false issue.spam = false

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.shared_context 'background migration job class' do
let!(:job_class_name) { 'TestJob' }
let!(:job_class) { Class.new }
let!(:job_perform_method) do
->(*arguments) do
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
# Value is 'TestJob' defined by :job_class_name in the let! above.
# Scoping prohibits us from directly referencing job_class_name.
RSpec.current_example.example_group_instance.job_class_name,
arguments
)
end
end
before do
job_class.define_method(:perform, job_perform_method)
expect(Gitlab::BackgroundMigration).to receive(:migration_class_for).with(job_class_name).at_least(:once) { job_class }
end
end

View file

@ -21,3 +21,46 @@ RSpec.shared_examples 'marks background migration job records' do
expect(jobs_updated).to eq(1) expect(jobs_updated).to eq(1)
end end
end end
RSpec.shared_examples 'finalized background migration' do
it 'processed the scheduled sidekiq queue' do
queued = Sidekiq::ScheduledSet
.new
.select do |scheduled|
scheduled.klass == 'BackgroundMigrationWorker' &&
scheduled.args.first == job_class_name
end
expect(queued.size).to eq(0)
end
it 'processed the async sidekiq queue' do
queued = Sidekiq::Queue.new('BackgroundMigrationWorker')
.select { |scheduled| scheduled.klass == job_class_name }
expect(queued.size).to eq(0)
end
include_examples 'removed tracked jobs', 'pending'
end
RSpec.shared_examples 'finalized tracked background migration' do
include_examples 'finalized background migration'
include_examples 'removed tracked jobs', 'succeeded'
end
RSpec.shared_examples 'removed tracked jobs' do |status|
it "removes '#{status}' tracked jobs" do
jobs = Gitlab::Database::BackgroundMigrationJob
.where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status])
.for_migration_class(job_class_name)
expect(jobs).to be_empty
end
end
RSpec.shared_examples 'retained tracked jobs' do |status|
it "retains '#{status}' tracked jobs" do
jobs = Gitlab::Database::BackgroundMigrationJob
.where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status])
.for_migration_class(job_class_name)
expect(jobs).to be_present
end
end