Commit e596e619 authored by David Kim's avatar David Kim Committed by Igor Drozdov

Use gitaly pagination to improve performance

Pagination by Kaminari is slow because it requests all branches and
paginate after receiving results from Gitaly. It's a first iteration to
implement native pagination with Gitaly.
parent ad649e9c
...@@ -5,11 +5,15 @@ class BranchesFinder < GitRefsFinder ...@@ -5,11 +5,15 @@ class BranchesFinder < GitRefsFinder
super(repository, params) super(repository, params)
end end
def execute def execute(gitaly_pagination: false)
branches = repository.branches_sorted_by(sort) if gitaly_pagination && names.blank? && search.blank?
branches = by_search(branches) repository.branches_sorted_by(sort, pagination_params)
branches = by_names(branches) else
branches branches = repository.branches_sorted_by(sort)
branches = by_search(branches)
branches = by_names(branches)
branches
end
end end
private private
...@@ -18,6 +22,18 @@ class BranchesFinder < GitRefsFinder ...@@ -18,6 +22,18 @@ class BranchesFinder < GitRefsFinder
@params[:names].presence @params[:names].presence
end end
def per_page
@params[:per_page].presence
end
def page_token
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{@params[:page_token]}" if @params[:page_token]
end
def pagination_params
{ limit: per_page, page_token: page_token }
end
def by_names(branches) def by_names(branches)
return branches unless names return branches unless names
......
...@@ -713,8 +713,8 @@ class Repository ...@@ -713,8 +713,8 @@ class Repository
"#{name}-#{highest_branch_id + 1}" "#{name}-#{highest_branch_id + 1}"
end end
def branches_sorted_by(value) def branches_sorted_by(sort_by, pagination_params = nil)
raw_repository.local_branches(sort_by: value) raw_repository.local_branches(sort_by: sort_by, pagination_params: pagination_params)
end end
def tags_sorted_by(value) def tags_sorted_by(value)
......
---
title: Use native Gitaly pagination for Branch list API
merge_request: 35819
author:
type: changed
...@@ -32,14 +32,21 @@ module API ...@@ -32,14 +32,21 @@ module API
params do params do
use :pagination use :pagination
use :filter_params use :filter_params
optional :page_token, type: String, desc: 'Name of branch to start the paginaition from'
end end
get ':id/repository/branches' do get ':id/repository/branches' do
user_project.preload_protected_branches user_project.preload_protected_branches
repository = user_project.repository repository = user_project.repository
branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute if Feature.enabled?(:branch_list_keyset_pagination, user_project)
branches = paginate(::Kaminari.paginate_array(branches)) branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute(gitaly_pagination: true)
else
branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute
branches = paginate(::Kaminari.paginate_array(branches))
end
merged_branch_names = repository.merged_branch_names(branches.map(&:name)) merged_branch_names = repository.merged_branch_names(branches.map(&:name))
present( present(
......
...@@ -127,9 +127,9 @@ module Gitlab ...@@ -127,9 +127,9 @@ module Gitlab
end end
end end
def local_branches(sort_by: nil) def local_branches(sort_by: nil, pagination_params: nil)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_ref_client.local_branches(sort_by: sort_by) gitaly_ref_client.local_branches(sort_by: sort_by, pagination_params: pagination_params)
end end
end end
......
...@@ -110,8 +110,8 @@ module Gitlab ...@@ -110,8 +110,8 @@ module Gitlab
branch_names.count branch_names.count
end end
def local_branches(sort_by: nil) def local_branches(sort_by: nil, pagination_params: nil)
request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo, pagination_params: pagination_params)
request.sort_by = sort_by_param(sort_by) if sort_by request.sort_by = sort_by_param(sort_by) if sort_by
response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request, timeout: GitalyClient.fast_timeout) response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request, timeout: GitalyClient.fast_timeout)
consume_find_local_branches_response(response) consume_find_local_branches_response(response)
......
This diff is collapsed.
...@@ -17,6 +17,7 @@ RSpec.describe API::Branches do ...@@ -17,6 +17,7 @@ RSpec.describe API::Branches do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
project.repository.add_branch(user, 'ends-with.txt', branch_sha) project.repository.add_branch(user, 'ends-with.txt', branch_sha)
stub_feature_flags(branch_list_keyset_pagination: false)
end end
describe "GET /projects/:id/repository/branches" do describe "GET /projects/:id/repository/branches" do
...@@ -29,16 +30,6 @@ RSpec.describe API::Branches do ...@@ -29,16 +30,6 @@ RSpec.describe API::Branches do
end end
end end
it 'returns the repository branches' do
get api(route, current_user), params: { per_page: 100 }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/branches')
expect(response).to include_pagination_headers
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
end
def check_merge_status(json_response) def check_merge_status(json_response)
merged, unmerged = json_response.partition { |branch| branch['merged'] } merged, unmerged = json_response.partition { |branch| branch['merged'] }
merged_branches = merged.map { |branch| branch['name'] } merged_branches = merged.map { |branch| branch['name'] }
...@@ -47,22 +38,107 @@ RSpec.describe API::Branches do ...@@ -47,22 +38,107 @@ RSpec.describe API::Branches do
expect(project.repository.merged_branch_names(unmerged_branches)).to be_empty expect(project.repository.merged_branch_names(unmerged_branches)).to be_empty
end end
it 'determines only a limited number of merged branch names' do context 'with branch_list_keyset_pagination feature off' do
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original context 'with legacy pagination params' do
it 'returns the repository branches' do
get api(route, current_user), params: { per_page: 100 }
get api(route, current_user), params: { per_page: 2 } expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/branches')
expect(response).to include_pagination_headers
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
end
expect(response).to have_gitlab_http_status(:ok) it 'determines only a limited number of merged branch names' do
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
get api(route, current_user), params: { per_page: 2 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq 2
check_merge_status(json_response)
end
check_merge_status(json_response) it 'merge status matches reality on paginated input' do
expected_first_branch_name = project.repository.branches_sorted_by('name')[20].name
get api(route, current_user), params: { per_page: 20, page: 2 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq 20
expect(json_response.first['name']).to eq(expected_first_branch_name)
check_merge_status(json_response)
end
end
context 'with gitaly pagination params ' do
it 'merge status matches reality on paginated input' do
expected_first_branch_name = project.repository.branches_sorted_by('name').first.name
get api(route, current_user), params: { per_page: 20, page_token: 'feature' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq 20
expect(json_response.first['name']).to eq(expected_first_branch_name)
check_merge_status(json_response)
end
end
end end
it 'merge status matches reality on paginated input' do context 'with branch_list_keyset_pagination feature on' do
get api(route, current_user), params: { per_page: 20, page: 2 } before do
stub_feature_flags(branch_list_keyset_pagination: true)
end
expect(response).to have_gitlab_http_status(:ok) context 'with gitaly pagination params ' do
it 'returns the repository branches' do
get api(route, current_user), params: { per_page: 100 }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/branches')
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
end
it 'determines only a limited number of merged branch names' do
expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
get api(route, current_user), params: { per_page: 2 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq 2
check_merge_status(json_response)
end
check_merge_status(json_response) it 'merge status matches reality on paginated input' do
expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name
get api(route, current_user), params: { per_page: 20, page_token: 'feature' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq 20
expect(json_response.first['name']).to eq(expected_first_branch_name)
check_merge_status(json_response)
end
end
context 'with legacy pagination params' do
it 'ignores legacy pagination params' do
expected_first_branch_name = project.repository.branches_sorted_by('name').first.name
get api(route, current_user), params: { per_page: 20, page: 2 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first['name']).to eq(expected_first_branch_name)
check_merge_status(json_response)
end
end
end end
context 'when repository is disabled' do context 'when repository is disabled' do
......
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