Merge changes from topic 'max-pages'

* changes:
  IndexConfig: Add maxPages option
  IndexConfig: Configure maxLimit from Config
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 61c764a..8119d4a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2205,6 +2205,24 @@
 +
 Defaults to true.
 
+[[index.maxLimit]]index.maxLimit::
++
+Maximum limit to allow for search queries. Requesting results above this
+limit will truncate the list (but will still set `_more_changes` on
+result lists). Set to 0 for no limit.
++
+Defaults to no limit.
+
+[[index.maxPages]]index.maxPages::
++
+Maximum number of pages of search results to allow, as index
+implementations may have to scan through large numbers of skipped
+results when searching with an offset. Requesting results starting past
+this threshold times the requested limit will result in an error. Set to
+0 for no limit.
++
+Defaults to no limit.
+
 ==== Lucene configuration
 
 Open and closed changes are indexed in separate indexes named
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 0fc11d9..35d6636 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
@@ -26,6 +27,8 @@
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 public class LuceneIndexModule extends LifecycleModule {
   private final Integer singleVersion;
   private final int threads;
@@ -44,7 +47,6 @@
 
   @Override
   protected void configure() {
-    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     factory(LuceneChangeIndex.Factory.class);
     install(new IndexModule(threads));
     if (singleVersion == null && base == null) {
@@ -54,6 +56,12 @@
     }
   }
 
+  @Provides
+  @Singleton
+  IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+    return IndexConfig.fromConfig(cfg);
+  }
+
   private static class MultiVersionModule extends LifecycleModule {
     @Override
     public void configure() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
index 1857e55..08f4748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
@@ -18,6 +18,8 @@
 
 import com.google.auto.value.AutoValue;
 
+import org.eclipse.jgit.lib.Config;
+
 /**
  * Implementation-specific configuration for secondary indexes.
  * <p>
@@ -28,13 +30,30 @@
 @AutoValue
 public abstract class IndexConfig {
   public static IndexConfig createDefault() {
-    return create(Integer.MAX_VALUE);
+    return create(0, 0);
   }
 
-  public static IndexConfig create(int maxLimit) {
-    checkArgument(maxLimit > 0, "maxLimit must be positive: %s", maxLimit);
-    return new AutoValue_IndexConfig(maxLimit);
+  public static IndexConfig fromConfig(Config cfg) {
+    return create(
+        cfg.getInt("index", null, "maxLimit", 0),
+        cfg.getInt("index", null, "maxPages", 0));
+  }
+
+  public static IndexConfig create(int maxLimit, int maxPages) {
+    return new AutoValue_IndexConfig(
+        checkLimit(maxLimit, "maxLimit"),
+        checkLimit(maxPages, "maxPages"));
+  }
+
+  private static int checkLimit(int limit, String name) {
+    if (limit == 0) {
+      return Integer.MAX_VALUE;
+    }
+    checkArgument(limit > 0, "%s must be positive: %s", name, limit);
+    return limit;
   }
 
   public abstract int maxLimit();
+
+  public abstract int maxPages();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 51d971d..6068dd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -132,6 +132,13 @@
       if (limit == getBackendSupportedLimit()) {
         limit--;
       }
+
+      int page = (start / limit) + 1;
+      if (page > indexConfig.maxPages()) {
+        throw new QueryParseException(
+            "Cannot go beyond page " + indexConfig.maxPages() + "of results");
+      }
+
       Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(open(), q);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 9ad896e..85e06e8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -84,9 +84,19 @@
 @Ignore
 @RunWith(ConfigSuite.class)
 public abstract class AbstractQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return updateConfig(new Config());
+  }
+
   @ConfigSuite.Config
   public static Config noteDbEnabled() {
-    return NotesMigration.allEnabledConfig();
+    return updateConfig(NotesMigration.allEnabledConfig());
+  }
+
+  private static Config updateConfig(Config cfg) {
+    cfg.setInt("index", null, "maxPages", 10);
+    return cfg;
   }
 
   @ConfigSuite.Parameter public Config config;
@@ -557,6 +567,19 @@
   }
 
   @Test
+  public void maxPages() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, null, null).insert();
+
+    QueryRequest query = newQuery("status:new").withLimit(10);
+    assertQuery(query, change);
+    assertQuery(query.withStart(1));
+    assertQuery(query.withStart(99));
+    assertBadQuery(query.withStart(100));
+    assertQuery(query.withLimit(100).withStart(100));
+  }
+
+  @Test
   public void updateOrder() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
     TestRepository<InMemoryRepository> repo = createProject("repo");
@@ -1073,8 +1096,12 @@
   }
 
   protected void assertBadQuery(Object query) throws Exception {
+    assertBadQuery(newQuery(query));
+  }
+
+  protected void assertBadQuery(QueryRequest query) throws Exception {
     try {
-      newQuery(query).get();
+      query.get();
       fail("expected BadRequestException for query: " + query);
     } catch (BadRequestException e) {
       // Expected.
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
index 38de6ee..0133e33 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -50,7 +50,6 @@
 
   @Override
   protected void configure() {
-    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     install(new IndexModule(threads));
     bind(ChangeIndex.class).to(SolrChangeIndex.class);
     listener().to(SolrChangeIndex.class);
@@ -61,6 +60,12 @@
 
   @Provides
   @Singleton
+  IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+    return IndexConfig.fromConfig(cfg);
+  }
+
+  @Provides
+  @Singleton
   public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,