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/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/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/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/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/admin/impersonation_tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||||
/app/assets/javascripts/pages/groups/settings/access_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:
|
extends:
|
||||||
- .default-retry
|
- .default-retry
|
||||||
- .review:rules:review-cleanup
|
- .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
|
stage: prepare
|
||||||
environment:
|
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
|
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:
|
rules:
|
||||||
- <<: *if-not-ee
|
- <<: *if-not-ee
|
||||||
when: never
|
when: never
|
||||||
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
- <<: *if-dot-com-gitlab-org-schedule
|
- <<: *if-dot-com-gitlab-org-schedule
|
||||||
allow_failure: true
|
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 { initWorkItemsRoot } from '~/work_items/index';
|
||||||
|
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||||
|
|
||||||
initWorkItemsRoot();
|
initWorkItemsRoot();
|
||||||
|
initInviteMembersModal();
|
||||||
|
|
|
@ -2,7 +2,7 @@ import produce from 'immer';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
|
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
|
||||||
import createDefaultClient from '~/lib/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 = {
|
const resolvers = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
@ -13,6 +13,7 @@ const resolvers = {
|
||||||
});
|
});
|
||||||
cache.writeQuery({ query: getIssueStateQuery, data });
|
cache.writeQuery({ query: getIssueStateQuery, data });
|
||||||
},
|
},
|
||||||
|
...workItemResolvers.Mutation,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
<script>
|
<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 { debounce } from 'lodash';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
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 userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||||
|
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||||
import { n__ } from '~/locale';
|
import { n__ } from '~/locale';
|
||||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||||
|
@ -27,7 +38,11 @@ export default {
|
||||||
GlAvatar,
|
GlAvatar,
|
||||||
GlLink,
|
GlLink,
|
||||||
GlSkeletonLoader,
|
GlSkeletonLoader,
|
||||||
|
GlButton,
|
||||||
SidebarParticipant,
|
SidebarParticipant,
|
||||||
|
InviteMembersTrigger,
|
||||||
|
GlDropdownItem,
|
||||||
|
GlDropdownDivider,
|
||||||
},
|
},
|
||||||
inject: ['fullPath'],
|
inject: ['fullPath'],
|
||||||
props: {
|
props: {
|
||||||
|
@ -47,6 +62,7 @@ export default {
|
||||||
localAssignees: this.assignees.map(addClass),
|
localAssignees: this.assignees.map(addClass),
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
searchUsers: [],
|
searchUsers: [],
|
||||||
|
currentUser: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -70,6 +86,9 @@ export default {
|
||||||
this.$emit('error', i18n.fetchError);
|
this.$emit('error', i18n.fetchError);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
currentUser: {
|
||||||
|
query: currentUserQuery,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
assigneeListEmpty() {
|
assigneeListEmpty() {
|
||||||
|
@ -78,12 +97,24 @@ export default {
|
||||||
containerClass() {
|
containerClass() {
|
||||||
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
|
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
|
||||||
},
|
},
|
||||||
isLoading() {
|
isLoadingUsers() {
|
||||||
return this.$apollo.queries.searchUsers.loading;
|
return this.$apollo.queries.searchUsers.loading;
|
||||||
},
|
},
|
||||||
assigneeText() {
|
assigneeText() {
|
||||||
return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
|
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: {
|
watch: {
|
||||||
assignees(newVal) {
|
assignees(newVal) {
|
||||||
|
@ -99,15 +130,18 @@ export default {
|
||||||
getUserId(id) {
|
getUserId(id) {
|
||||||
return getIdFromGraphQLId(id);
|
return getIdFromGraphQLId(id);
|
||||||
},
|
},
|
||||||
setAssignees(e) {
|
handleBlur(e) {
|
||||||
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
|
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
|
this.setAssignees(this.localAssignees);
|
||||||
|
},
|
||||||
|
setAssignees(assignees) {
|
||||||
this.$apollo.mutate({
|
this.$apollo.mutate({
|
||||||
mutation: localUpdateWorkItemMutation,
|
mutation: localUpdateWorkItemMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: this.workItemId,
|
id: this.workItemId,
|
||||||
assignees: this.localAssignees,
|
assignees,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -132,6 +166,15 @@ export default {
|
||||||
setSearchKey(value) {
|
setSearchKey(value) {
|
||||||
this.searchKey = 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>
|
</script>
|
||||||
|
@ -147,13 +190,13 @@ export default {
|
||||||
ref="tokenSelector"
|
ref="tokenSelector"
|
||||||
v-model="localAssignees"
|
v-model="localAssignees"
|
||||||
:container-class="containerClass"
|
: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"
|
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="searchUsers"
|
:dropdown-items="dropdownItems"
|
||||||
:loading="isLoading"
|
:loading="isLoadingUsers"
|
||||||
@input="focusTokenSelector"
|
@input="focusTokenSelector"
|
||||||
@text-input="debouncedSearchKeyUpdate"
|
@text-input="debouncedSearchKeyUpdate"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="setAssignees"
|
@blur="handleBlur"
|
||||||
@mouseover.native="handleMouseOver"
|
@mouseover.native="handleMouseOver"
|
||||||
@mouseout.native="handleMouseOut"
|
@mouseout.native="handleMouseOut"
|
||||||
>
|
>
|
||||||
|
@ -163,7 +206,15 @@ export default {
|
||||||
data-testid="empty-state"
|
data-testid="empty-state"
|
||||||
>
|
>
|
||||||
<gl-icon name="profile" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #token-content="{ token }">
|
<template #token-content="{ token }">
|
||||||
|
@ -189,6 +240,18 @@ export default {
|
||||||
<rect width="280" height="20" x="10" y="130" rx="4" />
|
<rect width="280" height="20" x="10" y="130" rx="4" />
|
||||||
</gl-skeleton-loader>
|
</gl-skeleton-loader>
|
||||||
</template>
|
</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>
|
</gl-token-selector>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const temporaryConfig = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
__typename: 'UserCore',
|
__typename: 'UserCore',
|
||||||
id: 'gid://gitlab/User/1',
|
id: 'gid://gitlab/User/10001',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
webUrl: '',
|
webUrl: '',
|
||||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
@ -34,7 +34,7 @@ export const temporaryConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'UserCore',
|
__typename: 'UserCore',
|
||||||
id: 'gid://gitlab/User/2',
|
id: 'gid://gitlab/User/10002',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
webUrl: '',
|
webUrl: '',
|
||||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
|
|
@ -13,3 +13,13 @@
|
||||||
#weight-widget-input[readonly] {
|
#weight-widget-input[readonly] {
|
||||||
background-color: var(--white, $white);
|
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
|
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
|
||||||
|
|
||||||
after_create :keep_around_commits, unless: :importing?
|
after_create :keep_around_commits, unless: :importing?
|
||||||
|
after_find :observe_age_in_minutes, unless: :importing?
|
||||||
|
|
||||||
use_fast_destroy :job_artifacts
|
use_fast_destroy :job_artifacts
|
||||||
use_fast_destroy :build_trace_chunks
|
use_fast_destroy :build_trace_chunks
|
||||||
|
@ -1322,6 +1323,16 @@ module Ci
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def add_message(severity, content)
|
def add_message(severity, content)
|
||||||
|
@ -1372,6 +1383,21 @@ module Ci
|
||||||
project.repository.keep_around(self.sha, self.before_sha)
|
project.repository.keep_around(self.sha, self.before_sha)
|
||||||
end
|
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.
|
# Without using `unscoped`, caller scope is also included into the query.
|
||||||
# Using `unscoped` here will be redundant after Rails 6.1
|
# Using `unscoped` here will be redundant after Rails 6.1
|
||||||
def object_hierarchy(options = {})
|
def object_hierarchy(options = {})
|
||||||
|
|
|
@ -11,8 +11,18 @@ module Deployments
|
||||||
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
|
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
|
||||||
# `create_deployment_in_separate_transaction` feature flag has been removed.
|
# `create_deployment_in_separate_transaction` feature flag has been removed.
|
||||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/348778
|
# 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
|
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||||
.new(build, build.persisted_environment).to_resource
|
.new(build, environment).to_resource
|
||||||
|
|
||||||
return unless deployment
|
return unless deployment
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,7 @@ module Members
|
||||||
if user_by_email
|
if user_by_email
|
||||||
find_or_initialize_member_by_user(user_id: user_by_email.id)
|
find_or_initialize_member_by_user(user_id: user_by_email.id)
|
||||||
else
|
else
|
||||||
source.members.build(invite_email: invitee)
|
source.members_and_requesters.find_or_initialize_by(invite_email: invitee) # rubocop:disable CodeReuse/ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
- if can_update_merge_request
|
- if can_update_merge_request
|
||||||
%p
|
%p
|
||||||
= _('Push commits to the source branch or add previously merged commits to review them.')
|
= _('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')
|
= _('Add previously merged commits')
|
||||||
- else
|
- else
|
||||||
%ol#commits-list.list-unstyled
|
%ol#commits-list.list-unstyled
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
- add_page_specific_style 'page_bundles/work_items'
|
- add_page_specific_style 'page_bundles/work_items'
|
||||||
|
|
||||||
#js-work-items{ data: work_items_index_data(@project) }
|
#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.
|
# Schedules multiple jobs and waits for them to be completed.
|
||||||
def bulk_perform_and_wait(args_list, timeout: 10)
|
def bulk_perform_and_wait(args_list, timeout: 10)
|
||||||
# Short-circuit: it's more efficient to do small numbers of jobs inline
|
# 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
|
# 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
|
# waiter allows them to be deduplicated and it skips waiting for jobs that
|
||||||
|
@ -45,6 +47,10 @@ module WaitableWorker
|
||||||
def async_only_refresh?
|
def async_only_refresh?
|
||||||
Feature.enabled?(:async_only_project_authorizations_refresh)
|
Feature.enabled?(:async_only_project_authorizations_refresh)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inline_refresh_only_for_single_element?
|
||||||
|
Feature.enabled?(:inline_project_authorizations_refresh_only_for_single_element)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(*args)
|
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
|
description: Users that have a last_activity_on date within the past 28 days
|
||||||
product_section: dev
|
product_section: dev
|
||||||
product_stage: manage
|
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_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_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_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_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` |
|
| `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 | |
|
| `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
|
JOB_TOKEN_PARAM = :token
|
||||||
|
|
||||||
def authenticate_runner!
|
def authenticate_runner!
|
||||||
|
track_runner_authentication
|
||||||
forbidden! unless current_runner
|
forbidden! unless current_runner
|
||||||
|
|
||||||
current_runner
|
current_runner
|
||||||
|
@ -42,6 +43,14 @@ module API
|
||||||
end
|
end
|
||||||
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:
|
# HTTP status codes to terminate the job on GitLab Runner:
|
||||||
# - 403
|
# - 403
|
||||||
def authenticate_job!(require_running: true, heartbeat_runner: false)
|
def authenticate_job!(require_running: true, heartbeat_runner: false)
|
||||||
|
@ -149,6 +158,10 @@ module API
|
||||||
def request_using_running_job_token?
|
def request_using_running_job_token?
|
||||||
current_job.present? && current_authenticated_job.present? && current_job != current_authenticated_job
|
current_job.present? && current_authenticated_job.present? && current_job != current_authenticated_job
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def metrics
|
||||||
|
strong_memoize(:metrics) { ::Gitlab::Ci::Runner::Metrics.new }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,6 +42,15 @@ module Gitlab
|
||||||
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
|
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
|
||||||
end
|
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
|
def self.active_jobs_histogram
|
||||||
name = :gitlab_ci_active_jobs
|
name = :gitlab_ci_active_jobs
|
||||||
comment = 'Total amount of 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!
|
BatchOptimizer.new(self).optimize!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def health_context
|
||||||
|
HealthStatus::Context.new([table_name])
|
||||||
|
end
|
||||||
|
|
||||||
def hold!(until_time: 10.minutes.from_now)
|
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)
|
update!(on_hold_until: until_time)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def validate_batched_jobs_status
|
def validate_batched_jobs_status
|
||||||
|
|
|
@ -29,7 +29,8 @@ module Gitlab
|
||||||
if next_batched_job = find_or_create_next_batched_job(active_migration)
|
if next_batched_job = find_or_create_next_batched_job(active_migration)
|
||||||
migration_wrapper.perform(next_batched_job)
|
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?
|
active_migration.failure! if next_batched_job.failed? && active_migration.should_stop?
|
||||||
else
|
else
|
||||||
finish_active_migration(active_migration)
|
finish_active_migration(active_migration)
|
||||||
|
@ -139,6 +140,16 @@ module Gitlab
|
||||||
migration.reload_last_job
|
migration.reload_last_job
|
||||||
end
|
end
|
||||||
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
|
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"
|
msgid "Assign milestone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Assign myself"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Assign reviewer"
|
msgid "Assign reviewer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
source scripts/utils.sh
|
source scripts/utils.sh
|
||||||
|
|
||||||
function setup_gcp_dependencies() {
|
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 auth activate-service-account --key-file="${REVIEW_APPS_GCP_CREDENTIALS}"
|
||||||
gcloud config set project "${REVIEW_APPS_GCP_PROJECT}"
|
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 } }
|
trait(:maintainer) { group_access { Gitlab::Access::MAINTAINER } }
|
||||||
|
|
||||||
after(:create) do |project_group_link, evaluator|
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -98,7 +98,9 @@ FactoryBot.define do
|
||||||
project.add_owner(project.first_owner)
|
project.add_owner(project.first_owner)
|
||||||
end
|
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
|
# assign the delegated `#ci_cd_settings` attributes after create
|
||||||
project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
|
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 createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
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 { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
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 workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||||
import { i18n } from '~/work_items/constants';
|
import { i18n } from '~/work_items/constants';
|
||||||
import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
|
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);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
@ -24,17 +33,31 @@ describe('WorkItemAssignees component', () => {
|
||||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||||
|
|
||||||
const findEmptyState = () => wrapper.findByTestId('empty-state');
|
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 errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||||
|
|
||||||
const createComponent = ({
|
const createComponent = ({
|
||||||
assignees = mockAssignees,
|
assignees = mockAssignees,
|
||||||
searchQueryHandler = successSearchQueryHandler,
|
searchQueryHandler = successSearchQueryHandler,
|
||||||
|
currentUserQueryHandler = successCurrentUserQueryHandler,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const apolloProvider = createMockApollo([[userSearchQuery, searchQueryHandler]], resolvers, {
|
const apolloProvider = createMockApollo(
|
||||||
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
[
|
||||||
});
|
[userSearchQuery, searchQueryHandler],
|
||||||
|
[currentUserQuery, currentUserQueryHandler],
|
||||||
|
],
|
||||||
|
resolvers,
|
||||||
|
{
|
||||||
|
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
apolloProvider.clients.defaultClient.writeQuery({
|
apolloProvider.clients.defaultClient.writeQuery({
|
||||||
query: workItemQuery,
|
query: workItemQuery,
|
||||||
|
@ -171,4 +194,95 @@ describe('WorkItemAssignees component', () => {
|
||||||
expect.objectContaining({ search: searchKey }),
|
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: {
|
data: {
|
||||||
workspace: {
|
workspace: {
|
||||||
id: '1',
|
id: '1',
|
||||||
__typename: 'Project',
|
__typename: 'Project',
|
||||||
users: {
|
users: {
|
||||||
nodes: [
|
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',
|
id: 'user-1',
|
||||||
user: {
|
user: {
|
||||||
|
@ -353,6 +365,19 @@ export const projectMembersResponse = {
|
||||||
status: null,
|
status: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectMembersResponseWithoutCurrentUser = {
|
||||||
|
data: {
|
||||||
|
workspace: {
|
||||||
|
id: '1',
|
||||||
|
__typename: 'Project',
|
||||||
|
users: {
|
||||||
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'user-2',
|
id: 'user-2',
|
||||||
user: {
|
user: {
|
||||||
|
@ -389,3 +414,23 @@ export const mockAssignees = [
|
||||||
username: 'ruthfull',
|
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)
|
expect(helper.current_runner).to eq(runner)
|
||||||
end
|
end
|
||||||
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
|
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
|
||||||
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
|
describe '#run_migration_job' do
|
||||||
shared_examples_for 'it has completed the migration' do
|
shared_examples_for 'it has completed the migration' do
|
||||||
it 'does not create and run a migration job' 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)
|
sub_batch_size: migration.sub_batch_size)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'optimizes the migration after executing the job' do
|
context 'migration health' do
|
||||||
migration.update!(min_value: event1.id, max_value: event2.id)
|
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
|
before do
|
||||||
expect(migration).to receive(:optimize!).ordered
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -572,6 +572,30 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
|
||||||
end
|
end
|
||||||
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
|
describe '.for_configuration' do
|
||||||
let!(:attributes) 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
|
||||||
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
|
describe '#set_status' do
|
||||||
let(:pipeline) { build(:ci_empty_pipeline, :created) }
|
let(:pipeline) { build(:ci_empty_pipeline, :created) }
|
||||||
|
|
||||||
|
@ -4979,4 +5011,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -104,6 +104,7 @@ RSpec.describe 'factories' do
|
||||||
factories_based_on_view = %i[
|
factories_based_on_view = %i[
|
||||||
postgres_index
|
postgres_index
|
||||||
postgres_index_bloat_estimate
|
postgres_index_bloat_estimate
|
||||||
|
postgres_autovacuum_activity
|
||||||
].to_set.freeze
|
].to_set.freeze
|
||||||
|
|
||||||
without_fd, with_fd = FactoryBot.factories
|
without_fd, with_fd = FactoryBot.factories
|
||||||
|
|
|
@ -69,6 +69,20 @@ RSpec.describe API::Invitations do
|
||||||
end
|
end
|
||||||
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
|
it 'adds a new member by email' do
|
||||||
expect do
|
expect do
|
||||||
post invitations_url(source, maintainer),
|
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).to eq(build)
|
||||||
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
||||||
expect(build.deployment.environment).to eq(build.persisted_environment)
|
expect(build.deployment.environment).to eq(build.persisted_environment)
|
||||||
|
expect(build.deployment.valid?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when creation failure occures' do
|
context 'when creation failure occures' do
|
||||||
|
|
|
@ -601,8 +601,8 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
}.from(0).to(1)
|
}.from(0).to(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'performs authorizations job immediately' do
|
it 'performs authorizations job' do
|
||||||
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
|
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
|
||||||
|
|
||||||
transfer_service.execute(new_parent_group)
|
transfer_service.execute(new_parent_group)
|
||||||
end
|
end
|
||||||
|
|
|
@ -146,12 +146,14 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when passing an existing invite user id' do
|
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
|
it 'allows already invited members to be re-invited by email and updates the member access' do
|
||||||
expect(execute_service[:status]).to eq(:error)
|
expect(execute_service[:status]).to eq(:success)
|
||||||
expect(execute_service[:message]).to eq("The member's email address has already been taken")
|
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||||
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
|
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -353,15 +353,16 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
|
||||||
|
|
||||||
context 'when member already exists' do
|
context 'when member already exists' do
|
||||||
context 'with email' do
|
context 'with email' do
|
||||||
let!(:invited_member) { create(:project_member, :invited, project: project) }
|
let!(:invited_member) { create(:project_member, :guest, :invited, project: project) }
|
||||||
let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
|
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_to_create_members(count: 1)
|
||||||
expect(result[:status]).to eq(:error)
|
expect(result[:status]).to eq(:success)
|
||||||
expect(result[:message][invited_member.invite_email])
|
|
||||||
.to eq("The member's email address has already been taken")
|
|
||||||
expect(project.users).to include project_user
|
expect(project.users).to include project_user
|
||||||
|
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,10 @@ module Database
|
||||||
module DatabaseHelpers
|
module DatabaseHelpers
|
||||||
# In order to directly work with views using factories,
|
# In order to directly work with views using factories,
|
||||||
# we can swapout the view for a table of identical structure.
|
# we can swapout the view for a table of identical structure.
|
||||||
def swapout_view_for_table(view)
|
def swapout_view_for_table(view, connection: nil)
|
||||||
ActiveRecord::Base.connection.execute(<<~SQL.squish)
|
connection ||= ActiveRecord::Base.connection
|
||||||
|
|
||||||
|
connection.execute(<<~SQL.squish)
|
||||||
CREATE TABLE #{view}_copy (LIKE #{view});
|
CREATE TABLE #{view}_copy (LIKE #{view});
|
||||||
DROP VIEW #{view};
|
DROP VIEW #{view};
|
||||||
ALTER TABLE #{view}_copy RENAME TO #{view};
|
ALTER TABLE #{view}_copy RENAME TO #{view};
|
||||||
|
|
|
@ -78,22 +78,21 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when member is already a member by email' do
|
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
|
visit members_page_path
|
||||||
|
|
||||||
invite_member('test@example.com', role: 'Developer')
|
invite_member('test@example.com', role: 'Developer')
|
||||||
|
|
||||||
invite_member('test@example.com', role: 'Reporter', refresh: false)
|
invite_member('test@example.com', role: 'Reporter', refresh: false)
|
||||||
|
|
||||||
expect(page).to have_selector(invite_modal_selector)
|
expect(page).not_to have_selector(invite_modal_selector)
|
||||||
expect(page).to have_content("The member's email address has already been taken")
|
|
||||||
|
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
click_link 'Invited'
|
click_link 'Invited'
|
||||||
|
|
||||||
page.within find_invited_member_row('test@example.com') do
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -299,37 +299,51 @@ RSpec.shared_examples_for "member creation" do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when member already exists' do
|
context 'when member already exists' do
|
||||||
before do
|
context 'when member is a user' do
|
||||||
source.add_member(user, :developer)
|
before do
|
||||||
end
|
source.add_member(user, :developer)
|
||||||
|
end
|
||||||
|
|
||||||
context 'with no current_user' do
|
context 'with no current_user' do
|
||||||
it 'updates the member' do
|
it 'updates the member' do
|
||||||
expect(source.users).to include(user)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when current_user can update member', :enable_admin_mode do
|
context 'when member is an invite by email' do
|
||||||
it 'updates the member' do
|
let_it_be(:email) { 'user@email.com' }
|
||||||
expect(source.users).to include(user)
|
let_it_be(:existing_member) { source.add_developer(email) }
|
||||||
|
|
||||||
described_class.add_member(source, user, :maintainer, current_user: admin)
|
it 'updates the member for that email' do
|
||||||
|
expect do
|
||||||
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
|
described_class.add_member(source, email, :maintainer)
|
||||||
end
|
end.to change { existing_member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER)
|
||||||
end
|
.and not_change { source.members.invite.count }
|
||||||
|
|
||||||
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
|
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
|
describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do
|
||||||
include Gitlab::Database::DynamicModelHelpers
|
include Gitlab::Database::DynamicModelHelpers
|
||||||
|
include Database::DatabaseHelpers
|
||||||
|
|
||||||
let(:migration_class) do
|
let(:migration_class) do
|
||||||
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) 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
|
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 }
|
expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,54 +29,81 @@ RSpec.describe WaitableWorker do
|
||||||
subject(:job) { worker.new }
|
subject(:job) { worker.new }
|
||||||
|
|
||||||
describe '.bulk_perform_and_wait' do
|
describe '.bulk_perform_and_wait' do
|
||||||
it 'schedules the jobs and waits for them to complete' do
|
context '1 job' do
|
||||||
worker.bulk_perform_and_wait([[1], [2]])
|
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)
|
worker.bulk_perform_and_wait(args_list)
|
||||||
end
|
|
||||||
|
|
||||||
it 'inlines workloads <= 3 jobs' do
|
expect(worker.counter).to eq(1)
|
||||||
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]])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'runs > 3 jobs using sidekiq and no waiter key' do
|
context 'between 2 and 3 jobs' do
|
||||||
arguments = 1.upto(5).map { |i| [i] }
|
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
|
end
|
||||||
|
|
||||||
it 'runs > 10 * timeout jobs using sidekiq and no waiter key' do
|
context '>= 4 jobs' do
|
||||||
arguments = 1.upto(21).map { |i| [i] }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue