diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-10 18:10:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-10 18:10:24 +0300 |
commit | ecc11e5d608ff4393fb6c44d02416569e7d2785d (patch) | |
tree | d6e2921cf11f525d8fd7bbbab213684983dba0cf /app | |
parent | e838c62efb5d95fe76b5bbb6cba8b73c40eb2008 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
47 files changed, 383 insertions, 186 deletions
diff --git a/app/assets/javascripts/issues/form.js b/app/assets/javascripts/issues/form.js index 20a8c251304..33371d065f9 100644 --- a/app/assets/javascripts/issues/form.js +++ b/app/assets/javascripts/issues/form.js @@ -4,8 +4,7 @@ import $ from 'jquery'; import IssuableForm from 'ee_else_ce/issuable/issuable_form'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import GLForm from '~/gl_form'; -import initSuggestions from '~/issues/suggestions'; -import initIssuableTypeSelector from '~/issues/type_selector'; +import { initTitleSuggestions, initTypePopover } from '~/issues/new'; import LabelsSelect from '~/labels/labels_select'; import MilestoneSelect from '~/milestones/milestone_select'; import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors'; @@ -20,6 +19,6 @@ export default () => { warnTemplateOverride: true, }); - initSuggestions(); - initIssuableTypeSelector(); + initTitleSuggestions(); + initTypePopover(); }; diff --git a/app/assets/javascripts/issues/suggestions/components/app.vue b/app/assets/javascripts/issues/new/components/title_suggestions.vue index 48a5e220abf..0a9cdb12519 100644 --- a/app/assets/javascripts/issues/suggestions/components/app.vue +++ b/app/assets/javascripts/issues/new/components/title_suggestions.vue @@ -2,12 +2,12 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import query from '../queries/issues.query.graphql'; -import Suggestion from './item.vue'; +import TitleSuggestionsItem from './title_suggestions_item.vue'; export default { components: { - Suggestion, GlIcon, + TitleSuggestionsItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -66,7 +66,7 @@ export default { </script> <template> - <div v-show="showSuggestions" class="form-group row issuable-suggestions"> + <div v-show="showSuggestions" class="form-group row"> <div v-once class="col-form-label col-sm-2 pt-0"> {{ __('Similar issues') }} <gl-icon @@ -86,7 +86,7 @@ export default { 'gl-mb-3': index !== issues.length - 1, }" > - <suggestion :suggestion="suggestion" /> + <title-suggestions-item :suggestion="suggestion" /> </li> </ul> </div> diff --git a/app/assets/javascripts/issues/suggestions/components/item.vue b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue index a01f4f747b9..a01f4f747b9 100644 --- a/app/assets/javascripts/issues/suggestions/components/item.vue +++ b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue diff --git a/app/assets/javascripts/issues/type_selector/components/info_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue index 3a20ccba814..a70e79b70f9 100644 --- a/app/assets/javascripts/issues/type_selector/components/info_popover.vue +++ b/app/assets/javascripts/issues/new/components/type_popover.vue @@ -19,9 +19,9 @@ export default { <template> <span id="popovercontainer"> - <gl-icon id="issuable-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" /> + <gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" /> <gl-popover - target="issuable-type-info" + target="issue-type-info" container="popovercontainer" :title="$options.i18n.issueTypes" triggers="focus hover" diff --git a/app/assets/javascripts/issues/suggestions/index.js b/app/assets/javascripts/issues/new/index.js index 8f7f317d6b4..59a7cbec627 100644 --- a/app/assets/javascripts/issues/suggestions/index.js +++ b/app/assets/javascripts/issues/new/index.js @@ -1,14 +1,19 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import App from './components/app.vue'; +import TitleSuggestions from './components/title_suggestions.vue'; +import TypePopover from './components/type_popover.vue'; -Vue.use(VueApollo); +export function initTitleSuggestions() { + Vue.use(VueApollo); -export default function initIssuableSuggestions() { const el = document.getElementById('js-suggestions'); const issueTitle = document.getElementById('issue_title'); - const { projectPath } = el.dataset; + + if (!el) { + return undefined; + } + const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); @@ -26,13 +31,26 @@ export default function initIssuableSuggestions() { this.search = issueTitle.value; }); }, - render(h) { - return h(App, { + render(createElement) { + return createElement(TitleSuggestions, { props: { - projectPath, + projectPath: el.dataset.projectPath, search: this.search, }, }); }, }); } + +export function initTypePopover() { + const el = document.getElementById('js-type-popover'); + + if (!el) { + return undefined; + } + + return new Vue({ + el, + render: (createElement) => createElement(TypePopover), + }); +} diff --git a/app/assets/javascripts/issues/suggestions/queries/issues.query.graphql b/app/assets/javascripts/issues/new/queries/issues.query.graphql index dc0757b141f..dc0757b141f 100644 --- a/app/assets/javascripts/issues/suggestions/queries/issues.query.graphql +++ b/app/assets/javascripts/issues/new/queries/issues.query.graphql diff --git a/app/assets/javascripts/issues/type_selector/index.js b/app/assets/javascripts/issues/type_selector/index.js deleted file mode 100644 index 433a62d1ae8..00000000000 --- a/app/assets/javascripts/issues/type_selector/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vue from 'vue'; -import InfoPopover from './components/info_popover.vue'; - -export default function initIssuableTypeSelector() { - const el = document.getElementById('js-type-popover'); - - return new Vue({ - el, - components: { - InfoPopover, - }, - render(h) { - return h(InfoPopover); - }, - }); -} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a82dad7e2c9..7235b38848c 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -735,3 +735,14 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag]; export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i)); export const isLoggedIn = () => Boolean(window.gon?.current_user_id); + +/** + * This method takes in array of objects with snake_case + * property names and returns a new array of objects with + * camelCase property names + * + * @param {Array[Object]} array - Array to be converted + * @returns {Array[Object]} Converted array + */ +export const convertArrayOfObjectsToCamelCase = (array) => + array.map((o) => convertObjectPropsToCamelCase(o)); diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index f96eb0fa564..023308dbac2 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -2,8 +2,9 @@ import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { __, s__ } from '~/locale'; +import { formatNumber, __, s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { RUNNER_JOB_COUNT_LIMIT } from '../constants'; import RunnerActionsCell from './cells/runner_actions_cell.vue'; import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; @@ -52,6 +53,12 @@ export default { }, }, methods: { + formatJobCount(jobCount) { + if (jobCount > RUNNER_JOB_COUNT_LIMIT) { + return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`; + } + return formatNumber(jobCount); + }, runnerTrAttr(runner) { if (runner) { return { @@ -66,6 +73,7 @@ export default { tableField({ key: 'summary', label: s__('Runners|Runner ID'), thClasses: ['gl-lg-w-25p'] }), tableField({ key: 'version', label: __('Version') }), tableField({ key: 'ipAddress', label: __('IP Address') }), + tableField({ key: 'jobCount', label: __('Jobs') }), tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }), tableField({ key: 'contactedAt', label: __('Last contact') }), tableField({ key: 'actions', label: '' }), @@ -112,6 +120,10 @@ export default { </tooltip-on-truncate> </template> + <template #cell(jobCount)="{ item: { jobCount } }"> + {{ formatJobCount(jobCount) }} + </template> + <template #cell(tagList)="{ item: { tagList } }"> <runner-tags :tag-list="tagList" size="sm" /> </template> diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index f0aa15ef64c..68e45fcf8e9 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -1,6 +1,7 @@ import { s__ } from '~/locale'; export const RUNNER_PAGE_SIZE = 20; +export const RUNNER_JOB_COUNT_LIMIT = 1000; export const GROUP_RUNNER_COUNT_LIMIT = 1000; export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.'); diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql index 3828d725758..169f6ffd2ea 100644 --- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql @@ -8,6 +8,7 @@ fragment RunnerNode on CiRunner { ipAddress active locked + jobCount tagList contactedAt status(legacyMode: null) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 327135d17ab..8600a4059d8 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -869,10 +869,6 @@ } } -.issuable-suggestions svg { - vertical-align: sub; -} - .suggestion-footer { font-size: 12px; line-height: 15px; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 976a9fa5038..d3ecbdcc1f6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -23,6 +23,7 @@ class ApplicationController < ActionController::Base include Gitlab::Utils::StrongMemoize include ::Gitlab::EndpointAttributes include FlocOptOut + include CheckRateLimit before_action :authenticate_user!, except: [:route_not_found] before_action :enforce_terms!, if: :should_enforce_terms? diff --git a/app/controllers/concerns/check_rate_limit.rb b/app/controllers/concerns/check_rate_limit.rb index c4de3315e22..5ccdf843525 100644 --- a/app/controllers/concerns/check_rate_limit.rb +++ b/app/controllers/concerns/check_rate_limit.rb @@ -5,19 +5,27 @@ # Controller concern that checks if the rate limit for a given action is throttled by calling the # Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request # will be logged and an error message will be rendered with a Too Many Requests response status. +# See lib/api/helpers/rate_limiter.rb for API version module CheckRateLimit - def check_rate_limit(key) - return unless rate_limiter.throttled?(key, scope: current_user, users_allowlist: rate_limit_users_allowlist) + def check_rate_limit!(key, scope:, redirect_back: false, **options) + return unless rate_limiter.throttled?(key, scope: scope, **options) rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) - render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests + + return yield if block_given? + + message = _('This endpoint has been requested too many times. Try again later.') + + if redirect_back + redirect_back_or_default(options: { alert: message }) + else + render plain: message, status: :too_many_requests + end end + private + def rate_limiter ::Gitlab::ApplicationRateLimiter end - - def rate_limit_users_allowlist - Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist - end end diff --git a/app/controllers/concerns/integrations/hooks_execution.rb b/app/controllers/concerns/integrations/hooks_execution.rb index af039057a9c..6a9d3d51f9b 100644 --- a/app/controllers/concerns/integrations/hooks_execution.rb +++ b/app/controllers/concerns/integrations/hooks_execution.rb @@ -32,16 +32,4 @@ module Integrations::HooksExecution flash[:alert] = "Hook execution failed: #{message}" end end - - def create_rate_limit(key, scope) - if rate_limiter.throttled?(key, scope: [scope, current_user]) - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) - - render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests - end - end - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 113030429d8..8410a8779f6 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -3,7 +3,6 @@ module NotesActions include RendersNotes include Gitlab::Utils::StrongMemoize - include CheckRateLimit extend ActiveSupport::Concern # last_fetched_at is an integer number of microseconds, which is the same @@ -16,7 +15,11 @@ module NotesActions before_action :require_noteable!, only: [:index, :create] before_action :authorize_admin_note!, only: [:update, :destroy] before_action :note_project, only: [:create] - before_action -> { check_rate_limit(:notes_create) }, only: [:create] + before_action -> { + check_rate_limit!(:notes_create, + scope: current_user, + users_allowlist: Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist) + }, only: [:create] end def index diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7f2b026b5b3..62336c7eede 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -37,7 +37,7 @@ class GroupsController < Groups::ApplicationController push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml) end - before_action :export_rate_limit, only: [:export, :download_export] + before_action :check_export_rate_limit!, only: [:export, :download_export] helper_method :captcha_required? @@ -314,16 +314,12 @@ class GroupsController < Groups::ApplicationController url_for(safe_params) end - def export_rate_limit + def check_export_rate_limit! prefixed_action = "group_#{params[:action]}".to_sym scope = params[:action] == :download_export ? @group : nil - if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, scope].compact) - Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user) - - render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests - end + check_rate_limit!(prefixed_action, scope: [current_user, scope].compact) end def ensure_export_enabled diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 53856e4575b..7ad3a2ee358 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -3,7 +3,7 @@ class Import::BaseController < ApplicationController include ActionView::Helpers::SanitizeHelper - before_action :import_rate_limit, only: [:create] + before_action -> { check_rate_limit!(:project_import, scope: [current_user, :project_import], redirect_back: true) }, only: [:create] feature_category :importers def status @@ -98,18 +98,4 @@ class Import::BaseController < ApplicationController def project_save_error(project) project.errors.full_messages.join(', ') end - - def import_rate_limit - key = "project_import".to_sym - - if rate_limiter.throttled?(key, scope: [current_user, key]) - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) - - redirect_back_or_default(options: { alert: _('This endpoint has been requested too many times. Try again later.') }) - end - end - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end end diff --git a/app/controllers/import/gitlab_groups_controller.rb b/app/controllers/import/gitlab_groups_controller.rb index 503b10f766b..aca71f6d57a 100644 --- a/app/controllers/import/gitlab_groups_controller.rb +++ b/app/controllers/import/gitlab_groups_controller.rb @@ -4,7 +4,7 @@ class Import::GitlabGroupsController < ApplicationController include WorkhorseAuthorization before_action :ensure_group_import_enabled - before_action :import_rate_limit, only: %i[create] + before_action :check_import_rate_limit!, only: %i[create] feature_category :importers @@ -55,12 +55,9 @@ class Import::GitlabGroupsController < ApplicationController render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true) end - def import_rate_limit - if Gitlab::ApplicationRateLimiter.throttled?(:group_import, scope: current_user) - Gitlab::ApplicationRateLimiter.log_request(request, :group_import_request_limit, current_user) - - flash[:alert] = _('This endpoint has been requested too many times. Try again later.') - redirect_to new_group_path + def check_import_rate_limit! + check_rate_limit!(:group_import, scope: current_user) do + redirect_to new_group_path, alert: _('This endpoint has been requested too many times. Try again later.') end end diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 6e5b18cb885..be2cb270a19 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -2,8 +2,10 @@ class Profiles::EmailsController < Profiles::ApplicationController before_action :find_email, only: [:destroy, :resend_confirmation_instructions] - before_action -> { rate_limit!(:profile_add_new_email) }, only: [:create] - before_action -> { rate_limit!(:profile_resend_email_confirmation) }, only: [:resend_confirmation_instructions] + before_action -> { check_rate_limit!(:profile_add_new_email, scope: current_user, redirect_back: true) }, + only: [:create] + before_action -> { check_rate_limit!(:profile_resend_email_confirmation, scope: current_user, redirect_back: true) }, + only: [:resend_confirmation_instructions] feature_category :users @@ -42,16 +44,6 @@ class Profiles::EmailsController < Profiles::ApplicationController private - def rate_limit!(action) - rate_limiter = ::Gitlab::ApplicationRateLimiter - - if rate_limiter.throttled?(action, scope: current_user) - rate_limiter.log_request(request, action, current_user) - - redirect_back_or_default(options: { alert: _('This action has been performed too many times. Try again later.') }) - end - end - def email_params params.require(:email).permit(:email) end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index c79e5a8cc85..99eba32e00f 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -6,7 +6,7 @@ class Projects::HooksController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! before_action :hook_logs, only: :edit - before_action -> { create_rate_limit(:project_testing_hook, @project) }, only: :test + before_action -> { check_rate_limit!(:project_testing_hook, scope: [@project, current_user]) }, only: :test respond_to :html diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d2d7ecfab6f..970efd9cdfb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -37,7 +37,9 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_download_code!, only: [:related_branches] # Limit the amount of issues created per minute - before_action :create_rate_limit, only: [:create], if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) } + before_action -> { check_rate_limit!(:issues_create, scope: [@project, @current_user])}, + only: [:create], + if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) } before_action do push_frontend_feature_flag(:tribute_autocomplete, @project) @@ -363,20 +365,6 @@ class Projects::IssuesController < Projects::ApplicationController project_compare_path(project, from: project.default_branch, to: branch[:name]) end - def create_rate_limit - key = :issues_create - - if rate_limiter.throttled?(key, scope: [@project, @current_user]) - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) - - render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests - end - end - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - def service_desk? action_name == 'service_desk' end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 4af7508b935..ac94cc001dd 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -3,7 +3,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :schedule, except: [:index, :new, :create] - before_action :play_rate_limit, only: [:play] + before_action :check_play_rate_limit!, only: [:play] before_action :authorize_play_pipeline_schedule!, only: [:play] before_action :authorize_read_pipeline_schedule! before_action :authorize_create_pipeline_schedule!, only: [:new, :create] @@ -81,19 +81,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController private - def play_rate_limit + def check_play_rate_limit! return unless current_user - if rate_limiter.throttled?(:play_pipeline_schedule, scope: [current_user, schedule]) + check_rate_limit!(:play_pipeline_schedule, scope: [current_user, schedule]) do flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.') redirect_to pipeline_schedules_path(@project) end end - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - def schedule @schedule ||= project.pipeline_schedules.find(params[:id]) end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 8960783400e..9707b70f26f 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -13,7 +13,7 @@ class Projects::RawController < Projects::ApplicationController before_action :set_ref_and_path before_action :require_non_empty_project before_action :authorize_download_code! - before_action :show_rate_limit, only: [:show], unless: :external_storage_request? + before_action :check_show_rate_limit!, only: [:show], unless: :external_storage_request? before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled? feature_category :source_code_management @@ -33,23 +33,11 @@ class Projects::RawController < Projects::ApplicationController @ref, @path = extract_ref(get_id) end - def show_rate_limit - if rate_limiter.throttled?(:show_raw_controller, scope: [@project, @path], threshold: raw_blob_request_limit) - rate_limiter.log_request(request, :raw_blob_request_limit, current_user) - + def check_show_rate_limit! + check_rate_limit!(:raw_blob, scope: [@project, @path]) do render plain: _('You cannot access the raw file. Please wait a minute.'), status: :too_many_requests end end - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - - def raw_blob_request_limit - Gitlab::CurrentSettings - .current_application_settings - .raw_blob_request_limit - end end Projects::RawController.prepend_mod diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 8beebb52980..77826a2f789 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -3,16 +3,16 @@ class Projects::RepositoriesController < Projects::ApplicationController include ExtractsPath include StaticObjectExternalStorage - include Gitlab::RateLimitHelpers include HotlinkInterceptor + include Gitlab::RepositoryArchiveRateLimiter prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) } skip_before_action :default_cache_headers, only: :archive # Authorize + before_action :check_archive_rate_limiting!, only: :archive before_action :require_non_empty_project, except: :create - before_action :archive_rate_limit!, only: :archive before_action :intercept_hotlinking!, only: :archive before_action :assign_archive_vars, only: :archive before_action :assign_append_sha, only: :archive @@ -42,12 +42,6 @@ class Projects::RepositoriesController < Projects::ApplicationController private - def archive_rate_limit! - if archive_rate_limit_reached?(current_user, @project) - render plain: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE, status: :too_many_requests - end - end - def repo_params @repo_params ||= { ref: @ref, path: params[:path], format: params[:format], append_sha: @append_sha } end @@ -125,6 +119,12 @@ class Projects::RepositoriesController < Projects::ApplicationController [path, nil] end end + + def check_archive_rate_limiting! + check_archive_rate_limit!(current_user, @project) do + render(plain: _('This archive has been requested too many times. Try again later.'), status: :too_many_requests) + end + end end Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController') diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 887f98362b4..ef6c10d43cd 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -9,6 +9,7 @@ module Projects layout 'project_settings' before_action :authorize_admin_pipeline! + before_action :check_builds_available! before_action :define_variables before_action do push_frontend_feature_flag(:ajax_new_deploy_token, @project) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6af46e22d8f..428903a9e75 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -30,7 +30,7 @@ class ProjectsController < Projects::ApplicationController before_action :event_filter, only: [:show, :activity] # Project Export Rate Limit - before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export] + before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export] before_action do push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) @@ -544,20 +544,12 @@ class ProjectsController < Projects::ApplicationController @project = @project.present(current_user: current_user) end - def export_rate_limit + def check_export_rate_limit! prefixed_action = "project_#{params[:action]}".to_sym project_scope = params[:action] == 'download_export' ? @project : nil - if rate_limiter.throttled?(prefixed_action, scope: [current_user, project_scope].compact) - rate_limiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user) - - render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests - end - end - - def rate_limiter - ::Gitlab::ApplicationRateLimiter + check_rate_limit!(prefixed_action, scope: [current_user, project_scope].compact) end def render_edit diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb index a497d2cabe5..8a4be3139e8 100644 --- a/app/models/ci/namespace_mirror.rb +++ b/app/models/ci/namespace_mirror.rb @@ -4,6 +4,34 @@ module Ci # This model represents a record in a shadow table of the main database's namespaces table. # It allows us to navigate the namespace hierarchy on the ci database without resorting to a JOIN. class NamespaceMirror < ApplicationRecord - # Will be filled by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517 + belongs_to :namespace + + scope :contains_namespace, -> (id) do + where('traversal_ids @> ARRAY[?]::int[]', id) + end + + class << self + def sync!(event) + namespace = event.namespace + traversal_ids = namespace.self_and_ancestor_ids(hierarchy_order: :desc) + + upsert({ namespace_id: event.namespace_id, traversal_ids: traversal_ids }, + unique_by: :namespace_id) + + # It won't be necessary once we remove `sync_traversal_ids`. + # More info: https://gitlab.com/gitlab-org/gitlab/-/issues/347541 + sync_children_namespaces!(event.namespace_id, traversal_ids) + end + + private + + def sync_children_namespaces!(namespace_id, traversal_ids) + contains_namespace(namespace_id) + .where.not(namespace_id: namespace_id) + .update_all( + "traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]" + ) + end + end end end diff --git a/app/models/ci/project_mirror.rb b/app/models/ci/project_mirror.rb index c6e3101fb3a..d6aaa3f50c1 100644 --- a/app/models/ci/project_mirror.rb +++ b/app/models/ci/project_mirror.rb @@ -4,6 +4,13 @@ module Ci # This model represents a shadow table of the main database's projects table. # It allows us to navigate the project and namespace hierarchy on the ci database. class ProjectMirror < ApplicationRecord - # Will be filled by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517 + belongs_to :project + + class << self + def sync!(event) + upsert({ project_id: event.project_id, namespace_id: event.project.namespace_id }, + unique_by: :project_id) + end + end end end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 8e130998f11..c914819f79d 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -145,9 +145,14 @@ class ContainerRepository < ApplicationRecord name: path.repository_name) end - def self.create_from_path!(path) - safe_find_or_create_by!(project: path.repository_project, - name: path.repository_name) + def self.find_or_create_from_path(path) + repository = safe_find_or_create_by( + project: path.repository_project, + name: path.repository_name + ) + return repository if repository.persisted? + + find_by_path!(path) end def self.build_root_repository(project) diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb index 0b638f65768..18c1467e6f6 100644 --- a/app/models/error_tracking/error_event.rb +++ b/app/models/error_tracking/error_event.rb @@ -3,6 +3,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord belongs_to :error, counter_cache: :events_count + # Scrub null bytes + attribute :payload, Gitlab::Database::Type::JsonPgSafe.new + validates :payload, json_schema: { filename: 'error_tracking_event_payload' } validates :error, presence: true diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb index c3b3e76f67b..0fbdd2d8a5b 100644 --- a/app/models/loose_foreign_keys/deleted_record.rb +++ b/app/models/loose_foreign_keys/deleted_record.rb @@ -1,15 +1,45 @@ # frozen_string_literal: true class LooseForeignKeys::DeletedRecord < ApplicationRecord + PARTITION_DURATION = 1.day + + include PartitionedTable + self.primary_key = :id + self.ignored_columns = %i[partition] + + partitioned_by :partition, strategy: :sliding_list, + next_partition_if: -> (active_partition) do + return false if Feature.disabled?(:lfk_automatic_partition_creation, default_enabled: :yaml) + + oldest_record_in_partition = LooseForeignKeys::DeletedRecord + .select(:id, :created_at) + .for_partition(active_partition) + .order(:id) + .limit(1) + .take + + oldest_record_in_partition.present? && oldest_record_in_partition.created_at < PARTITION_DURATION.ago + end, + detach_partition_if: -> (partition) do + return false if Feature.disabled?(:lfk_automatic_partition_dropping, default_enabled: :yaml) + + !LooseForeignKeys::DeletedRecord + .for_partition(partition) + .status_pending + .exists? + end scope :for_table, -> (table) { where(fully_qualified_table_name: table) } + scope :for_partition, -> (partition) { where(partition: partition) } scope :consume_order, -> { order(:partition, :consume_after, :id) } enum status: { pending: 1, processed: 2 }, _prefix: :status def self.load_batch_for_table(table, batch_size) - for_table(table) + # selecting partition as partition_number to workaround the sliding partitioning column ignore + select(arel_table[Arel.star], arel_table[:partition].as('partition_number')) + .for_table(table) .status_pending .consume_order .limit(batch_size) @@ -20,9 +50,9 @@ class LooseForeignKeys::DeletedRecord < ApplicationRecord # Run a query for each partition to optimize the row lookup by primary key (partition, id) update_count = 0 - all_records.group_by(&:partition).each do |partition, records_within_partition| + all_records.group_by(&:partition_number).each do |partition, records_within_partition| update_count += status_pending - .where(partition: partition) + .for_partition(partition) .where(id: records_within_partition.pluck(:id)) .update_all(status: :processed) end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index db306221318..4b1cf2fa217 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -64,6 +64,9 @@ class Namespace < ApplicationRecord has_one :admin_note, inverse_of: :namespace accepts_nested_attributes_for :admin_note, update_only: true + has_one :ci_namespace_mirror, class_name: 'Ci::NamespaceMirror' + has_many :sync_events, class_name: 'Namespaces::SyncEvent' + validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, presence: true, @@ -104,6 +107,8 @@ class Namespace < ApplicationRecord delegate :name, to: :owner, allow_nil: true, prefix: true delegate :avatar_url, to: :owner, allow_nil: true + after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? } + after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } before_create :sync_share_with_group_lock_with_parent @@ -609,6 +614,13 @@ class Namespace < ApplicationRecord def enforce_minimum_path_length? path_changed? && !project_namespace? end + + # SyncEvents are created by PG triggers (with the function `insert_namespaces_sync_event`) + def schedule_sync_event_worker + run_after_commit do + Namespaces::SyncEvent.enqueue_worker + end + end end Namespace.prepend_mod_with('Namespace') diff --git a/app/models/namespaces/sync_event.rb b/app/models/namespaces/sync_event.rb new file mode 100644 index 00000000000..8534d8afb8c --- /dev/null +++ b/app/models/namespaces/sync_event.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# This model serves to keep track of changes to the namespaces table in the main database, and allowing to safely +# replicate these changes to other databases. +class Namespaces::SyncEvent < ApplicationRecord + self.table_name = 'namespaces_sync_events' + + belongs_to :namespace + + scope :preload_synced_relation, -> { preload(:namespace) } + scope :order_by_id_asc, -> { order(id: :asc) } + + def self.enqueue_worker + ::Namespaces::ProcessSyncEventsWorker.perform_async # rubocop:disable CodeReuse/Worker + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 088a2f9ea27..a751e8adeb0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -102,6 +102,8 @@ class Project < ApplicationRecord after_save :update_project_statistics, if: :saved_change_to_namespace_id? + after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? } + after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? } after_save :save_topics @@ -394,6 +396,9 @@ class Project < ApplicationRecord has_many :timelogs + has_one :ci_project_mirror, class_name: 'Ci::ProjectMirror' + has_many :sync_events, class_name: 'Projects::SyncEvent' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_setting, update_only: true @@ -2938,6 +2943,13 @@ class Project < ApplicationRecord project_namespace.shared_runners_enabled = shared_runners_enabled project_namespace.visibility_level = visibility_level end + + # SyncEvents are created by PG triggers (with the function `insert_projects_sync_event`) + def schedule_sync_event_worker + run_after_commit do + Projects::SyncEvent.enqueue_worker + end + end end Project.prepend_mod_with('Project') diff --git a/app/models/projects/sync_event.rb b/app/models/projects/sync_event.rb new file mode 100644 index 00000000000..5221b00c55f --- /dev/null +++ b/app/models/projects/sync_event.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# This model serves to keep track of changes to the namespaces table in the main database as they relate to projects, +# allowing to safely replicate changes to other databases. +class Projects::SyncEvent < ApplicationRecord + self.table_name = 'projects_sync_events' + + belongs_to :project + + scope :preload_synced_relation, -> { preload(:project) } + scope :order_by_id_asc, -> { order(id: :asc) } + + def self.enqueue_worker + ::Projects::ProcessSyncEventsWorker.perform_async # rubocop:disable CodeReuse/Worker + end +end diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb index 93626c6d1a1..09b0f5d608d 100644 --- a/app/policies/namespaces/user_namespace_policy.rb +++ b/app/policies/namespaces/user_namespace_policy.rb @@ -4,7 +4,6 @@ module Namespaces class UserNamespacePolicy < ::NamespacePolicy rule { anonymous }.prevent_all - condition(:personal_project, scope: :subject) { @subject.kind == 'user' } condition(:can_create_personal_project, scope: :user) { @user.can_create_project? } condition(:owner) { @subject.owner == @user } @@ -19,7 +18,7 @@ module Namespaces enable :read_package_settings end - rule { personal_project & ~can_create_personal_project }.prevent :create_projects + rule { ~can_create_personal_project }.prevent :create_projects rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects end diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 00820e00863..7f5dffadcfb 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -3,7 +3,6 @@ module Ci class PipelinePresenter < Gitlab::View::Presenter::Delegated include Gitlab::Utils::StrongMemoize - include ActionView::Helpers::UrlHelper delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag` @@ -108,7 +107,7 @@ module Ci end def link_to_pipeline_ref - link_to(pipeline.ref, + ApplicationController.helpers.link_to(pipeline.ref, project_commits_path(pipeline.project, pipeline.ref), class: "ref-name") end @@ -116,7 +115,7 @@ module Ci def link_to_merge_request return unless merge_request_presenter - link_to(merge_request_presenter.to_reference, + ApplicationController.helpers.link_to(merge_request_presenter.to_reference, project_merge_request_path(merge_request_presenter.project, merge_request_presenter), class: 'mr-iid') end @@ -143,7 +142,7 @@ module Ci private def plain_ref_name - content_tag(:span, pipeline.ref, class: 'ref-name') + ApplicationController.helpers.content_tag(:span, pipeline.ref, class: 'ref-name') end def merge_request_presenter @@ -160,7 +159,7 @@ module Ci all_related_merge_requests.first(limit).map do |merge_request| mr_path = project_merge_request_path(merge_request.project, merge_request) - link_to "#{merge_request.to_reference} #{merge_request.title}", mr_path, class: 'mr-iid' + ApplicationController.helpers.link_to "#{merge_request.to_reference} #{merge_request.title}", mr_path, class: 'mr-iid' end end diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb index 714329ede71..776e2baebdd 100644 --- a/app/presenters/prometheus_alert_presenter.rb +++ b/app/presenters/prometheus_alert_presenter.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated - include ActionView::Helpers::UrlHelper - presents ::PrometheusAlert, as: :prometheus_alert def humanized_text diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index bc734465750..ea4723c9e28 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -156,7 +156,7 @@ module Auth return if path.has_repository? return unless actions.include?('push') - ContainerRepository.create_from_path!(path) + ContainerRepository.find_or_create_from_path(path) end # Overridden in EE diff --git a/app/services/ci/process_sync_events_service.rb b/app/services/ci/process_sync_events_service.rb new file mode 100644 index 00000000000..6be8c41dc6a --- /dev/null +++ b/app/services/ci/process_sync_events_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Ci + class ProcessSyncEventsService + include Gitlab::Utils::StrongMemoize + include ExclusiveLeaseGuard + + BATCH_SIZE = 1000 + + def initialize(sync_event_class, sync_class) + @sync_event_class = sync_event_class + @sync_class = sync_class + end + + def execute + return unless ::Feature.enabled?(:ci_namespace_project_mirrors, default_enabled: :yaml) + + # preventing parallel processing over the same event table + try_obtain_lease { process_events } + + enqueue_worker_if_there_still_event + end + + private + + def process_events + events = @sync_event_class.preload_synced_relation.first(BATCH_SIZE) + + return if events.empty? + + first = events.first + last_processed = nil + + begin + events.each do |event| + @sync_class.sync!(event) + + last_processed = event + end + ensure + # remove events till the one that was last succesfully processed + @sync_event_class.id_in(first.id..last_processed.id).delete_all if last_processed + end + end + + def enqueue_worker_if_there_still_event + @sync_event_class.enqueue_worker if @sync_event_class.exists? + end + + def lease_key + "#{super}::#{@sync_event_class}" + end + + def lease_timeout + 1.minute + end + end +end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 3bf6644bb29..be21ed5b73d 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -7,7 +7,7 @@ module Ci allow_failure stage stage_id stage_idx trigger_request yaml_variables when environment coverage_regex description tag_list protected needs_attributes - resource_group scheduling_type].freeze + job_variables_attributes resource_group scheduling_type].freeze end def self.extra_accessors @@ -68,13 +68,7 @@ module Ci end def build_attributes(build) - clone_attributes = if ::Feature.enabled?(:clone_job_variables_at_job_retry, build.project, default_enabled: :yaml) - self.class.clone_accessors + [:job_variables_attributes] - else - self.class.clone_accessors - end - - attributes = clone_attributes.to_h do |attribute| + attributes = self.class.clone_accessors.to_h do |attribute| [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 9700daf1c4b..171d52c328d 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -80,7 +80,7 @@ class SearchService def abuse_messages return [] unless params.abusive? - params.abuse_detection.errors.messages + params.abuse_detection.errors.full_messages end def valid_request? diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index c66ba5ba2e1..a1e94172ec3 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -79,7 +79,7 @@ %span= milestone.issues_visible_to_user(current_user).count .title.hide-collapsed = s_('MilestoneSidebar|Issues') - %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.issues_visible_to_user(current_user).count + = gl_badge_tag milestone.issues_visible_to_user(current_user).count, variant: :muted, size: :sm - if show_new_issue_link?(project) = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: s_('MilestoneSidebar|New Issue') do = s_('MilestoneSidebar|New issue') @@ -111,7 +111,7 @@ %span= milestone.merge_requests.count .title.hide-collapsed = s_('MilestoneSidebar|Merge requests') - %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.merge_requests.count + = gl_badge_tag milestone.merge_requests.count, variant: :muted, size: :sm .value.hide-collapsed.bold - if !project || can?(current_user, :read_merge_request, project) %span.milestone-stat diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 89a4d9dc7cf..e84646047e4 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2492,6 +2492,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: namespaces_process_sync_events + :worker_name: Namespaces::ProcessSyncEventsWorker + :feature_category: :sharding + :has_external_dependencies: + :urgency: :high + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: new_issue :worker_name: NewIssueWorker :feature_category: :team_planning @@ -2663,6 +2672,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: projects_process_sync_events + :worker_name: Projects::ProcessSyncEventsWorker + :feature_category: :sharding + :has_external_dependencies: + :urgency: :high + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: projects_schedule_bulk_repository_shard_moves :worker_name: Projects::ScheduleBulkRepositoryShardMovesWorker :feature_category: :gitaly diff --git a/app/workers/namespaces/process_sync_events_worker.rb b/app/workers/namespaces/process_sync_events_worker.rb new file mode 100644 index 00000000000..f3c4f5bebb1 --- /dev/null +++ b/app/workers/namespaces/process_sync_events_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Namespaces + # This worker can be called multiple times at the same time but only one of them can + # process events at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`. + # `until_executing` here is to reduce redundant worker enqueuing. + class ProcessSyncEventsWorker + include ApplicationWorker + + data_consistency :always + + feature_category :sharding + urgency :high + + idempotent! + deduplicate :until_executing + + def perform + ::Ci::ProcessSyncEventsService.new(::Namespaces::SyncEvent, ::Ci::NamespaceMirror).execute + end + end +end diff --git a/app/workers/projects/process_sync_events_worker.rb b/app/workers/projects/process_sync_events_worker.rb new file mode 100644 index 00000000000..b7c4b4de3d0 --- /dev/null +++ b/app/workers/projects/process_sync_events_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Projects + # This worker can be called multiple times at the same time but only one of them can + # process events at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`. + # `until_executing` here is to reduce redundant worker enqueuing. + class ProcessSyncEventsWorker + include ApplicationWorker + + data_consistency :always + + feature_category :sharding + urgency :high + + idempotent! + deduplicate :until_executing + + def perform + ::Ci::ProcessSyncEventsService.new(::Projects::SyncEvent, ::Ci::ProjectMirror).execute + end + end +end |