Commit 0338e978 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'docs-bprescott-pgbouncer-20200319' into 'master'

PgBouncer - going direct to master

See merge request gitlab-org/gitlab!27619
parents bf0f0568 181e2e7d
...@@ -64,10 +64,18 @@ export const generateLinksData = ({ links }, jobs, containerID) => { ...@@ -64,10 +64,18 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
// Start point // Start point
path.moveTo(sourceNodeX, sourceNodeY); path.moveTo(sourceNodeX, sourceNodeY);
// Make cross-stages lines a straight line all the way
// until we can safely draw the bezier to look nice.
const straightLineDestinationX = targetNodeX - 100;
const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
if (straightLineDestinationX > 0) {
path.lineTo(straightLineDestinationX, sourceNodeY);
}
// Add bezier curve. The first 4 coordinates are the 2 control // Add bezier curve. The first 4 coordinates are the 2 control
// points to create the curve, and the last one is the end point (x, y). // points to create the curve, and the last one is the end point (x, y).
// We want our control points to be in the middle of the line // We want our control points to be in the middle of the line
const controlPointX = sourceNodeX + (targetNodeX - sourceNodeX) / 2;
path.bezierCurveTo( path.bezierCurveTo(
controlPointX, controlPointX,
sourceNodeY, sourceNodeY,
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { n__ } from '~/locale'; import { n__, __ } from '~/locale';
export default { export default {
name: 'AssigneeTitle', name: 'AssigneeTitle',
...@@ -26,12 +26,19 @@ export default { ...@@ -26,12 +26,19 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
changing: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
assigneeTitle() { assigneeTitle() {
const assignees = this.numberOfAssignees; const assignees = this.numberOfAssignees;
return n__('Assignee', `%d Assignees`, assignees); return n__('Assignee', `%d Assignees`, assignees);
}, },
titleCopy() {
return this.changing ? __('Apply') : __('Edit');
},
}, },
}; };
</script> </script>
...@@ -43,11 +50,12 @@ export default { ...@@ -43,11 +50,12 @@ export default {
v-if="editable" v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right" class="js-sidebar-dropdown-toggle edit-link float-right"
href="#" href="#"
data-test-id="edit-link"
data-track-event="click_edit_button" data-track-event="click_edit_button"
data-track-label="right_sidebar" data-track-label="right_sidebar"
data-track-property="assignee" data-track-property="assignee"
> >
{{ __('Edit') }} {{ titleCopy }}
</a> </a>
<a <a
v-if="showToggle" v-if="showToggle"
......
...@@ -89,6 +89,8 @@ export default { ...@@ -89,6 +89,8 @@ export default {
.saveAssignees(this.field) .saveAssignees(this.field)
.then(() => { .then(() => {
this.loading = false; this.loading = false;
this.store.resetChanging();
refreshUserMergeRequestCounts(); refreshUserMergeRequestCounts();
}) })
.catch(() => { .catch(() => {
...@@ -113,6 +115,7 @@ export default { ...@@ -113,6 +115,7 @@ export default {
:loading="loading || store.isFetching.assignees" :loading="loading || store.isFetching.assignees"
:editable="store.editable" :editable="store.editable"
:show-toggle="!signedIn" :show-toggle="!signedIn"
:changing="store.changing"
/> />
<assignees <assignees
v-if="!store.isFetching.assignees" v-if="!store.isFetching.assignees"
......
...@@ -33,6 +33,7 @@ export default class SidebarStore { ...@@ -33,6 +33,7 @@ export default class SidebarStore {
this.projectEmailsDisabled = false; this.projectEmailsDisabled = false;
this.subscribeDisabledDescription = ''; this.subscribeDisabledDescription = '';
this.subscribed = null; this.subscribed = null;
this.changing = false;
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
...@@ -51,6 +52,10 @@ export default class SidebarStore { ...@@ -51,6 +52,10 @@ export default class SidebarStore {
} }
} }
resetChanging() {
this.changing = false;
}
setTimeTrackingData(data) { setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate; this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent; this.totalTimeSpent = data.total_time_spent;
...@@ -80,6 +85,7 @@ export default class SidebarStore { ...@@ -80,6 +85,7 @@ export default class SidebarStore {
addAssignee(assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.changing = true;
this.assignees.push(assignee); this.assignees.push(assignee);
} }
} }
...@@ -100,6 +106,7 @@ export default class SidebarStore { ...@@ -100,6 +106,7 @@ export default class SidebarStore {
removeAssignee(assignee) { removeAssignee(assignee) {
if (assignee) { if (assignee) {
this.changing = true;
this.assignees = this.assignees.filter(({ id }) => id !== assignee.id); this.assignees = this.assignees.filter(({ id }) => id !== assignee.id);
} }
} }
...@@ -111,6 +118,7 @@ export default class SidebarStore { ...@@ -111,6 +118,7 @@ export default class SidebarStore {
} }
removeAllAssignees() { removeAllAssignees() {
this.changing = true;
this.assignees = []; this.assignees = [];
} }
......
...@@ -61,4 +61,8 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -61,4 +61,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.underscore def self.underscore
Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore } Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
end end
def self.where_exists(query)
where('EXISTS (?)', query.select(1))
end
end end
...@@ -136,6 +136,10 @@ module Ci ...@@ -136,6 +136,10 @@ module Ci
# We are using optimistic locking combined with Redis locking to ensure # We are using optimistic locking combined with Redis locking to ensure
# that a chunk gets migrated properly. # that a chunk gets migrated properly.
# #
# We are catching an exception related to an exclusive lock not being
# acquired because it is creating a lot of noise, and is a result of
# duplicated workers running in parallel for the same build trace chunk.
#
def persist_data! def persist_data!
in_lock(*lock_params) do # exclusive Redis lock is acquired first in_lock(*lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
...@@ -144,6 +148,8 @@ module Ci ...@@ -144,6 +148,8 @@ module Ci
chunk.unsafe_persist_data! # we migrate the data and update data store chunk.unsafe_persist_data! # we migrate the data and update data store
end end
end end
rescue FailedToObtainLockError
metrics.increment_trace_operation(operation: :stalled)
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
raise FailedToPersistDataError, <<~MSG raise FailedToPersistDataError, <<~MSG
Data migration race condition detected Data migration race condition detected
......
---
title: Add apply button when user changes assignees
merge_request: 44812
author:
type: added
...@@ -1445,7 +1445,7 @@ Considering these, you should carefully plan your PostgreSQL upgrade: ...@@ -1445,7 +1445,7 @@ Considering these, you should carefully plan your PostgreSQL upgrade:
sudo gitlab-ctl pg-upgrade -V 12 sudo gitlab-ctl pg-upgrade -V 12
``` ```
CAUTION: **Warning:** NOTE: **Note:**
Reverting PostgreSQL upgrade with `gitlab-ctl revert-pg-upgrade` has the same considerations as Reverting PostgreSQL upgrade with `gitlab-ctl revert-pg-upgrade` has the same considerations as
`gitlab-ctl pg-upgrade`. It can be complicated and may involve deletion of the data directory. `gitlab-ctl pg-upgrade`. You should follow the same procedure by first stopping the replicas,
If you need to do that, please contact GitLab support. then reverting the leader, and finally reverting the replicas.
...@@ -61,7 +61,7 @@ RSpec.describe 'epics swimlanes filtering', :js do ...@@ -61,7 +61,7 @@ RSpec.describe 'epics swimlanes filtering', :js do
wait_for_empty_boards((3..4)) wait_for_empty_boards((3..4))
end end
it 'filters by assignee' do it 'filters by assignee', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/266990' do
wait_for_all_requests wait_for_all_requests
set_filter("assignee", user.username) set_filter("assignee", user.username)
......
...@@ -6,9 +6,20 @@ module Gitlab ...@@ -6,9 +6,20 @@ module Gitlab
class Metrics class Metrics
extend Gitlab::Utils::StrongMemoize extend Gitlab::Utils::StrongMemoize
OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite, OPERATIONS = [
:accepted, :finalized, :discarded, :conflict, :locked, :appended, # new trace data has been written to a chunk
:invalid].freeze :streamed, # new trace data has been sent by a runner
:chunked, # new trace chunk has been created
:mutated, # trace has been mutated when removing secrets
:overwrite, # runner requested overwritting a build trace
:accepted, # scheduled chunks for migration and responded with 202
:finalized, # all live build trace chunks have been persisted
:discarded, # failed to persist live chunks before timeout
:conflict, # runner has sent unrecognized build state details
:locked, # build trace has been locked by a different mechanism
:stalled, # failed to migrate chunk due to a worker duplication
:invalid # malformed build trace has been detected using CRC32
].freeze
def increment_trace_operation(operation: :unknown) def increment_trace_operation(operation: :unknown)
unless OPERATIONS.include?(operation) unless OPERATIONS.include?(operation)
......
...@@ -140,6 +140,18 @@ RSpec.describe 'Issue Sidebar' do ...@@ -140,6 +140,18 @@ RSpec.describe 'Issue Sidebar' do
end end
end end
end end
it 'shows label text as "Apply" when assignees are changed' do
project.add_developer(user)
visit_issue(project, issue2)
find('.block.assignee .edit-link').click
wait_for_requests
click_on 'Unassigned'
expect(page).to have_link('Apply')
end
end end
context 'as a allowed user' do context 'as a allowed user' do
......
...@@ -11,6 +11,7 @@ describe('AssigneeTitle component', () => { ...@@ -11,6 +11,7 @@ describe('AssigneeTitle component', () => {
propsData: { propsData: {
numberOfAssignees: 0, numberOfAssignees: 0,
editable: false, editable: false,
changing: false,
...props, ...props,
}, },
}); });
...@@ -62,6 +63,22 @@ describe('AssigneeTitle component', () => { ...@@ -62,6 +63,22 @@ describe('AssigneeTitle component', () => {
}); });
}); });
describe('when changing is false', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Edit');
});
});
describe('when changing is true', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true, changing: true });
expect(wrapper.find('[data-test-id="edit-link"]').text()).toEqual('Apply');
});
});
it('does not render spinner by default', () => { it('does not render spinner by default', () => {
wrapper = createComponent({ wrapper = createComponent({
numberOfAssignees: 0, numberOfAssignees: 0,
......
...@@ -20,6 +20,7 @@ describe('sidebar assignees', () => { ...@@ -20,6 +20,7 @@ describe('sidebar assignees', () => {
mediator, mediator,
field: '', field: '',
projectPath: 'projectPath', projectPath: 'projectPath',
changing: false,
...props, ...props,
}, },
provide: { provide: {
......
...@@ -57,16 +57,40 @@ describe('Sidebar store', () => { ...@@ -57,16 +57,40 @@ describe('Sidebar store', () => {
expect(testContext.store.isFetching.assignees).toBe(true); expect(testContext.store.isFetching.assignees).toBe(true);
}); });
it('adds a new assignee', () => { it('resets changing when resetChanging is called', () => {
testContext.store.changing = true;
testContext.store.resetChanging();
expect(testContext.store.changing).toBe(false);
});
describe('when it adds a new assignee', () => {
beforeEach(() => {
testContext.store.addAssignee(ASSIGNEE); testContext.store.addAssignee(ASSIGNEE);
});
expect(testContext.store.assignees.length).toEqual(1); it('adds a new assignee', () => {
expect(testContext.store.assignees).toHaveLength(1);
}); });
it('removes an assignee', () => { it('sets changing to true', () => {
expect(testContext.store.changing).toBe(true);
});
});
describe('when it removes an assignee', () => {
beforeEach(() => {
testContext.store.removeAssignee(ASSIGNEE); testContext.store.removeAssignee(ASSIGNEE);
});
expect(testContext.store.assignees.length).toEqual(0); it('removes an assignee', () => {
expect(testContext.store.assignees).toHaveLength(0);
});
it('sets changing to true', () => {
expect(testContext.store.changing).toBe(true);
});
}); });
it('finds an existent assignee', () => { it('finds an existent assignee', () => {
...@@ -86,6 +110,7 @@ describe('Sidebar store', () => { ...@@ -86,6 +110,7 @@ describe('Sidebar store', () => {
testContext.store.removeAllAssignees(); testContext.store.removeAllAssignees();
expect(testContext.store.assignees.length).toEqual(0); expect(testContext.store.assignees.length).toEqual(0);
expect(testContext.store.changing).toBe(true);
}); });
it('sets participants data', () => { it('sets participants data', () => {
......
...@@ -90,4 +90,12 @@ RSpec.describe ApplicationRecord do ...@@ -90,4 +90,12 @@ RSpec.describe ApplicationRecord do
expect(User.at_most(2).count).to eq(2) expect(User.at_most(2).count).to eq(2)
end end
end end
describe '.where_exists' do
it 'produces a WHERE EXISTS query' do
user = create(:user)
expect(User.where_exists(User.limit(1))).to eq([user])
end
end
end end
...@@ -508,20 +508,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do ...@@ -508,20 +508,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject { build_trace_chunk.persist_data! } subject { build_trace_chunk.persist_data! }
shared_examples_for 'Atomic operation' do
context 'when the other process is persisting' do
let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
before do
stub_exclusive_lease_taken(lease_key)
end
it 'raise an error' do
expect { subject }.to raise_error('Failed to obtain a lock')
end
end
end
context 'when data_store is redis' do context 'when data_store is redis' do
let(:data_store) { :redis } let(:data_store) { :redis }
...@@ -552,8 +538,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do ...@@ -552,8 +538,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(build_trace_chunk.reload.checksum).to eq '3398914352' expect(build_trace_chunk.reload.checksum).to eq '3398914352'
end end
it_behaves_like 'Atomic operation'
end end
context 'when data size has not reached CHUNK_SIZE' do context 'when data size has not reached CHUNK_SIZE' do
...@@ -606,6 +590,35 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do ...@@ -606,6 +590,35 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
/Modifed build trace chunk detected/) /Modifed build trace chunk detected/)
end end
end end
context 'when the chunk is being locked by a different worker' do
let(:metrics) { spy('metrics') }
it 'does not raise an exception' do
lock_chunk do
expect { build_trace_chunk.persist_data! }.not_to raise_error
end
end
it 'increments stalled chunk trace metric' do
allow(build_trace_chunk)
.to receive(:metrics)
.and_return(metrics)
lock_chunk { build_trace_chunk.persist_data! }
expect(metrics)
.to have_received(:increment_trace_operation)
.with(operation: :stalled)
.once
end
def lock_chunk(&block)
"trace_write:#{build.id}:chunks:#{chunk_index}".then do |key|
build_trace_chunk.in_lock(key, &block)
end
end
end
end end
end end
...@@ -640,8 +653,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do ...@@ -640,8 +653,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end end
it_behaves_like 'Atomic operation'
end end
context 'when data size has not reached CHUNK_SIZE' do context 'when data size has not reached CHUNK_SIZE' do
...@@ -701,8 +712,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do ...@@ -701,8 +712,6 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end end
it_behaves_like 'Atomic operation'
end end
context 'when data size has not reached CHUNK_SIZE' do context 'when data size has not reached CHUNK_SIZE' do
......
...@@ -38,7 +38,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button ...@@ -38,7 +38,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button
page.within '.issuable-sidebar' do page.within '.issuable-sidebar' do
page.within '.assignee' do page.within '.assignee' do
# Closing dropdown to persist # Closing dropdown to persist
click_link 'Edit' click_link 'Apply'
expect(page).to have_content user2.name expect(page).to have_content user2.name
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment