Commit de9c8056 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Exclude the total headers from the commits API

Excluding these headers allows us to avoid the extra Gitaly call
needed to count the commits for the specific call that is being made
to the API.
parent 5697b480
---
name: api_commits_without_count
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43159
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254994
type: development
group: team::Scalability
default_enabled: false
...@@ -62,19 +62,29 @@ module API ...@@ -62,19 +62,29 @@ module API
first_parent: first_parent, first_parent: first_parent,
order: order) order: order)
commit_count = serializer = with_stats ? Entities::CommitWithStats : Entities::Commit
if all || path || before || after || first_parent
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent)
else
# Cacheable commit count.
user_project.repository.commit_count_for_ref(ref)
end
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) if Feature.enabled?(:api_commits_without_count, user_project)
# This tells kaminari that there is 1 more commit after the one we've
# loaded, meaning there will be a next page, if the currently loaded set
# of commits is equal to the requested page size.
commit_count = offset + commits.size + 1
paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
serializer = with_stats ? Entities::CommitWithStats : Entities::Commit present paginate(paginated_commits, exclude_total_headers: true), with: serializer
else
commit_count =
if all || path || before || after || first_parent
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent)
else
# Cacheable commit count.
user_project.repository.commit_count_for_ref(ref)
end
present paginate(paginated_commits), with: serializer paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
present paginate(paginated_commits), with: serializer
end
end end
desc 'Commit multiple file changes as one commit' do desc 'Commit multiple file changes as one commit' do
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
module API module API
module Helpers module Helpers
module Pagination module Pagination
def paginate(relation) def paginate(*args)
Gitlab::Pagination::OffsetPagination.new(self).paginate(relation) Gitlab::Pagination::OffsetPagination.new(self).paginate(*args)
end end
end end
end end
......
...@@ -10,9 +10,9 @@ module Gitlab ...@@ -10,9 +10,9 @@ module Gitlab
@request_context = request_context @request_context = request_context
end end
def paginate(relation) def paginate(relation, exclude_total_headers: false)
paginate_with_limit_optimization(add_default_order(relation)).tap do |data| paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
add_pagination_headers(data) add_pagination_headers(data, exclude_total_headers)
end end
end end
...@@ -47,14 +47,14 @@ module Gitlab ...@@ -47,14 +47,14 @@ module Gitlab
relation relation
end end
def add_pagination_headers(paginated_data) def add_pagination_headers(paginated_data, exclude_total_headers)
header 'X-Per-Page', paginated_data.limit_value.to_s header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data) header 'Link', pagination_links(paginated_data)
return if data_without_counts?(paginated_data) return if exclude_total_headers || data_without_counts?(paginated_data)
header 'X-Total', paginated_data.total_count.to_s header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', total_pages(paginated_data).to_s header 'X-Total-Pages', total_pages(paginated_data).to_s
......
...@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do ...@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
let(:request_context) { double("request_context") } let(:request_context) { double("request_context") }
subject do subject(:paginator) do
described_class.new(request_context) described_class.new(request_context)
end end
...@@ -119,6 +119,34 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do ...@@ -119,6 +119,34 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
subject.paginate(resource) subject.paginate(resource)
end end
end end
it 'does not return the total headers when excluding them' do
expect_no_header('X-Total')
expect_no_header('X-Total-Pages')
expect_header('X-Per-Page', '2')
expect_header('X-Page', '1')
paginator.paginate(resource, exclude_total_headers: true)
end
end
context 'when resource is a paginatable array' do
let(:resource) { Kaminari.paginate_array(Project.all.to_a) }
it_behaves_like 'response with pagination headers'
it 'only returns the requested resources' do
expect(paginator.paginate(resource).count).to eq(2)
end
it 'does not return total headers when excluding them' do
expect_no_header('X-Total')
expect_no_header('X-Total-Pages')
expect_header('X-Per-Page', '2')
expect_header('X-Page', '1')
paginator.paginate(resource, exclude_total_headers: true)
end
end end
end end
......
...@@ -36,6 +36,13 @@ RSpec.describe API::Commits do ...@@ -36,6 +36,13 @@ RSpec.describe API::Commits do
end end
it 'include correct pagination headers' do it 'include correct pagination headers' do
get api(route, current_user)
expect(response).to include_limited_pagination_headers
end
it 'includes the total headers when the count is not disabled' do
stub_feature_flags(api_commits_without_count: false)
commit_count = project.repository.count_commits(ref: 'master').to_s commit_count = project.repository.count_commits(ref: 'master').to_s
get api(route, current_user) get api(route, current_user)
...@@ -79,12 +86,10 @@ RSpec.describe API::Commits do ...@@ -79,12 +86,10 @@ RSpec.describe API::Commits do
it 'include correct pagination headers' do it 'include correct pagination headers' do
commits = project.repository.commits("master", limit: 2) commits = project.repository.commits("master", limit: 2)
after = commits.second.created_at after = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
expect(response.headers['X-Page']).to eql('1') expect(response.headers['X-Page']).to eql('1')
end end
end end
...@@ -109,12 +114,10 @@ RSpec.describe API::Commits do ...@@ -109,12 +114,10 @@ RSpec.describe API::Commits do
it 'include correct pagination headers' do it 'include correct pagination headers' do
commits = project.repository.commits("master", limit: 2) commits = project.repository.commits("master", limit: 2)
before = commits.second.created_at before = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
expect(response.headers['X-Page']).to eql('1') expect(response.headers['X-Page']).to eql('1')
end end
end end
...@@ -137,49 +140,49 @@ RSpec.describe API::Commits do ...@@ -137,49 +140,49 @@ RSpec.describe API::Commits do
context "path optional parameter" do context "path optional parameter" do
it "returns project commits matching provided path parameter" do it "returns project commits matching provided path parameter" do
path = 'files/ruby/popen.rb' path = 'files/ruby/popen.rb'
commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
get api("/projects/#{project_id}/repository/commits?path=#{path}", user) get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
expect(json_response.size).to eq(3) expect(json_response.size).to eq(3)
expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
end end
it 'include correct pagination headers' do it 'include correct pagination headers' do
path = 'files/ruby/popen.rb' path = 'files/ruby/popen.rb'
commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
get api("/projects/#{project_id}/repository/commits?path=#{path}", user) get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
expect(response.headers['X-Page']).to eql('1') expect(response.headers['X-Page']).to eql('1')
end end
end end
context 'all optional parameter' do context 'all optional parameter' do
it 'returns all project commits' do it 'returns all project commits' do
commit_count = project.repository.count_commits(all: true) expected_commit_ids = project.repository.commits(nil, all: true, limit: 50).map(&:id)
get api("/projects/#{project_id}/repository/commits?all=true&per_page=50", user)
get api("/projects/#{project_id}/repository/commits?all=true", user) commit_ids = json_response.map { |c| c['id'] }
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count.to_s) expect(commit_ids).to eq(expected_commit_ids)
expect(response.headers['X-Page']).to eql('1') expect(response.headers['X-Page']).to eql('1')
end end
end end
context 'first_parent optional parameter' do context 'first_parent optional parameter' do
it 'returns all first_parent commits' do it 'returns all first_parent commits' do
commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true) expected_commit_ids = project.repository.commits(SeedRepo::Commit::ID, limit: 50, first_parent: true).map(&:id)
get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } get api("/projects/#{project_id}/repository/commits?per_page=50", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
expect(response).to include_pagination_headers commit_ids = json_response.map { |c| c['id'] }
expect(commit_count).to eq(12)
expect(response.headers['X-Total']).to eq(commit_count.to_s) expect(response).to include_limited_pagination_headers
expect(expected_commit_ids.size).to eq(12)
expect(commit_ids).to eq(expected_commit_ids)
end end
end end
...@@ -209,11 +212,7 @@ RSpec.describe API::Commits do ...@@ -209,11 +212,7 @@ RSpec.describe API::Commits do
end end
it 'returns correct headers' do it 'returns correct headers' do
commit_count = project.repository.count_commits(ref: ref_name).to_s expect(response).to include_limited_pagination_headers
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
expect(response.headers['X-Page']).to eq('1')
expect(response.headers['Link']).to match(/page=1&per_page=5/) expect(response.headers['Link']).to match(/page=1&per_page=5/)
expect(response.headers['Link']).to match(/page=2&per_page=5/) expect(response.headers['Link']).to match(/page=2&per_page=5/)
end end
...@@ -972,7 +971,7 @@ RSpec.describe API::Commits do ...@@ -972,7 +971,7 @@ RSpec.describe API::Commits do
refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}) refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]})
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end end
...@@ -1262,7 +1261,7 @@ RSpec.describe API::Commits do ...@@ -1262,7 +1261,7 @@ RSpec.describe API::Commits do
get api(route, current_user) get api(route, current_user)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(json_response.size).to be >= 1 expect(json_response.size).to be >= 1
expect(json_response.first.keys).to include 'diff' expect(json_response.first.keys).to include 'diff'
end end
...@@ -1276,7 +1275,7 @@ RSpec.describe API::Commits do ...@@ -1276,7 +1275,7 @@ RSpec.describe API::Commits do
get api(route, current_user) get api(route, current_user)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(json_response.size).to be <= 1 expect(json_response.size).to be <= 1
end end
end end
...@@ -1914,7 +1913,7 @@ RSpec.describe API::Commits do ...@@ -1914,7 +1913,7 @@ RSpec.describe API::Commits do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user) get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_limited_pagination_headers
expect(json_response.length).to eq(1) expect(json_response.length).to eq(1)
expect(json_response[0]['id']).to eq(merged_mr.id) expect(json_response[0]['id']).to eq(merged_mr.id)
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