Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f4a9d976cf
commit
2abeca2d92
|
@ -809,7 +809,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
|
|||
/app/assets/javascripts/authentication/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/ide/components/shared/tokened_input.vue @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/invite_members/components/members_token_select.vue @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/logs/components/tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/pages/admin/impersonation_tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/pages/groups/settings/access_tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||
|
|
|
@ -5,7 +5,7 @@ review-cleanup:
|
|||
extends:
|
||||
- .default-retry
|
||||
- .review:rules:review-cleanup
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-gcloud-helm3.5-kubectl1.17
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ruby-3.0:gcloud-383-kubectl-1.23-helm-3.5
|
||||
stage: prepare
|
||||
environment:
|
||||
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
|
||||
|
|
|
@ -1729,6 +1729,9 @@
|
|||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- <<: *if-dot-com-gitlab-org-schedule
|
||||
allow_failure: true
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#import "../fragments/user.fragment.graphql"
|
||||
|
||||
query currentUser {
|
||||
currentUser {
|
||||
...User
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { initWorkItemsRoot } from '~/work_items/index';
|
||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||
|
||||
initWorkItemsRoot();
|
||||
initInviteMembersModal();
|
||||
|
|
|
@ -2,7 +2,7 @@ import produce from 'immer';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { temporaryConfig } from '~/work_items/graphql/provider';
|
||||
import { temporaryConfig, resolvers as workItemResolvers } from '~/work_items/graphql/provider';
|
||||
|
||||
const resolvers = {
|
||||
Mutation: {
|
||||
|
@ -13,6 +13,7 @@ const resolvers = {
|
|||
});
|
||||
cache.writeQuery({ query: getIssueStateQuery, data });
|
||||
},
|
||||
...workItemResolvers.Mutation,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
<script>
|
||||
import { GlTokenSelector, GlIcon, GlAvatar, GlLink, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import {
|
||||
GlTokenSelector,
|
||||
GlIcon,
|
||||
GlAvatar,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlButton,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
import { n__ } from '~/locale';
|
||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
|
@ -27,7 +38,11 @@ export default {
|
|||
GlAvatar,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlButton,
|
||||
SidebarParticipant,
|
||||
InviteMembersTrigger,
|
||||
GlDropdownItem,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
|
@ -47,6 +62,7 @@ export default {
|
|||
localAssignees: this.assignees.map(addClass),
|
||||
searchKey: '',
|
||||
searchUsers: [],
|
||||
currentUser: null,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -70,6 +86,9 @@ export default {
|
|||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
currentUser: {
|
||||
query: currentUserQuery,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
assigneeListEmpty() {
|
||||
|
@ -78,12 +97,24 @@ export default {
|
|||
containerClass() {
|
||||
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
|
||||
},
|
||||
isLoading() {
|
||||
isLoadingUsers() {
|
||||
return this.$apollo.queries.searchUsers.loading;
|
||||
},
|
||||
assigneeText() {
|
||||
return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
|
||||
},
|
||||
dropdownItems() {
|
||||
if (this.currentUser && this.searchEmpty) {
|
||||
if (this.searchUsers.some((user) => user.username === this.currentUser.username)) {
|
||||
return this.moveCurrentUserToStart(this.searchUsers);
|
||||
}
|
||||
return [this.currentUser, ...this.searchUsers];
|
||||
}
|
||||
return this.searchUsers;
|
||||
},
|
||||
searchEmpty() {
|
||||
return this.searchKey.length === 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
assignees(newVal) {
|
||||
|
@ -99,15 +130,18 @@ export default {
|
|||
getUserId(id) {
|
||||
return getIdFromGraphQLId(id);
|
||||
},
|
||||
setAssignees(e) {
|
||||
handleBlur(e) {
|
||||
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
|
||||
this.isEditing = false;
|
||||
this.setAssignees(this.localAssignees);
|
||||
},
|
||||
setAssignees(assignees) {
|
||||
this.$apollo.mutate({
|
||||
mutation: localUpdateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
assignees: this.localAssignees,
|
||||
assignees,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -132,6 +166,15 @@ export default {
|
|||
setSearchKey(value) {
|
||||
this.searchKey = value;
|
||||
},
|
||||
moveCurrentUserToStart(users = []) {
|
||||
if (this.currentUser) {
|
||||
return [this.currentUser, ...users.filter((user) => user.id !== this.currentUser.id)];
|
||||
}
|
||||
return users;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.$refs.tokenSelector.closeDropdown();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -147,13 +190,13 @@ export default {
|
|||
ref="tokenSelector"
|
||||
v-model="localAssignees"
|
||||
:container-class="containerClass"
|
||||
class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start"
|
||||
:dropdown-items="searchUsers"
|
||||
:loading="isLoading"
|
||||
class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0!"
|
||||
:dropdown-items="dropdownItems"
|
||||
:loading="isLoadingUsers"
|
||||
@input="focusTokenSelector"
|
||||
@text-input="debouncedSearchKeyUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="setAssignees"
|
||||
@blur="handleBlur"
|
||||
@mouseover.native="handleMouseOver"
|
||||
@mouseout.native="handleMouseOut"
|
||||
>
|
||||
|
@ -163,7 +206,15 @@ export default {
|
|||
data-testid="empty-state"
|
||||
>
|
||||
<gl-icon name="profile" />
|
||||
<span class="gl-ml-2">{{ __('Add assignees') }}</span>
|
||||
<span class="gl-ml-2 gl-mr-4">{{ __('Add assignees') }}</span>
|
||||
<gl-button
|
||||
v-if="currentUser"
|
||||
size="small"
|
||||
class="assign-myself"
|
||||
data-testid="assign-self"
|
||||
@click.stop="setAssignees([currentUser])"
|
||||
>{{ __('Assign myself') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #token-content="{ token }">
|
||||
|
@ -189,6 +240,18 @@ export default {
|
|||
<rect width="280" height="20" x="10" y="130" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</template>
|
||||
<template #dropdown-footer>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item @click="closeDropdown">
|
||||
<invite-members-trigger
|
||||
:display-text="__('Invite members')"
|
||||
trigger-element="side-nav"
|
||||
icon="plus"
|
||||
trigger-source="work-item-assignees-dropdown"
|
||||
classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
|
||||
/>
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-token-selector>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -25,7 +25,7 @@ export const temporaryConfig = {
|
|||
nodes: [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
id: 'gid://gitlab/User/10001',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
|
@ -34,7 +34,7 @@ export const temporaryConfig = {
|
|||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/2',
|
||||
id: 'gid://gitlab/User/10002',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
|
|
|
@ -13,3 +13,13 @@
|
|||
#weight-widget-input[readonly] {
|
||||
background-color: var(--white, $white);
|
||||
}
|
||||
|
||||
.work-item-assignees {
|
||||
.assign-myself {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assignees-selector:hover .assign-myself {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,6 +131,7 @@ module Ci
|
|||
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
|
||||
|
||||
after_create :keep_around_commits, unless: :importing?
|
||||
after_find :observe_age_in_minutes, unless: :importing?
|
||||
|
||||
use_fast_destroy :job_artifacts
|
||||
use_fast_destroy :build_trace_chunks
|
||||
|
@ -1322,6 +1323,16 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def age_in_minutes
|
||||
return 0 unless persisted?
|
||||
|
||||
unless has_attribute?(:created_at)
|
||||
raise ArgumentError, 'pipeline not fully loaded'
|
||||
end
|
||||
|
||||
(Time.current - created_at).ceil / 60
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_message(severity, content)
|
||||
|
@ -1372,6 +1383,21 @@ module Ci
|
|||
project.repository.keep_around(self.sha, self.before_sha)
|
||||
end
|
||||
|
||||
def observe_age_in_minutes
|
||||
return unless age_metric_enabled?
|
||||
return unless persisted? && has_attribute?(:created_at)
|
||||
|
||||
::Gitlab::Ci::Pipeline::Metrics
|
||||
.pipeline_age_histogram
|
||||
.observe({}, age_in_minutes)
|
||||
end
|
||||
|
||||
def age_metric_enabled?
|
||||
::Gitlab::SafeRequestStore.fetch(:age_metric_enabled) do
|
||||
::Feature.enabled?(:ci_pipeline_age_histogram, type: :ops)
|
||||
end
|
||||
end
|
||||
|
||||
# Without using `unscoped`, caller scope is also included into the query.
|
||||
# Using `unscoped` here will be redundant after Rails 6.1
|
||||
def object_hierarchy(options = {})
|
||||
|
|
|
@ -11,8 +11,18 @@ module Deployments
|
|||
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
|
||||
# `create_deployment_in_separate_transaction` feature flag has been removed.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/348778
|
||||
|
||||
# If build.persisted_environment is a BatchLoader, we need to remove
|
||||
# the method proxy in order to clone into new item here
|
||||
# https://github.com/exAspArk/batch-loader/issues/31
|
||||
environment = if build.persisted_environment.respond_to?(:__sync)
|
||||
build.persisted_environment.__sync
|
||||
else
|
||||
build.persisted_environment
|
||||
end
|
||||
|
||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||
.new(build, build.persisted_environment).to_resource
|
||||
.new(build, environment).to_resource
|
||||
|
||||
return unless deployment
|
||||
|
||||
|
|
|
@ -256,7 +256,7 @@ module Members
|
|||
if user_by_email
|
||||
find_or_initialize_member_by_user(user_id: user_by_email.id)
|
||||
else
|
||||
source.members.build(invite_email: invitee)
|
||||
source.members_and_requesters.find_or_initialize_by(invite_email: invitee) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- if can_update_merge_request
|
||||
%p
|
||||
= _('Push commits to the source branch or add previously merged commits to review them.')
|
||||
%button.btn.gl-button.btn-confirm.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
|
||||
= render Pajamas::ButtonComponent.new(variant: 'confirm', button_options: { class: 'add-review-item-modal-trigger', data: { commits_empty: 'true', context_commits_empty: 'true' } }) do
|
||||
= _('Add previously merged commits')
|
||||
- else
|
||||
%ol#commits-list.list-unstyled
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
- add_page_specific_style 'page_bundles/work_items'
|
||||
|
||||
#js-work-items{ data: work_items_index_data(@project) }
|
||||
= render 'projects/invite_members_modal', project: @project
|
||||
|
|
|
@ -7,7 +7,9 @@ module WaitableWorker
|
|||
# Schedules multiple jobs and waits for them to be completed.
|
||||
def bulk_perform_and_wait(args_list, timeout: 10)
|
||||
# Short-circuit: it's more efficient to do small numbers of jobs inline
|
||||
return bulk_perform_inline(args_list) if args_list.size <= 3
|
||||
if args_list.size == 1 || (args_list.size <= 3 && !inline_refresh_only_for_single_element?)
|
||||
return bulk_perform_inline(args_list)
|
||||
end
|
||||
|
||||
# Don't wait if there's too many jobs to be waited for. Not including the
|
||||
# waiter allows them to be deduplicated and it skips waiting for jobs that
|
||||
|
@ -45,6 +47,10 @@ module WaitableWorker
|
|||
def async_only_refresh?
|
||||
Feature.enabled?(:async_only_project_authorizations_refresh)
|
||||
end
|
||||
|
||||
def inline_refresh_only_for_single_element?
|
||||
Feature.enabled?(:inline_project_authorizations_refresh_only_for_single_element)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(*args)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: inline_project_authorizations_refresh_only_for_single_element
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91572
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366815
|
||||
milestone: '15.2'
|
||||
type: development
|
||||
group: group::workspace
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: batched_migrations_health_status_autovacuum
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85196
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360331
|
||||
milestone: '15.2'
|
||||
type: ops
|
||||
group: group::database
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_pipeline_age_histogram
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90027
|
||||
rollout_issue_url:
|
||||
milestone: '15.1'
|
||||
type: ops
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
key_path: redis_hll_counters.manage.unque_active_users_monthly
|
||||
key_path: redis_hll_counters.manage.unique_active_users_monthly
|
||||
description: Users that have a last_activity_on date within the past 28 days
|
||||
product_section: dev
|
||||
product_stage: manage
|
|
@ -42,6 +42,8 @@ The following metrics are available:
|
|||
| `gitlab_ci_pipeline_builder_scoped_variables_duration` | Histogram | 14.5 | Time in seconds it takes to create the scoped variables for a CI/CD job
|
||||
| `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | |
|
||||
| `gitlab_ci_pipeline_size_builds` | Histogram | 13.1 | Total number of builds within a pipeline grouped by a pipeline source | `source` |
|
||||
| `gitlab_ci_runner_authentication_success_total` | Counter | 15.2 | Total number of times that runner authentication has succeeded | `type` |
|
||||
| `gitlab_ci_runner_authentication_failure_total` | Counter | 15.2 | Total number of times that runner authentication has failed
|
||||
| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |
|
||||
| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | `worker` |
|
||||
| `gitlab_ci_active_jobs` | Histogram | 14.2 | Count of active jobs when pipeline is created | |
|
||||
|
|
|
@ -12,6 +12,7 @@ module API
|
|||
JOB_TOKEN_PARAM = :token
|
||||
|
||||
def authenticate_runner!
|
||||
track_runner_authentication
|
||||
forbidden! unless current_runner
|
||||
|
||||
current_runner
|
||||
|
@ -42,6 +43,14 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
def track_runner_authentication
|
||||
if current_runner
|
||||
metrics.increment_runner_authentication_success_counter(runner_type: current_runner.runner_type)
|
||||
else
|
||||
metrics.increment_runner_authentication_failure_counter
|
||||
end
|
||||
end
|
||||
|
||||
# HTTP status codes to terminate the job on GitLab Runner:
|
||||
# - 403
|
||||
def authenticate_job!(require_running: true, heartbeat_runner: false)
|
||||
|
@ -149,6 +158,10 @@ module API
|
|||
def request_using_running_job_token?
|
||||
current_job.present? && current_authenticated_job.present? && current_job != current_authenticated_job
|
||||
end
|
||||
|
||||
def metrics
|
||||
strong_memoize(:metrics) { ::Gitlab::Ci::Runner::Metrics.new }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,6 +42,15 @@ module Gitlab
|
|||
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
|
||||
end
|
||||
|
||||
def self.pipeline_age_histogram
|
||||
name = :gitlab_ci_pipeline_age_minutes
|
||||
comment = 'Pipeline age histogram'
|
||||
buckets = [5, 30, 120, 720, 1440, 7200, 21600, 43200, 86400, 172800, 518400, 1036800]
|
||||
# 5m 30m 2h 12h 24h 5d 15d 30d 60d 180d 360d 2y
|
||||
|
||||
::Gitlab::Metrics.histogram(name, comment, {}, buckets)
|
||||
end
|
||||
|
||||
def self.active_jobs_histogram
|
||||
name = :gitlab_ci_active_jobs
|
||||
comment = 'Total amount of active jobs'
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Runner
|
||||
class Metrics
|
||||
extend Gitlab::Utils::StrongMemoize
|
||||
|
||||
def increment_runner_authentication_success_counter(runner_type: 'unknown_type')
|
||||
raise ArgumentError, "unknown runner type: #{runner_type}" unless
|
||||
::Ci::Runner.runner_types.include? runner_type
|
||||
|
||||
self.class.runner_authentication_success_counter.increment(runner_type: runner_type)
|
||||
end
|
||||
|
||||
def increment_runner_authentication_failure_counter
|
||||
self.class.runner_authentication_failure_counter.increment
|
||||
end
|
||||
|
||||
def self.runner_authentication_success_counter
|
||||
strong_memoize(:runner_authentication_success) do
|
||||
name = :gitlab_ci_runner_authentication_success_total
|
||||
comment = 'Runner authentication success'
|
||||
labels = { runner_type: nil }
|
||||
|
||||
::Gitlab::Metrics.counter(name, comment, labels)
|
||||
end
|
||||
end
|
||||
|
||||
def self.runner_authentication_failure_counter
|
||||
strong_memoize(:runner_authentication_failure) do
|
||||
name = :gitlab_ci_runner_authentication_failure_total
|
||||
comment = 'Runner authentication failure'
|
||||
|
||||
::Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -199,10 +199,32 @@ module Gitlab
|
|||
BatchOptimizer.new(self).optimize!
|
||||
end
|
||||
|
||||
def health_context
|
||||
HealthStatus::Context.new([table_name])
|
||||
end
|
||||
|
||||
def hold!(until_time: 10.minutes.from_now)
|
||||
duration_s = (until_time - Time.current).round
|
||||
Gitlab::AppLogger.info(
|
||||
message: "#{self} put on hold until #{until_time}",
|
||||
migration_id: id,
|
||||
job_class_name: job_class_name,
|
||||
duration_s: duration_s
|
||||
)
|
||||
|
||||
update!(on_hold_until: until_time)
|
||||
end
|
||||
|
||||
def on_hold?
|
||||
return false unless on_hold_until
|
||||
|
||||
on_hold_until > Time.zone.now
|
||||
end
|
||||
|
||||
def to_s
|
||||
"BatchedMigration[id: #{id}]"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_batched_jobs_status
|
||||
|
|
|
@ -29,7 +29,8 @@ module Gitlab
|
|||
if next_batched_job = find_or_create_next_batched_job(active_migration)
|
||||
migration_wrapper.perform(next_batched_job)
|
||||
|
||||
active_migration.optimize!
|
||||
adjust_migration(active_migration)
|
||||
|
||||
active_migration.failure! if next_batched_job.failed? && active_migration.should_stop?
|
||||
else
|
||||
finish_active_migration(active_migration)
|
||||
|
@ -139,6 +140,16 @@ module Gitlab
|
|||
migration.reload_last_job
|
||||
end
|
||||
end
|
||||
|
||||
def adjust_migration(active_migration)
|
||||
signal = HealthStatus.evaluate(active_migration)
|
||||
|
||||
if signal.is_a?(HealthStatus::Signals::Stop)
|
||||
active_migration.hold!
|
||||
else
|
||||
active_migration.optimize!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
module HealthStatus
|
||||
# Rather than passing along the migration, we use a more explicitly defined context
|
||||
Context = Struct.new(:tables)
|
||||
|
||||
def self.evaluate(migration, indicator = Indicators::AutovacuumActiveOnTable)
|
||||
signal = begin
|
||||
indicator.new(migration.health_context).evaluate
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, migration_id: migration.id,
|
||||
job_class_name: migration.job_class_name)
|
||||
Signals::Unknown.new(indicator, reason: "unexpected error: #{e.message} (#{e.class})")
|
||||
end
|
||||
|
||||
log_signal(signal, migration) if signal.log_info?
|
||||
|
||||
signal
|
||||
end
|
||||
|
||||
def self.log_signal(signal, migration)
|
||||
Gitlab::AppLogger.info(
|
||||
message: "#{migration} signaled: #{signal}",
|
||||
migration_id: migration.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
module HealthStatus
|
||||
module Indicators
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
module HealthStatus
|
||||
module Indicators
|
||||
class AutovacuumActiveOnTable
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
def evaluate
|
||||
return Signals::NotAvailable.new(self.class, reason: 'indicator disabled') unless enabled?
|
||||
|
||||
autovacuum_active_on = active_autovacuums_for(context.tables)
|
||||
|
||||
if autovacuum_active_on.empty?
|
||||
Signals::Normal.new(self.class, reason: 'no autovacuum running on any relevant tables')
|
||||
else
|
||||
Signals::Stop.new(self.class, reason: "autovacuum running on: #{autovacuum_active_on.join(', ')}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def enabled?
|
||||
Feature.enabled?(:batched_migrations_health_status_autovacuum, type: :ops)
|
||||
end
|
||||
|
||||
def active_autovacuums_for(tables)
|
||||
Gitlab::Database::PostgresAutovacuumActivity.for_tables(tables)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module BackgroundMigration
|
||||
module HealthStatus
|
||||
module Signals
|
||||
# Base class for a signal
|
||||
class Base
|
||||
attr_reader :indicator_class, :reason
|
||||
|
||||
def initialize(indicator_class, reason:)
|
||||
@indicator_class = indicator_class
|
||||
@reason = reason
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{short_name} (indicator: #{indicator_class}; reason: #{reason})"
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
def log_info?
|
||||
false
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
private
|
||||
|
||||
def short_name
|
||||
self.class.name.demodulize
|
||||
end
|
||||
end
|
||||
|
||||
# A Signals::Stop is an indication to put a migration on hold or stop it entirely:
|
||||
# In general, we want to slow down or pause the migration.
|
||||
class Stop < Base
|
||||
# :nocov:
|
||||
def log_info?
|
||||
true
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
|
||||
# A Signals::Normal indicates normal system state: We carry on with the migration
|
||||
# and may even attempt to optimize its throughput etc.
|
||||
class Normal < Base; end
|
||||
|
||||
# When given an Signals::Unknown, something unexpected happened while
|
||||
# we evaluated system indicators.
|
||||
class Unknown < Base
|
||||
# :nocov:
|
||||
def log_info?
|
||||
true
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
|
||||
# No signal could be determined, e.g. because the indicator
|
||||
# was disabled.
|
||||
class NotAvailable < Base; end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
class PostgresAutovacuumActivity < SharedModel
|
||||
self.table_name = 'postgres_autovacuum_activity'
|
||||
self.primary_key = 'table_identifier'
|
||||
|
||||
def self.for_tables(tables)
|
||||
Gitlab::Database::LoadBalancing::Session.current.use_primary do
|
||||
where('schema = current_schema()').where(table: tables)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"table #{table_identifier} (started: #{vacuum_start})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5129,6 +5129,9 @@ msgstr ""
|
|||
msgid "Assign milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign myself"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign reviewer"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
source scripts/utils.sh
|
||||
|
||||
function setup_gcp_dependencies() {
|
||||
apk add jq
|
||||
apt-get update && apt-get install -y jq
|
||||
|
||||
gcloud auth activate-service-account --key-file="${REVIEW_APPS_GCP_CREDENTIALS}"
|
||||
gcloud config set project "${REVIEW_APPS_GCP_PROJECT}"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :postgres_autovacuum_activity, class: 'Gitlab::Database::PostgresAutovacuumActivity' do
|
||||
table_identifier { "#{schema}.#{table}" }
|
||||
schema { 'public' }
|
||||
table { 'projects' }
|
||||
vacuum_start { Time.zone.now - 3.minutes }
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ FactoryBot.define do
|
|||
trait(:maintainer) { group_access { Gitlab::Access::MAINTAINER } }
|
||||
|
||||
after(:create) do |project_group_link, evaluator|
|
||||
project_group_link.group.refresh_members_authorized_projects
|
||||
AuthorizedProjectUpdate::ProjectRecalculateService.new(project_group_link.project).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,7 +98,9 @@ FactoryBot.define do
|
|||
project.add_owner(project.first_owner)
|
||||
end
|
||||
|
||||
project.group&.refresh_members_authorized_projects
|
||||
if project.group
|
||||
AuthorizedProjectUpdate::ProjectRecalculateService.new(project).execute
|
||||
end
|
||||
|
||||
# assign the delegated `#ci_cd_settings` attributes after create
|
||||
project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
|
||||
|
|
|
@ -4,13 +4,22 @@ import VueApollo from 'vue-apollo';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stripTypenames } from 'helpers/graphql_helpers';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
|
||||
import { projectMembersResponse, mockAssignees, workItemQueryResponse } from '../mock_data';
|
||||
import {
|
||||
projectMembersResponseWithCurrentUser,
|
||||
mockAssignees,
|
||||
workItemQueryResponse,
|
||||
currentUserResponse,
|
||||
currentUserNullResponse,
|
||||
projectMembersResponseWithoutCurrentUser,
|
||||
} from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
@ -24,17 +33,31 @@ describe('WorkItemAssignees component', () => {
|
|||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
|
||||
const findEmptyState = () => wrapper.findByTestId('empty-state');
|
||||
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
|
||||
|
||||
const successSearchQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithCurrentUser);
|
||||
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
|
||||
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
|
||||
|
||||
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse);
|
||||
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
|
||||
const createComponent = ({
|
||||
assignees = mockAssignees,
|
||||
searchQueryHandler = successSearchQueryHandler,
|
||||
currentUserQueryHandler = successCurrentUserQueryHandler,
|
||||
} = {}) => {
|
||||
const apolloProvider = createMockApollo([[userSearchQuery, searchQueryHandler]], resolvers, {
|
||||
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
||||
});
|
||||
const apolloProvider = createMockApollo(
|
||||
[
|
||||
[userSearchQuery, searchQueryHandler],
|
||||
[currentUserQuery, currentUserQueryHandler],
|
||||
],
|
||||
resolvers,
|
||||
{
|
||||
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
||||
},
|
||||
);
|
||||
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: workItemQuery,
|
||||
|
@ -171,4 +194,95 @@ describe('WorkItemAssignees component', () => {
|
|||
expect.objectContaining({ search: searchKey }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show `Assign myself` button if current user is loading', () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show `Assign myself` button if work item has assignees', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does now show `Assign myself` button if user is not logged in', async () => {
|
||||
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when user is logged in and there are no assignees', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ assignees: [] });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('renders `Assign myself` button', async () => {
|
||||
findTokenSelector().trigger('mouseover');
|
||||
expect(findAssignSelfButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls update work item assignees mutation with current user as a variable on button click', () => {
|
||||
// TODO: replace this test as soon as we have a real mutation implemented
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementation(jest.fn());
|
||||
|
||||
findTokenSelector().trigger('mouseover');
|
||||
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
input: {
|
||||
assignees: [stripTypenames(currentUserResponse.data.currentUser)],
|
||||
id: workItemId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('moves current user to the top of dropdown items if user is a project member', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
...stripTypenames(currentUserResponse.data.currentUser),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when current user is not in the list of project members', () => {
|
||||
const searchQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithoutCurrentUser);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ searchQueryHandler });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('adds current user to the top of dropdown items', () => {
|
||||
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
|
||||
stripTypenames(currentUserResponse.data.currentUser),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add current user if search is not empty', async () => {
|
||||
findTokenSelector().vm.$emit('text-input', 'test');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTokenSelector().props('dropdownItems')[0]).not.toEqual(
|
||||
stripTypenames(currentUserResponse.data.currentUser),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -333,13 +333,25 @@ export const availableWorkItemsResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const projectMembersResponse = {
|
||||
export const projectMembersResponseWithCurrentUser = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: '1',
|
||||
__typename: 'Project',
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'user-2',
|
||||
user: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/5',
|
||||
avatarUrl: '/avatar2',
|
||||
name: 'rookie',
|
||||
username: 'rookie',
|
||||
webUrl: 'rookie',
|
||||
status: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'user-1',
|
||||
user: {
|
||||
|
@ -353,6 +365,19 @@ export const projectMembersResponse = {
|
|||
status: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const projectMembersResponseWithoutCurrentUser = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: '1',
|
||||
__typename: 'Project',
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'user-2',
|
||||
user: {
|
||||
|
@ -389,3 +414,23 @@ export const mockAssignees = [
|
|||
username: 'ruthfull',
|
||||
},
|
||||
];
|
||||
|
||||
export const currentUserResponse = {
|
||||
data: {
|
||||
currentUser: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: '/root',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const currentUserNullResponse = {
|
||||
data: {
|
||||
currentUser: null,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -66,4 +66,30 @@ RSpec.describe API::Ci::Helpers::Runner do
|
|||
expect(helper.current_runner).to eq(runner)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#track_runner_authentication', :prometheus do
|
||||
subject { helper.track_runner_authentication }
|
||||
|
||||
let(:runner) { create(:ci_runner, token: 'foo') }
|
||||
|
||||
it 'increments gitlab_ci_runner_authentication_success_total' do
|
||||
allow(helper).to receive(:params).and_return(token: runner.token)
|
||||
|
||||
success_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_success_counter
|
||||
failure_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_failure_counter
|
||||
expect { subject }.to change { success_counter.get(runner_type: 'instance_type') }.by(1)
|
||||
.and not_change { success_counter.get(runner_type: 'project_type') }
|
||||
.and not_change { failure_counter.get }
|
||||
end
|
||||
|
||||
it 'increments gitlab_ci_runner_authentication_failure_total' do
|
||||
allow(helper).to receive(:params).and_return(token: 'invalid')
|
||||
|
||||
success_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_success_counter
|
||||
failure_counter = ::Gitlab::Ci::Runner::Metrics.runner_authentication_failure_counter
|
||||
expect { subject }.to change { failure_counter.get }.by(1)
|
||||
.and not_change { success_counter.get(runner_type: 'instance_type') }
|
||||
.and not_change { success_counter.get(runner_type: 'project_type') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Runner::Metrics, :prometheus do
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#increment_runner_authentication_success_counter' do
|
||||
it 'increments count for same type' do
|
||||
expect { subject.increment_runner_authentication_success_counter(runner_type: 'instance_type') }
|
||||
.to change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') }.by(1)
|
||||
end
|
||||
|
||||
it 'does not increment count for different type' do
|
||||
expect { subject.increment_runner_authentication_success_counter(runner_type: 'group_type') }
|
||||
.to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'project_type') }
|
||||
end
|
||||
|
||||
it 'does not increment failure count' do
|
||||
expect { subject.increment_runner_authentication_success_counter(runner_type: 'project_type') }
|
||||
.to not_change { described_class.runner_authentication_failure_counter.get }
|
||||
end
|
||||
|
||||
it 'throws ArgumentError for invalid runner type' do
|
||||
expect { subject.increment_runner_authentication_success_counter(runner_type: 'unknown_type') }
|
||||
.to raise_error(ArgumentError, 'unknown runner type: unknown_type')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#increment_runner_authentication_failure_counter' do
|
||||
it 'increments count' do
|
||||
expect { subject.increment_runner_authentication_failure_counter }
|
||||
.to change { described_class.runner_authentication_failure_counter.get }.by(1)
|
||||
end
|
||||
|
||||
it 'does not increment success count' do
|
||||
expect { subject.increment_runner_authentication_failure_counter }
|
||||
.to not_change { described_class.runner_authentication_success_counter.get(runner_type: 'instance_type') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,6 +14,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
|
|||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::Database::BackgroundMigration::HealthStatus).to receive(:evaluate)
|
||||
.and_return(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal)
|
||||
end
|
||||
|
||||
describe '#run_migration_job' do
|
||||
shared_examples_for 'it has completed the migration' do
|
||||
it 'does not create and run a migration job' do
|
||||
|
@ -59,13 +64,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
|
|||
sub_batch_size: migration.sub_batch_size)
|
||||
end
|
||||
|
||||
it 'optimizes the migration after executing the job' do
|
||||
migration.update!(min_value: event1.id, max_value: event2.id)
|
||||
context 'migration health' do
|
||||
let(:health_status) { Gitlab::Database::BackgroundMigration::HealthStatus }
|
||||
let(:stop_signal) { health_status::Signals::Stop.new(:indicator, reason: 'Take a break') }
|
||||
let(:normal_signal) { health_status::Signals::Normal.new(:indicator, reason: 'All good') }
|
||||
let(:not_available_signal) { health_status::Signals::NotAvailable.new(:indicator, reason: 'Indicator is disabled') }
|
||||
let(:unknown_signal) { health_status::Signals::Unknown.new(:indicator, reason: 'Something went wrong') }
|
||||
|
||||
expect(migration_wrapper).to receive(:perform).ordered
|
||||
expect(migration).to receive(:optimize!).ordered
|
||||
before do
|
||||
migration.update!(min_value: event1.id, max_value: event2.id)
|
||||
expect(migration_wrapper).to receive(:perform)
|
||||
end
|
||||
|
||||
runner.run_migration_job(migration)
|
||||
it 'puts migration on hold on stop signal' do
|
||||
expect(health_status).to receive(:evaluate).and_return(stop_signal)
|
||||
|
||||
expect { runner.run_migration_job(migration) }.to change { migration.on_hold? }
|
||||
.from(false).to(true)
|
||||
end
|
||||
|
||||
it 'optimizes migration on normal signal' do
|
||||
expect(health_status).to receive(:evaluate).and_return(normal_signal)
|
||||
|
||||
expect(migration).to receive(:optimize!)
|
||||
|
||||
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
|
||||
end
|
||||
|
||||
it 'optimizes migration on no signal' do
|
||||
expect(health_status).to receive(:evaluate).and_return(not_available_signal)
|
||||
|
||||
expect(migration).to receive(:optimize!)
|
||||
|
||||
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
|
||||
end
|
||||
|
||||
it 'optimizes migration on unknown signal' do
|
||||
expect(health_status).to receive(:evaluate).and_return(unknown_signal)
|
||||
|
||||
expect(migration).to receive(:optimize!)
|
||||
|
||||
expect { runner.run_migration_job(migration) }.not_to change { migration.on_hold? }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -572,6 +572,30 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
|
|||
end
|
||||
end
|
||||
|
||||
describe '#on_hold?', :freeze_time do
|
||||
subject { migration.on_hold? }
|
||||
|
||||
let(:migration) { create(:batched_background_migration) }
|
||||
|
||||
it 'returns false if no on_hold_until is set' do
|
||||
migration.on_hold_until = nil
|
||||
|
||||
expect(subject).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns false if on_hold_until has passed' do
|
||||
migration.on_hold_until = 1.minute.ago
|
||||
|
||||
expect(subject).to be_falsey
|
||||
end
|
||||
|
||||
it 'returns true if on_hold_until is in the future' do
|
||||
migration.on_hold_until = 1.minute.from_now
|
||||
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_configuration' do
|
||||
let!(:attributes) do
|
||||
{
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::AutovacuumActiveOnTable do
|
||||
include Database::DatabaseHelpers
|
||||
|
||||
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
|
||||
|
||||
around do |example|
|
||||
Gitlab::Database::SharedModel.using_connection(connection) do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe '#evaluate' do
|
||||
subject { described_class.new(context).evaluate }
|
||||
|
||||
before do
|
||||
swapout_view_for_table(:postgres_autovacuum_activity)
|
||||
end
|
||||
|
||||
let(:context) { Gitlab::Database::BackgroundMigration::HealthStatus::Context.new(tables) }
|
||||
let(:tables) { [table] }
|
||||
let(:table) { 'users' }
|
||||
|
||||
context 'without autovacuum activity' do
|
||||
it 'returns Normal signal' do
|
||||
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Normal)
|
||||
end
|
||||
|
||||
it 'remembers the indicator class' do
|
||||
expect(subject.indicator_class).to eq(described_class)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with autovacuum activity' do
|
||||
before do
|
||||
create(:postgres_autovacuum_activity, table: table, table_identifier: "public.#{table}")
|
||||
end
|
||||
|
||||
it 'returns Stop signal' do
|
||||
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Stop)
|
||||
end
|
||||
|
||||
it 'explains why' do
|
||||
expect(subject.reason).to include('autovacuum running on: table public.users')
|
||||
end
|
||||
|
||||
it 'remembers the indicator class' do
|
||||
expect(subject.indicator_class).to eq(described_class)
|
||||
end
|
||||
|
||||
it 'returns NoSignal signal in case the feature flag is disabled' do
|
||||
stub_feature_flags(batched_migrations_health_status_autovacuum: false)
|
||||
|
||||
expect(subject).to be_a(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::NotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::BackgroundMigration::HealthStatus do
|
||||
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
|
||||
|
||||
around do |example|
|
||||
Gitlab::Database::SharedModel.using_connection(connection) do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe '.evaluate' do
|
||||
subject(:evaluate) { described_class.evaluate(migration, indicator_class) }
|
||||
|
||||
let(:migration) { build(:batched_background_migration, :active) }
|
||||
|
||||
let(:health_status) { 'Gitlab::Database::BackgroundMigration::HealthStatus' }
|
||||
let(:indicator_class) { class_double("#{health_status}::Indicators::AutovacuumActiveOnTable") }
|
||||
let(:indicator) { instance_double("#{health_status}::Indicators::AutovacuumActiveOnTable") }
|
||||
|
||||
before do
|
||||
allow(indicator_class).to receive(:new).with(migration.health_context).and_return(indicator)
|
||||
end
|
||||
|
||||
it 'returns a signal' do
|
||||
signal = instance_double("#{health_status}::Signals::Normal", log_info?: false)
|
||||
|
||||
expect(indicator).to receive(:evaluate).and_return(signal)
|
||||
|
||||
expect(evaluate).to eq(signal)
|
||||
end
|
||||
|
||||
it 'logs interesting signals' do
|
||||
signal = instance_double("#{health_status}::Signals::Stop", log_info?: true)
|
||||
|
||||
expect(indicator).to receive(:evaluate).and_return(signal)
|
||||
expect(described_class).to receive(:log_signal).with(signal, migration)
|
||||
|
||||
evaluate
|
||||
end
|
||||
|
||||
it 'does not log signals of no interest' do
|
||||
signal = instance_double("#{health_status}::Signals::Normal", log_info?: false)
|
||||
|
||||
expect(indicator).to receive(:evaluate).and_return(signal)
|
||||
expect(described_class).not_to receive(:log_signal)
|
||||
|
||||
evaluate
|
||||
end
|
||||
|
||||
context 'on indicator error' do
|
||||
let(:error) { RuntimeError.new('everything broken') }
|
||||
|
||||
before do
|
||||
expect(indicator).to receive(:evaluate).and_raise(error)
|
||||
end
|
||||
|
||||
it 'does not fail' do
|
||||
expect { evaluate }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'returns Unknown signal' do
|
||||
expect(evaluate).to be_an_instance_of(Gitlab::Database::BackgroundMigration::HealthStatus::Signals::Unknown)
|
||||
expect(evaluate.reason).to eq("unexpected error: everything broken (RuntimeError)")
|
||||
end
|
||||
|
||||
it 'reports the exception to error tracking' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
.with(error, migration_id: migration.id, job_class_name: migration.job_class_name)
|
||||
|
||||
evaluate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::PostgresAutovacuumActivity, type: :model do
|
||||
include Database::DatabaseHelpers
|
||||
|
||||
it { is_expected.to be_a Gitlab::Database::SharedModel }
|
||||
|
||||
describe '.for_tables' do
|
||||
subject { described_class.for_tables(tables) }
|
||||
|
||||
let(:tables) { %w[foo test] }
|
||||
|
||||
before do
|
||||
swapout_view_for_table(:postgres_autovacuum_activity)
|
||||
|
||||
# unrelated
|
||||
create(:postgres_autovacuum_activity, table: 'bar')
|
||||
|
||||
tables.each do |table|
|
||||
create(:postgres_autovacuum_activity, table: table)
|
||||
end
|
||||
|
||||
expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary).and_yield
|
||||
end
|
||||
|
||||
it 'returns autovacuum activity for queries tables' do
|
||||
expect(subject.map(&:table).sort).to eq(tables)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -118,6 +118,38 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'pipeline age metric' do
|
||||
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
|
||||
|
||||
let(:pipeline_age_histogram) do
|
||||
::Gitlab::Ci::Pipeline::Metrics.pipeline_age_histogram
|
||||
end
|
||||
|
||||
context 'when pipeline age histogram is enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_pipeline_age_histogram: true)
|
||||
end
|
||||
|
||||
it 'observes pipeline age' do
|
||||
expect(pipeline_age_histogram).to receive(:observe)
|
||||
|
||||
described_class.find(pipeline.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline age histogram is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_pipeline_age_histogram: false)
|
||||
end
|
||||
|
||||
it 'observes pipeline age' do
|
||||
expect(pipeline_age_histogram).not_to receive(:observe)
|
||||
|
||||
described_class.find(pipeline.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_status' do
|
||||
let(:pipeline) { build(:ci_empty_pipeline, :created) }
|
||||
|
||||
|
@ -4979,4 +5011,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#age_in_minutes' do
|
||||
let(:pipeline) { build(:ci_pipeline) }
|
||||
|
||||
context 'when pipeline has not been persisted' do
|
||||
it 'returns zero' do
|
||||
expect(pipeline.age_in_minutes).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has been saved' do
|
||||
it 'returns pipeline age in minutes' do
|
||||
pipeline.save!
|
||||
|
||||
travel_to(pipeline.created_at + 2.hours) do
|
||||
expect(pipeline.age_in_minutes).to eq 120
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has been loaded without all attributes' do
|
||||
it 'raises an exception' do
|
||||
pipeline.save!
|
||||
|
||||
pipeline_id = Ci::Pipeline.where(id: pipeline.id).select(:id).first
|
||||
|
||||
expect { pipeline_id.age_in_minutes }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -104,6 +104,7 @@ RSpec.describe 'factories' do
|
|||
factories_based_on_view = %i[
|
||||
postgres_index
|
||||
postgres_index_bloat_estimate
|
||||
postgres_autovacuum_activity
|
||||
].to_set.freeze
|
||||
|
||||
without_fd, with_fd = FactoryBot.factories
|
||||
|
|
|
@ -69,6 +69,20 @@ RSpec.describe API::Invitations do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when invitee is already an invited member' do
|
||||
it 'updates the member for that email' do
|
||||
member = source.add_developer(email)
|
||||
|
||||
expect do
|
||||
post invitations_url(source, maintainer),
|
||||
params: { email: email, access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.to change { member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
|
||||
.and not_change { source.members.invite.count }
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a new member by email' do
|
||||
expect do
|
||||
post invitations_url(source, maintainer),
|
||||
|
|
|
@ -25,6 +25,7 @@ RSpec.describe Deployments::CreateForBuildService do
|
|||
expect(build.deployment.deployable).to eq(build)
|
||||
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
||||
expect(build.deployment.environment).to eq(build.persisted_environment)
|
||||
expect(build.deployment.valid?).to be_truthy
|
||||
end
|
||||
|
||||
context 'when creation failure occures' do
|
||||
|
|
|
@ -601,8 +601,8 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
|||
}.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'performs authorizations job immediately' do
|
||||
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
|
||||
it 'performs authorizations job' do
|
||||
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
|
||||
|
||||
transfer_service.execute(new_parent_group)
|
||||
end
|
||||
|
|
|
@ -146,12 +146,14 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
|
|||
end
|
||||
|
||||
context 'when passing an existing invite user id' do
|
||||
let(:user_id) { create(:project_member, :invited, project: source).invite_email }
|
||||
let(:invited_member) { create(:project_member, :guest, :invited, project: source) }
|
||||
let(:user_id) { invited_member.invite_email }
|
||||
let(:access_level) { ProjectMember::MAINTAINER }
|
||||
|
||||
it 'does not add a member' do
|
||||
expect(execute_service[:status]).to eq(:error)
|
||||
expect(execute_service[:message]).to eq("The member's email address has already been taken")
|
||||
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
|
||||
it 'allows already invited members to be re-invited by email and updates the member access' do
|
||||
expect(execute_service[:status]).to eq(:success)
|
||||
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -353,15 +353,16 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
|
|||
|
||||
context 'when member already exists' do
|
||||
context 'with email' do
|
||||
let!(:invited_member) { create(:project_member, :invited, project: project) }
|
||||
let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
|
||||
let!(:invited_member) { create(:project_member, :guest, :invited, project: project) }
|
||||
let(:params) do
|
||||
{ email: "#{invited_member.invite_email},#{project_user.email}", access_level: ProjectMember::MAINTAINER }
|
||||
end
|
||||
|
||||
it 'adds new email and returns an error for the already invited email' do
|
||||
it 'adds new email and allows already invited members to be re-invited by email and updates the member access' do
|
||||
expect_to_create_members(count: 1)
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message][invited_member.invite_email])
|
||||
.to eq("The member's email address has already been taken")
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(project.users).to include project_user
|
||||
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,8 +4,10 @@ module Database
|
|||
module DatabaseHelpers
|
||||
# In order to directly work with views using factories,
|
||||
# we can swapout the view for a table of identical structure.
|
||||
def swapout_view_for_table(view)
|
||||
ActiveRecord::Base.connection.execute(<<~SQL.squish)
|
||||
def swapout_view_for_table(view, connection: nil)
|
||||
connection ||= ActiveRecord::Base.connection
|
||||
|
||||
connection.execute(<<~SQL.squish)
|
||||
CREATE TABLE #{view}_copy (LIKE #{view});
|
||||
DROP VIEW #{view};
|
||||
ALTER TABLE #{view}_copy RENAME TO #{view};
|
||||
|
|
|
@ -78,22 +78,21 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
|
|||
end
|
||||
|
||||
context 'when member is already a member by email' do
|
||||
it 'fails with an error', :js do
|
||||
it 'updates the member for that email', :js do
|
||||
visit members_page_path
|
||||
|
||||
invite_member('test@example.com', role: 'Developer')
|
||||
|
||||
invite_member('test@example.com', role: 'Reporter', refresh: false)
|
||||
|
||||
expect(page).to have_selector(invite_modal_selector)
|
||||
expect(page).to have_content("The member's email address has already been taken")
|
||||
expect(page).not_to have_selector(invite_modal_selector)
|
||||
|
||||
page.refresh
|
||||
|
||||
click_link 'Invited'
|
||||
|
||||
page.within find_invited_member_row('test@example.com') do
|
||||
expect(page).to have_button('Developer')
|
||||
expect(page).to have_button('Reporter')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -299,37 +299,51 @@ RSpec.shared_examples_for "member creation" do
|
|||
end
|
||||
|
||||
context 'when member already exists' do
|
||||
before do
|
||||
source.add_member(user, :developer)
|
||||
end
|
||||
context 'when member is a user' do
|
||||
before do
|
||||
source.add_member(user, :developer)
|
||||
end
|
||||
|
||||
context 'with no current_user' do
|
||||
it 'updates the member' do
|
||||
expect(source.users).to include(user)
|
||||
context 'with no current_user' do
|
||||
it 'updates the member' do
|
||||
expect(source.users).to include(user)
|
||||
|
||||
described_class.add_member(source, user, :maintainer)
|
||||
described_class.add_member(source, user, :maintainer)
|
||||
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user can update member', :enable_admin_mode do
|
||||
it 'updates the member' do
|
||||
expect(source.users).to include(user)
|
||||
|
||||
described_class.add_member(source, user, :maintainer, current_user: admin)
|
||||
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user cannot update member' do
|
||||
it 'does not update the member' do
|
||||
expect(source.users).to include(user)
|
||||
|
||||
described_class.add_member(source, user, :maintainer, current_user: user)
|
||||
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user can update member', :enable_admin_mode do
|
||||
it 'updates the member' do
|
||||
expect(source.users).to include(user)
|
||||
context 'when member is an invite by email' do
|
||||
let_it_be(:email) { 'user@email.com' }
|
||||
let_it_be(:existing_member) { source.add_developer(email) }
|
||||
|
||||
described_class.add_member(source, user, :maintainer, current_user: admin)
|
||||
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user cannot update member' do
|
||||
it 'does not update the member' do
|
||||
expect(source.users).to include(user)
|
||||
|
||||
described_class.add_member(source, user, :maintainer, current_user: user)
|
||||
|
||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
|
||||
it 'updates the member for that email' do
|
||||
expect do
|
||||
described_class.add_member(source, email, :maintainer)
|
||||
end.to change { existing_member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
|
||||
.and not_change { source.members.invite.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -229,6 +229,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
|
|||
|
||||
describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do
|
||||
include Gitlab::Database::DynamicModelHelpers
|
||||
include Database::DatabaseHelpers
|
||||
|
||||
let(:migration_class) do
|
||||
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
|
||||
|
@ -347,5 +348,20 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
|
|||
it 'does not update non-matching records in the range' do
|
||||
expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
|
||||
end
|
||||
|
||||
context 'health status' do
|
||||
subject(:migration_run) { described_class.new.perform }
|
||||
|
||||
it 'puts migration on hold when there is autovaccum activity on related tables' do
|
||||
swapout_view_for_table(:postgres_autovacuum_activity, connection: connection)
|
||||
create(
|
||||
:postgres_autovacuum_activity,
|
||||
table: migration.table_name,
|
||||
table_identifier: "public.#{migration.table_name}"
|
||||
)
|
||||
|
||||
expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,54 +29,81 @@ RSpec.describe WaitableWorker do
|
|||
subject(:job) { worker.new }
|
||||
|
||||
describe '.bulk_perform_and_wait' do
|
||||
it 'schedules the jobs and waits for them to complete' do
|
||||
worker.bulk_perform_and_wait([[1], [2]])
|
||||
context '1 job' do
|
||||
it 'inlines the job' do
|
||||
args_list = [[1]]
|
||||
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
|
||||
expect(Gitlab::AppJsonLogger).to(
|
||||
receive(:info).with(a_hash_including('message' => 'running inline',
|
||||
'class' => 'Gitlab::Foo::Bar::DummyWorker',
|
||||
'job_status' => 'running',
|
||||
'queue' => 'foo_bar_dummy'))
|
||||
.once)
|
||||
|
||||
expect(worker.counter).to eq(3)
|
||||
end
|
||||
worker.bulk_perform_and_wait(args_list)
|
||||
|
||||
it 'inlines workloads <= 3 jobs' do
|
||||
args_list = [[1], [2], [3]]
|
||||
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
|
||||
expect(Gitlab::AppJsonLogger).to(
|
||||
receive(:info).with(a_hash_including('message' => 'running inline',
|
||||
'class' => 'Gitlab::Foo::Bar::DummyWorker',
|
||||
'job_status' => 'running',
|
||||
'queue' => 'foo_bar_dummy'))
|
||||
.exactly(3).times)
|
||||
|
||||
worker.bulk_perform_and_wait(args_list)
|
||||
|
||||
expect(worker.counter).to eq(6)
|
||||
end
|
||||
|
||||
context 'when the feature flag `async_only_project_authorizations_refresh` is turned off' do
|
||||
before do
|
||||
stub_feature_flags(async_only_project_authorizations_refresh: false)
|
||||
end
|
||||
|
||||
it 'runs > 3 jobs using sidekiq and a waiter key' do
|
||||
expect(worker).to receive(:bulk_perform_async)
|
||||
.with([[1, anything], [2, anything], [3, anything], [4, anything]])
|
||||
|
||||
worker.bulk_perform_and_wait([[1], [2], [3], [4]])
|
||||
expect(worker.counter).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'runs > 3 jobs using sidekiq and no waiter key' do
|
||||
arguments = 1.upto(5).map { |i| [i] }
|
||||
context 'between 2 and 3 jobs' do
|
||||
it 'runs the jobs asynchronously' do
|
||||
arguments = [[1], [2], [3]]
|
||||
|
||||
expect(worker).to receive(:bulk_perform_async).with(arguments)
|
||||
expect(worker).to receive(:bulk_perform_async).with(arguments)
|
||||
|
||||
worker.bulk_perform_and_wait(arguments, timeout: 2)
|
||||
worker.bulk_perform_and_wait(arguments)
|
||||
end
|
||||
|
||||
context 'when the feature flag `inline_project_authorizations_refresh_only_for_single_element` is turned off' do
|
||||
before do
|
||||
stub_feature_flags(inline_project_authorizations_refresh_only_for_single_element: false)
|
||||
end
|
||||
|
||||
it 'inlines the jobs' do
|
||||
args_list = [[1], [2], [3]]
|
||||
expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
|
||||
expect(Gitlab::AppJsonLogger).to(
|
||||
receive(:info).with(a_hash_including('message' => 'running inline',
|
||||
'class' => 'Gitlab::Foo::Bar::DummyWorker',
|
||||
'job_status' => 'running',
|
||||
'queue' => 'foo_bar_dummy'))
|
||||
.exactly(3).times)
|
||||
|
||||
worker.bulk_perform_and_wait(args_list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'runs > 10 * timeout jobs using sidekiq and no waiter key' do
|
||||
arguments = 1.upto(21).map { |i| [i] }
|
||||
context '>= 4 jobs' do
|
||||
it 'runs jobs using sidekiq and no waiter key' do
|
||||
arguments = 1.upto(5).map { |i| [i] }
|
||||
|
||||
expect(worker).to receive(:bulk_perform_async).with(arguments)
|
||||
expect(worker).to receive(:bulk_perform_async).with(arguments)
|
||||
|
||||
worker.bulk_perform_and_wait(arguments, timeout: 2)
|
||||
worker.bulk_perform_and_wait(arguments, timeout: 2)
|
||||
end
|
||||
|
||||
it 'runs > 10 * timeout jobs using sidekiq and no waiter key' do
|
||||
arguments = 1.upto(21).map { |i| [i] }
|
||||
|
||||
expect(worker).to receive(:bulk_perform_async).with(arguments)
|
||||
|
||||
worker.bulk_perform_and_wait(arguments, timeout: 2)
|
||||
end
|
||||
|
||||
context 'when the feature flag `async_only_project_authorizations_refresh` is turned off' do
|
||||
before do
|
||||
stub_feature_flags(async_only_project_authorizations_refresh: false)
|
||||
end
|
||||
|
||||
it 'runs > 3 jobs using sidekiq and a waiter key' do
|
||||
expect(worker).to receive(:bulk_perform_async)
|
||||
.with([[1, anything], [2, anything], [3, anything], [4, anything]])
|
||||
|
||||
worker.bulk_perform_and_wait([[1], [2], [3], [4]])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue