From 5816ad4d588a2b711f82c41c5d19d2dba739ae7e Mon Sep 17 00:00:00 2001
From: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Date: Fri, 21 Oct 2022 22:43:41 +0700
Subject: [PATCH] Add server-side pagination to SearchFilesByName

In prior versions, Wiki operations use SearchFilesByName RPC to
access files in Wiki repositories. Gitaly did not support limit
and offset. Rails has to fetch all available pages and perform
pagination from its side with Karinami. This commit adds those
fields to the corresponding request after Gitaly adds such
functionality.

Changelog: performance
Signed-off-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
---
 Gemfile                                       |  2 +-
 Gemfile.checksum                              |  2 +-
 Gemfile.lock                                  |  4 +-
 app/models/wiki.rb                            | 16 +++---
 lib/gitlab/git/repository.rb                  |  8 +--
 .../gitaly_client/repository_service.rb       |  8 +--
 .../models/wiki_shared_examples.rb            | 49 ++++++++++++++++---
 7 files changed, 62 insertions(+), 27 deletions(-)

diff --git a/Gemfile b/Gemfile
index 4853249265349..b75090bfef592 100644
--- a/Gemfile
+++ b/Gemfile
@@ -503,7 +503,7 @@ gem 'ssh_data', '~> 1.3'
 gem 'spamcheck', '~> 1.0.0'
 
 # Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 15.4.0-rc2'
+gem 'gitaly', '~> 15.5.0'
 
 # KAS GRPC protocol definitions
 gem 'kas-grpc', '~> 0.0.2'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 50463fe271522..620934a40043a 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -198,7 +198,7 @@
 {"name":"gettext_i18n_rails","version":"1.8.0","platform":"ruby","checksum":"95e5cf8440b1e08705b27f2bccb56143272c5a7a0dabcf54ea1bd701140a496f"},
 {"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"},
 {"name":"git","version":"1.11.0","platform":"ruby","checksum":"7e95ba4da8298a0373ef1a6862aa22007d761f3c8274b675aa787966fecea0f1"},
-{"name":"gitaly","version":"15.4.0.pre.rc2","platform":"ruby","checksum":"48764528a730605a46f00cf86c7cfcb92d25f4f3d8cb9e09557baac3e9f3f8e3"},
+{"name":"gitaly","version":"15.5.0","platform":"ruby","checksum":"d85dd4890a1f0fd95f935c848bcedf03f19b78872f20f04b9811e602bea4ef42"},
 {"name":"github-markup","version":"1.7.0","platform":"ruby","checksum":"97eb27c70662d9cc1d5997cd6c99832026fae5d4913b5dce1ce6c9f65078e69d"},
 {"name":"gitlab","version":"4.16.1","platform":"ruby","checksum":"13fd7059cbdad5a1a21b15fa2cf9070b97d92e27f8c688581fe3d84dc038074f"},
 {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
diff --git a/Gemfile.lock b/Gemfile.lock
index dc1a39776bc2a..36c5ad429c19c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -547,7 +547,7 @@ GEM
       rails (>= 3.2.0)
     git (1.11.0)
       rchardet (~> 1.8)
-    gitaly (15.4.0.pre.rc2)
+    gitaly (15.5.0)
       grpc (~> 1.0)
     github-markup (1.7.0)
     gitlab (4.16.1)
@@ -1624,7 +1624,7 @@ DEPENDENCIES
   gettext (~> 3.3)
   gettext_i18n_rails (~> 1.8.0)
   gettext_i18n_rails_js (~> 1.3)
-  gitaly (~> 15.4.0.pre.rc2)
+  gitaly (~> 15.5.0)
   github-markup (~> 1.7.0)
   gitlab-chronic (~> 0.10.5)
   gitlab-dangerfiles (~> 3.6.1)
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index b718c3a096fa9..4fe8170a4ab03 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -190,7 +190,7 @@ def has_home_page?
   end
 
   def empty?
-    !repository_exists? || list_page_paths.empty?
+    !repository_exists? || list_page_paths(limit: 1).empty?
   end
 
   def exists?
@@ -207,9 +207,9 @@ def exists?
   #
   # Returns an Array of GitLab WikiPage instances or an
   # empty Array if this Wiki has no pages.
-  def list_pages(limit: 0, direction: DIRECTION_ASC, load_content: false)
+  def list_pages(direction: DIRECTION_ASC, load_content: false, limit: 0, offset: 0)
     create_wiki_repository unless repository_exists?
-    list_pages_with_repository_rpcs(limit: limit, direction: direction, load_content: load_content)
+    list_pages_with_repository_rpcs(direction: direction, load_content: load_content, limit: limit, offset: offset)
   end
 
   def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
@@ -457,7 +457,7 @@ def find_matched_file(title, version)
     escaped_path = RE2::Regexp.escape(sluggified_title(title))
     path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{file_extension_regexp})$")
 
-    matched_files = repository.search_files_by_regexp(path_regexp, version)
+    matched_files = repository.search_files_by_regexp(path_regexp, version, limit: 1)
     return if matched_files.blank?
 
     Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first)
@@ -509,15 +509,15 @@ def strip_extension(path)
     path.sub(/\.[^.]+\z/, "")
   end
 
-  def list_page_paths
+  def list_page_paths(limit: 0, offset: 0)
     return [] if repository.empty?
 
     path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)\\.(#{file_extension_regexp})$")
-    repository.search_files_by_regexp(path_regexp, default_branch)
+    repository.search_files_by_regexp(path_regexp, default_branch, limit: limit, offset: offset)
   end
 
-  def list_pages_with_repository_rpcs(limit:, direction:, load_content:)
-    paths = list_page_paths
+  def list_pages_with_repository_rpcs(direction:, load_content:, limit:, offset:)
+    paths = list_page_paths(limit: limit, offset: offset)
     return [] if paths.empty?
 
     pages = paths.map do |path|
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9bbe17dcad1f4..a5eab9aae8cc3 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1054,19 +1054,19 @@ def can_be_merged?(source_sha, target_branch)
         end
       end
 
-      def search_files_by_name(query, ref)
+      def search_files_by_name(query, ref, limit: 0, offset: 0)
         safe_query = query.sub(%r{^/*}, "")
         ref ||= root_ref
 
         return [] if empty? || safe_query.blank?
 
-        gitaly_repository_client.search_files_by_name(ref, safe_query).map do |file|
+        gitaly_repository_client.search_files_by_name(ref, safe_query, limit: limit, offset: offset).map do |file|
           Gitlab::EncodingHelper.encode_utf8(file)
         end
       end
 
-      def search_files_by_regexp(filter, ref = 'HEAD')
-        gitaly_repository_client.search_files_by_regexp(ref, filter).map do |file|
+      def search_files_by_regexp(filter, ref = 'HEAD', limit: 0, offset: 0)
+        gitaly_repository_client.search_files_by_regexp(ref, filter, limit: limit, offset: offset).map do |file|
           Gitlab::EncodingHelper.encode_utf8(file)
         end
       end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index f11437552e1fd..8934067551c20 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -303,8 +303,8 @@ def raw_changes_between(from, to)
         GitalyClient.call(@storage, :repository_service, :get_raw_changes, request, timeout: GitalyClient.fast_timeout)
       end
 
-      def search_files_by_name(ref, query)
-        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
+      def search_files_by_name(ref, query, limit: 0, offset: 0)
+        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query, limit: limit, offset: offset)
         GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
       end
 
@@ -314,8 +314,8 @@ def search_files_by_content(ref, query, options = {})
         search_results_from_response(response, options)
       end
 
-      def search_files_by_regexp(ref, filter)
-        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter)
+      def search_files_by_regexp(ref, filter, limit: 0, offset: 0)
+        request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter, limit: limit, offset: offset)
         GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
       end
 
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index b1aa90449e177..7f0a87be939d8 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -161,9 +161,10 @@
       let(:wiki_pages) { subject.list_pages }
 
       before do
-        subject.create_page('index', 'This is an index')
+        # The order is intentional
         subject.create_page('index2', 'This is an index2')
-        subject.create_page('an index3', 'This is an index3')
+        subject.create_page('index', 'This is an index')
+        subject.create_page('index3', 'This is an index3')
       end
 
       it 'returns an array of WikiPage instances' do
@@ -183,13 +184,47 @@
 
       context 'with limit option' do
         it 'returns limited set of pages' do
-          expect(subject.list_pages(limit: 1).count).to eq(1)
+          expect(
+            subject.list_pages(limit: 1).map(&:title)
+          ).to eql(%w[index])
+        end
+
+        it 'returns all set of pages if limit is more than the total pages' do
+          expect(subject.list_pages(limit: 4).count).to eq(3)
+        end
+
+        it 'returns all set of pages if limit is 0' do
+          expect(subject.list_pages(limit: 0).count).to eq(3)
+        end
+      end
+
+      context 'with offset option' do
+        it 'returns offset-ed set of pages' do
+          expect(
+            subject.list_pages(offset: 1).map(&:title)
+          ).to eq(%w[index2 index3])
+
+          expect(
+            subject.list_pages(offset: 2).map(&:title)
+          ).to eq(["index3"])
+          expect(subject.list_pages(offset: 3).count).to eq(0)
+          expect(subject.list_pages(offset: 4).count).to eq(0)
+        end
+
+        it 'returns all set of pages if offset is 0' do
+          expect(subject.list_pages(offset: 0).count).to eq(3)
+        end
+
+        it 'can combines with limit' do
+          expect(
+            subject.list_pages(offset: 1, limit: 1).map(&:title)
+          ).to eq(["index2"])
         end
       end
 
       context 'with sorting options' do
         it 'returns pages sorted by title by default' do
-          pages = ['an index3', 'index', 'index2']
+          pages = %w[index index2 index3]
 
           expect(subject.list_pages.map(&:title)).to eq(pages)
           expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
@@ -200,9 +235,9 @@
         let(:pages) { subject.list_pages(load_content: true) }
 
         it 'loads WikiPage content' do
-          expect(pages.first.content).to eq('This is an index3')
-          expect(pages.second.content).to eq('This is an index')
-          expect(pages.third.content).to eq('This is an index2')
+          expect(pages.first.content).to eq('This is an index')
+          expect(pages.second.content).to eq('This is an index2')
+          expect(pages.third.content).to eq('This is an index3')
         end
       end
     end
-- 
GitLab