Invalidate branches cache on PostReceive

Whenever `PostReceive` is enqueued, `UpdateMergeRequestsWorker`
is enqueued and `MergeRequests::RefreshService` is called, it'll
check if the source branch of each MR asssociated to the push exists
or not via `MergeRequest#source_branch_exists?`. The said method will
call `Repository#branch_exists?` which is cached in `Rails.cache`.

When the cache contains outdated data and the source branch actually
exists, the `MergeRequests#RefreshService` job will close associated
MRs which is not correct.

The fix is to expire the branches cache of the project so we have
updated data during the post receive hook which will help in the
accuracy of the check if we need to close associated MRs or not.
This commit is contained in:
Patrick Bajao 2019-08-09 18:09:06 +08:00
parent 136c3efe61
commit d96c24d815
8 changed files with 105 additions and 12 deletions

View file

@ -460,8 +460,8 @@ class Repository
end end
# Runs code after a new branch has been created. # Runs code after a new branch has been created.
def after_create_branch def after_create_branch(expire_cache: true)
expire_branches_cache expire_branches_cache if expire_cache
repository_event(:push_branch) repository_event(:push_branch)
end end
@ -474,8 +474,8 @@ class Repository
end end
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch(expire_cache: true)
expire_branches_cache expire_branches_cache if expire_cache
end end
def method_missing(msg, *args, &block) def method_missing(msg, *args, &block)

View file

@ -63,7 +63,7 @@ module Git
end end
def branch_create_hooks def branch_create_hooks
project.repository.after_create_branch project.repository.after_create_branch(expire_cache: false)
project.after_create_default_branch if default_branch? project.after_create_default_branch if default_branch?
end end
@ -78,7 +78,7 @@ module Git
end end
def branch_remove_hooks def branch_remove_hooks
project.repository.after_remove_branch project.repository.after_remove_branch(expire_cache: false)
end end
# Schedules processing of commit messages # Schedules processing of commit messages

View file

@ -42,6 +42,9 @@ class PostReceive
user = identify_user(post_received) user = identify_user(post_received)
return false unless user return false unless user
# Expire the branches cache so we have updated data for this push
post_received.project.repository.expire_branches_cache if post_received.branches_exist?
post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index|
service_klass = service_klass =
if Gitlab::Git.tag_ref?(ref) if Gitlab::Git.tag_ref?(ref)

View file

@ -0,0 +1,5 @@
---
title: Invalidate branches cache on PostReceive
merge_request: 31653
author:
type: fixed

View file

@ -27,6 +27,14 @@ module Gitlab
end end
end end
def branches_exist?
changes_refs do |_oldrev, _newrev, ref|
return true if Gitlab::Git.branch_ref?(ref) # rubocop:disable Cop/AvoidReturnFromBlocks
end
false
end
private private
def deserialize_changes(changes) def deserialize_changes(changes)

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::GitPostReceive do
let(:project) { create(:project) }
subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) }
describe '#branches_exist?' do
context 'with no branches' do
let(:changes) do
<<~EOF
654321 210987 refs/nobranches/tag1
654322 210986 refs/tags/test1
654323 210985 refs/merge-requests/mr1
EOF
end
it 'returns false' do
expect(subject.branches_exist?).to be_falsey
end
end
context 'with branches' do
let(:changes) do
<<~EOF
654322 210986 refs/heads/test1
654321 210987 refs/tags/tag1
654323 210985 refs/merge-requests/mr1
EOF
end
it 'returns true' do
expect(subject.branches_exist?).to be_truthy
end
end
context 'with malformed changes' do
let(:changes) do
<<~EOF
ref/heads/1 a
somebranch refs/heads/2
EOF
end
it 'returns false' do
expect(subject.branches_exist?).to be_falsey
end
end
end
end

View file

@ -1781,6 +1781,12 @@ describe Repository do
repository.after_create_branch repository.after_create_branch
end end
it 'does not expire the branch caches when specified' do
expect(repository).not_to receive(:expire_branches_cache)
repository.after_create_branch(expire_cache: false)
end
end end
describe '#after_remove_branch' do describe '#after_remove_branch' do
@ -1789,6 +1795,12 @@ describe Repository do
repository.after_remove_branch repository.after_remove_branch
end end
it 'does not expire the branch caches when specified' do
expect(repository).not_to receive(:expire_branches_cache)
repository.after_remove_branch(expire_cache: false)
end
end end
describe '#after_create' do describe '#after_create' do

View file

@ -57,12 +57,19 @@ describe PostReceive do
context 'with changes' do context 'with changes' do
before do before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])
end end
context "branches" do context "branches" do
let(:changes) { "123456 789012 refs/heads/tést" } let(:changes) { '123456 789012 refs/heads/tést' }
it "calls Git::BranchPushService" do it 'expires the branches cache' do
expect(project.repository).to receive(:expire_branches_cache)
described_class.new.perform(gl_repository, key_id, base64_changes)
end
it 'calls Git::BranchPushService' do
expect_next_instance_of(Git::BranchPushService) do |service| expect_next_instance_of(Git::BranchPushService) do |service|
expect(service).to receive(:execute).and_return(true) expect(service).to receive(:execute).and_return(true)
end end
@ -73,16 +80,22 @@ describe PostReceive do
end end
end end
context "tags" do context 'tags' do
let(:changes) { "123456 789012 refs/tags/tag" } let(:changes) { '123456 789012 refs/tags/tag' }
it "calls Git::TagPushService" do it 'does not expire branches cache' do
expect(Git::BranchPushService).not_to receive(:execute) expect(project.repository).not_to receive(:expire_branches_cache)
described_class.new.perform(gl_repository, key_id, base64_changes)
end
it 'calls Git::TagPushService' do
expect_next_instance_of(Git::TagPushService) do |service| expect_next_instance_of(Git::TagPushService) do |service|
expect(service).to receive(:execute).and_return(true) expect(service).to receive(:execute).and_return(true)
end end
expect(Git::BranchPushService).not_to receive(:new)
described_class.new.perform(gl_repository, key_id, base64_changes) described_class.new.perform(gl_repository, key_id, base64_changes)
end end
end end