Index project on creation and cache eviction

Change-Id: I5b737803dae56331845c46719dc30de99ad6ff27
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index ad2c50c..9debc2e 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -407,6 +407,10 @@
 +
 Update of the group secondary index
 
+* `com.google.gerrit.server.extensions.events.ProjectIndexedListener`:
++
+Update of the project secondary index
+
 * `com.google.gerrit.httpd.WebLoginListener`:
 +
 User login or logout interactively on the Web user interface.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectIndexedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectIndexedListener.java
new file mode 100644
index 0000000..bfbd851
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectIndexedListener.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a project is indexed */
+@ExtensionPoint
+public interface ProjectIndexedListener {
+  /**
+   * Invoked when a project is indexed
+   *
+   * @param name of the project
+   */
+  void onProjectIndexed(String project);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index b7aa416..cff720c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2563,7 +2563,11 @@
       }
       if (isConfig(cmd)) {
         logDebug("Reloading project in cache");
-        projectCache.evict(project);
+        try {
+          projectCache.evict(project);
+        } catch (IOException e) {
+          log.warn("Cannot evict from project cache, name key: " + project.getName(), e);
+        }
         ProjectState ps = projectCache.get(project.getNameKey());
         try {
           logDebug("Updating project description");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 6854a87..8c9a964 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -45,6 +45,12 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.index.group.GroupIndexerImpl;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
+import com.google.gerrit.server.index.project.ProjectIndexCollection;
+import com.google.gerrit.server.index.project.ProjectIndexDefinition;
+import com.google.gerrit.server.index.project.ProjectIndexRewriter;
+import com.google.gerrit.server.index.project.ProjectIndexer;
+import com.google.gerrit.server.index.project.ProjectIndexerImpl;
+import com.google.gerrit.server.index.project.ProjectSchemaDefinitions;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Provides;
@@ -70,7 +76,8 @@
       ImmutableList.<SchemaDefinitions<?>>of(
           AccountSchemaDefinitions.INSTANCE,
           ChangeSchemaDefinitions.INSTANCE,
-          GroupSchemaDefinitions.INSTANCE);
+          GroupSchemaDefinitions.INSTANCE,
+          ProjectSchemaDefinitions.INSTANCE);
 
   /** Type of secondary index. */
   public static IndexType getIndexType(Injector injector) {
@@ -112,14 +119,22 @@
     listener().to(GroupIndexCollection.class);
     factory(GroupIndexerImpl.Factory.class);
 
+    bind(ProjectIndexRewriter.class);
+    bind(ProjectIndexCollection.class);
+    listener().to(ProjectIndexCollection.class);
+    factory(ProjectIndexerImpl.Factory.class);
+
     DynamicSet.setOf(binder(), OnlineUpgradeListener.class);
   }
 
   @Provides
   Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions(
-      AccountIndexDefinition accounts, ChangeIndexDefinition changes, GroupIndexDefinition groups) {
+      AccountIndexDefinition accounts,
+      ChangeIndexDefinition changes,
+      GroupIndexDefinition groups,
+      ProjectIndexDefinition projects) {
     Collection<IndexDefinition<?, ?, ?>> result =
-        ImmutableList.<IndexDefinition<?, ?, ?>>of(accounts, groups, changes);
+        ImmutableList.<IndexDefinition<?, ?, ?>>of(accounts, groups, changes, projects);
     Set<String> expected =
         FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
     Set<String> actual = FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
@@ -156,6 +171,13 @@
 
   @Provides
   @Singleton
+  ProjectIndexer getProjectIndexer(
+      ProjectIndexerImpl.Factory factory, ProjectIndexCollection indexes) {
+    return factory.create(indexes);
+  }
+
+  @Provides
+  @Singleton
   @IndexExecutor(INTERACTIVE)
   ListeningExecutorService getInteractiveIndexExecutor(
       @GerritServerConfig Config config, WorkQueue workQueue) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/project/IndexedProjectQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/IndexedProjectQuery.java
new file mode 100644
index 0000000..ede7461
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/IndexedProjectQuery.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.project;
+
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexedQuery;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectState;
+
+public class IndexedProjectQuery extends IndexedQuery<Project.NameKey, ProjectState>
+    implements DataSource<ProjectState> {
+
+  public IndexedProjectQuery(
+      Index<Project.NameKey, ProjectState> index, Predicate<ProjectState> pred, QueryOptions opts)
+      throws QueryParseException {
+    super(index, pred, opts.convertForBackend());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexRewriter.java
new file mode 100644
index 0000000..e50d08e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexRewriter.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.project;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.index.IndexRewriter;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ProjectIndexRewriter implements IndexRewriter<ProjectState> {
+  private final ProjectIndexCollection indexes;
+
+  @Inject
+  ProjectIndexRewriter(ProjectIndexCollection indexes) {
+    this.indexes = indexes;
+  }
+
+  @Override
+  public Predicate<ProjectState> rewrite(Predicate<ProjectState> in, QueryOptions opts)
+      throws QueryParseException {
+    ProjectIndex index = indexes.getSearchIndex();
+    checkNotNull(index, "no active search index configured for projects");
+    return new IndexedProjectQuery(index, in, opts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexer.java
new file mode 100644
index 0000000..e8a8183
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexer.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.project;
+
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+
+public interface ProjectIndexer {
+
+  /**
+   * Synchronously index a project.
+   *
+   * @param nameKey name key of project to index.
+   */
+  void index(Project.NameKey nameKey) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
new file mode 100644
index 0000000..368a056
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.project;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+public class ProjectIndexerImpl implements ProjectIndexer {
+  public interface Factory {
+    ProjectIndexerImpl create(ProjectIndexCollection indexes);
+
+    ProjectIndexerImpl create(@Nullable ProjectIndex index);
+  }
+
+  private final ProjectCache projectCache;
+  private final DynamicSet<ProjectIndexedListener> indexedListener;
+  private final ProjectIndexCollection indexes;
+  private final ProjectIndex index;
+
+  @AssistedInject
+  ProjectIndexerImpl(
+      ProjectCache projectCache,
+      DynamicSet<ProjectIndexedListener> indexedListener,
+      @Assisted ProjectIndexCollection indexes) {
+    this.projectCache = projectCache;
+    this.indexedListener = indexedListener;
+    this.indexes = indexes;
+    this.index = null;
+  }
+
+  @AssistedInject
+  ProjectIndexerImpl(
+      ProjectCache projectCache,
+      DynamicSet<ProjectIndexedListener> indexedListener,
+      @Assisted ProjectIndex index) {
+    this.projectCache = projectCache;
+    this.indexedListener = indexedListener;
+    this.indexes = null;
+    this.index = index;
+  }
+
+  @Override
+  public void index(Project.NameKey nameKey) throws IOException {
+    for (Index<?, ProjectState> i : getWriteIndexes()) {
+      i.replace(projectCache.get(nameKey));
+    }
+    fireProjectIndexedEvent(nameKey.get());
+  }
+
+  private void fireProjectIndexedEvent(String name) {
+    for (ProjectIndexedListener listener : indexedListener) {
+      listener.onProjectIndexed(name);
+    }
+  }
+
+  private Collection<ProjectIndex> getWriteIndexes() {
+    if (indexes != null) {
+      return indexes.getWriteIndexes();
+    }
+
+    return index != null ? Collections.singleton(index) : ImmutableSet.of();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
index 0f71ac8..b68446f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
 import com.google.inject.servlet.RequestScoped;
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -48,7 +49,7 @@
     return ctl;
   }
 
-  public void evict(Project project) {
+  public void evict(Project project) throws IOException {
     projectCache.evict(project);
     controls.remove(project.getNameKey());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 65c7315..63052bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -45,17 +45,27 @@
    */
   ProjectState checkedGet(Project.NameKey projectName) throws IOException;
 
-  /** Invalidate the cached information about the given project. */
-  void evict(Project p);
+  /**
+   * Invalidate the cached information about the given project, and triggers reindexing for it
+   *
+   * @param p project that is being evicted
+   * @throws IOException thrown if the reindexing fails
+   */
+  void evict(Project p) throws IOException;
 
-  /** Invalidate the cached information about the given project. */
-  void evict(Project.NameKey p);
+  /**
+   * Invalidate the cached information about the given project, and triggers reindexing for it
+   *
+   * @param p the NameKey of the project that is being evicted
+   * @throws IOException thrown if the reindexing fails
+   */
+  void evict(Project.NameKey p) throws IOException;
 
   /**
    * Remove information about the given project from the cache. It will no longer be returned from
    * {@link #all()}.
    */
-  void remove(Project p);
+  void remove(Project p) throws IOException;
 
   /** @return sorted iteration of projects. */
   Iterable<Project.NameKey> all();
@@ -75,5 +85,5 @@
   Iterable<Project.NameKey> byName(String prefix);
 
   /** Notify the cache that a new project was constructed. */
-  void onCreateProject(Project.NameKey newProjectName);
+  void onCreateProject(Project.NameKey newProjectName) throws IOException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 6ee143c..2b31ce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -28,8 +28,10 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.project.ProjectIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
@@ -82,6 +84,7 @@
   private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
   private final Lock listLock;
   private final ProjectCacheClock clock;
+  private final Provider<ProjectIndexer> indexer;
 
   @Inject
   ProjectCacheImpl(
@@ -89,13 +92,15 @@
       final AllUsersName allUsersName,
       @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
       @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
-      ProjectCacheClock clock) {
+      ProjectCacheClock clock,
+      Provider<ProjectIndexer> indexer) {
     this.allProjectsName = allProjectsName;
     this.allUsersName = allUsersName;
     this.byName = byName;
     this.list = list;
     this.listLock = new ReentrantLock(true /* fair */);
     this.clock = clock;
+    this.indexer = indexer;
   }
 
   @Override
@@ -151,22 +156,20 @@
   }
 
   @Override
-  public void evict(Project p) {
-    if (p != null) {
-      byName.invalidate(p.getNameKey().get());
-    }
+  public void evict(Project p) throws IOException {
+    evict(p.getNameKey());
   }
 
-  /** Invalidate the cached information about the given project. */
   @Override
-  public void evict(Project.NameKey p) {
+  public void evict(Project.NameKey p) throws IOException {
     if (p != null) {
       byName.invalidate(p.get());
     }
+    indexer.get().index(p);
   }
 
   @Override
-  public void remove(Project p) {
+  public void remove(Project p) throws IOException {
     listLock.lock();
     try {
       SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
@@ -181,7 +184,7 @@
   }
 
   @Override
-  public void onCreateProject(Project.NameKey newProjectName) {
+  public void onCreateProject(Project.NameKey newProjectName) throws IOException {
     listLock.lock();
     try {
       SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
@@ -192,6 +195,7 @@
     } finally {
       listLock.unlock();
     }
+    indexer.get().index(newProjectName);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 7a7418c..1166970 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -390,6 +390,10 @@
     }
   }
 
+  public boolean canRead() {
+    return !isHidden() && allRefsAreVisible(Collections.emptySet());
+  }
+
   ForProject asForProject() {
     return new ForProjectImpl();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
new file mode 100644
index 0000000..07b1722
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.project;
+
+import com.google.gerrit.index.query.IsVisibleToPredicate;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.account.AccountQueryBuilder;
+import com.google.gwtorm.server.OrmException;
+
+public class ProjectIsVisibleToPredicate extends IsVisibleToPredicate<ProjectState> {
+  protected final PermissionBackend permissionBackend;
+  protected final CurrentUser user;
+
+  public ProjectIsVisibleToPredicate(PermissionBackend permissionBackend, CurrentUser user) {
+    super(AccountQueryBuilder.FIELD_VISIBLETO, IndexUtils.describe(user));
+    this.permissionBackend = permissionBackend;
+    this.user = user;
+  }
+
+  @Override
+  public boolean match(ProjectState projectState) throws OrmException {
+    return permissionBackend
+        .user(user)
+        .project(projectState.getProject().getNameKey())
+        .testOrFalse(ProjectPermission.READ);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index f3efdc1..2457f33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -14,14 +14,19 @@
 
 package com.google.gerrit.server.query.project;
 
+import com.google.common.primitives.Ints;
+import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 
 /** Parses a query string meant to be applied to project objects. */
 public class ProjectQueryBuilder extends QueryBuilder<ProjectState> {
+  public static final String FIELD_LIMIT = "limit";
+
   private static final QueryBuilder.Definition<ProjectState, ProjectQueryBuilder> mydef =
       new QueryBuilder.Definition<>(ProjectQueryBuilder.class);
 
@@ -34,4 +39,13 @@
   public Predicate<ProjectState> name(String name) {
     return ProjectPredicates.name(new Project.NameKey(name));
   }
+
+  @Operator
+  public Predicate<ProjectState> limit(String query) throws QueryParseException {
+    Integer limit = Ints.tryParse(query);
+    if (limit == null) {
+      throw error("Invalid limit: " + query);
+    }
+    return new LimitPredicate<>(FIELD_LIMIT, limit);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
new file mode 100644
index 0000000..234a67b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.project;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.query.project.ProjectQueryBuilder.FIELD_LIMIT;
+
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.AndSource;
+import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryProcessor;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountLimits;
+import com.google.gerrit.server.index.project.ProjectIndexCollection;
+import com.google.gerrit.server.index.project.ProjectIndexRewriter;
+import com.google.gerrit.server.index.project.ProjectSchemaDefinitions;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Query processor for the project index.
+ *
+ * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
+ * holding on to a single instance.
+ */
+public class ProjectQueryProcessor extends QueryProcessor<ProjectState> {
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> userProvider;
+
+  static {
+    // It is assumed that basic rewrites do not touch visibleto predicates.
+    checkState(
+        !ProjectIsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
+        "ProjectQueryProcessor assumes visibleto is not used by the index rewriter.");
+  }
+
+  @Inject
+  protected ProjectQueryProcessor(
+      Provider<CurrentUser> userProvider,
+      AccountLimits.Factory limitsFactory,
+      MetricMaker metricMaker,
+      IndexConfig indexConfig,
+      ProjectIndexCollection indexes,
+      ProjectIndexRewriter rewriter,
+      PermissionBackend permissionBackend) {
+    super(
+        metricMaker,
+        ProjectSchemaDefinitions.INSTANCE,
+        indexConfig,
+        indexes,
+        rewriter,
+        FIELD_LIMIT,
+        () -> limitsFactory.create(userProvider.get()).getQueryLimit());
+    this.permissionBackend = permissionBackend;
+    this.userProvider = userProvider;
+  }
+
+  @Override
+  protected Predicate<ProjectState> enforceVisibility(Predicate<ProjectState> pred) {
+    return new AndSource<>(
+        pred, new ProjectIsVisibleToPredicate(permissionBackend, userProvider.get()), start);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 0d7fa24..6649fcb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -174,7 +174,13 @@
         err.append("error: ").append(msg).append("\n");
       }
 
-      projectCache.evict(nameKey);
+      try {
+        projectCache.evict(nameKey);
+      } catch (IOException e) {
+        final String msg = "Cannot reindex project: " + name;
+        log.error(msg, e);
+        err.append("error: ").append(msg).append("\n");
+      }
     }
 
     if (err.length() > 0) {