Lookup changes in parallel during ReceiveCommits

If the database has high query latency, the loop that locates existing
changes on the destination branch given Change-Id can be slow. Start
all of the queries as commits are discovered, but don't block on
results until all queries were started.

If the database can build the ResultSet in the background, this may
hide some of the query latency by allowing the queries to overlap when
more than one lookup must be performed for a push.

Change-Id: I26f02506317d1c18e5d9562133a4afc0c32416ed
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index ef3530e..9571f39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -1137,6 +1137,7 @@
         }
       }
 
+      List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
       for (;;) {
         final RevCommit c = walk.next();
@@ -1156,53 +1157,58 @@
 
         Change.Key changeKey = new Change.Key("I" + c.name());
         final List<String> idList = c.getFooterLines(CHANGE_ID);
-        if (!idList.isEmpty()) {
-          final String idStr = idList.get(idList.size() - 1).trim();
-          if (idStr.matches("^I00*$")) {
-            // Reject this invalid line from EGit.
+        if (idList.isEmpty()) {
+          newChanges.add(new CreateRequest(c, changeKey));
+          continue;
+        }
+
+        final String idStr = idList.get(idList.size() - 1).trim();
+        if (idStr.matches("^I00*$")) {
+          // Reject this invalid line from EGit.
+          reject(newChange, "invalid Change-Id");
+          return Collections.emptyList();
+        }
+
+        changeKey = new Change.Key(idStr);
+        pending.add(new ChangeLookup(c, changeKey));
+      }
+
+      for (ChangeLookup p : pending) {
+        if (newChangeIds.contains(p.changeKey)) {
+          reject(newChange, "squash commits first");
+          return Collections.emptyList();
+        }
+
+        List<Change> changes = p.changes.toList();
+        if (changes.size() > 1) {
+          // WTF, multiple changes in this project have the same key?
+          // Since the commit is new, the user should recreate it with
+          // a different Change-Id. In practice, we should never see
+          // this error message as Change-Id should be unique.
+          //
+          reject(newChange, p.changeKey.get() + " has duplicates");
+          return Collections.emptyList();
+        }
+
+        if (changes.size() == 1) {
+          // Schedule as a replacement to this one matching change.
+          //
+          if (requestReplace(newChange, false, changes.get(0), p.commit)) {
+            continue;
+          } else {
+            return Collections.emptyList();
+          }
+        }
+
+        if (changes.size() == 0) {
+          if (!isValidChangeId(p.changeKey.get())) {
             reject(newChange, "invalid Change-Id");
             return Collections.emptyList();
           }
 
-          changeKey = new Change.Key(idStr);
-          if (newChangeIds.contains(changeKey)) {
-            reject(newChange, "squash commits first");
-            return Collections.emptyList();
-          }
-
-          final List<Change> changes =
-              db.changes().byBranchKey(destBranch, changeKey).toList();
-          if (changes.size() > 1) {
-            // WTF, multiple changes in this project have the same key?
-            // Since the commit is new, the user should recreate it with
-            // a different Change-Id. In practice, we should never see
-            // this error message as Change-Id should be unique.
-            //
-            reject(newChange, changeKey.get() + " has duplicates");
-            return Collections.emptyList();
-          }
-
-          if (changes.size() == 1) {
-            // Schedule as a replacement to this one matching change.
-            //
-            if (requestReplace(newChange, false, changes.get(0), c)) {
-              continue;
-            } else {
-              return Collections.emptyList();
-            }
-          }
-
-          if (changes.size() == 0) {
-            if (!isValidChangeId(idStr)) {
-              reject(newChange, "invalid Change-Id");
-              return Collections.emptyList();
-            }
-
-            newChangeIds.add(changeKey);
-          }
+          newChangeIds.add(p.changeKey);
         }
-
-        newChanges.add(new CreateRequest(c, changeKey));
+        newChanges.add(new CreateRequest(p.commit, p.changeKey));
       }
     } catch (IOException e) {
       // Should never happen, the core receive process would have
@@ -1227,11 +1233,22 @@
     return newChanges;
   }
 
-
   private static boolean isValidChangeId(String idStr) {
     return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
   }
 
+  private class ChangeLookup {
+    final RevCommit commit;
+    final Change.Key changeKey;
+    final ResultSet<Change> changes;
+
+    ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
+      commit = c;
+      changeKey = key;
+      changes = db.changes().byBranchKey(destBranch, key);
+    }
+  }
+
   private class CreateRequest {
     final RevCommit commit;
     final Change change;