MergeSuperSet: Handle changes that exist in index but not in NoteDb gracefully

If a system has multiple primary nodes it can happen that the index
returns changes for a topic that don't exist in NoteDb yet which then
causes the SubmittedTogether REST endpoint to fail on permission checks:

1. Change 1 with topic foo is created and replicated to all nodes.
2. Change 2 with topic foo is created on node X, the index data is
   replicated to all nodes, but the replication for the Git data is
   still pending.
3. The SubmittedTogether REST endpoint is invoked for change 1 on node
   Y. A query to the change index is executed to find all changes that
   have the same topic as change 1. The index query by topic returns
   change 1 and change 2 (because the index data is already replicated).
   Checking read permissions for change 2 fails because loading the
   change notes from Git fails (because the Git data was not replicated
   yet).

Instead of failing we now create the change notes from the index data so
that the read permission check can be performed successfully.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I4c3284877483b8248f8c2a1f7f529d93f41dbbb8
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index bcebc7f..b36eb7d 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -19,16 +19,19 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginContext;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -53,6 +56,7 @@
  * included.
  */
 public class MergeSuperSet {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeOpRepoManager> repoManagerProvider;
@@ -60,6 +64,7 @@
   private final PermissionBackend permissionBackend;
   private final Config cfg;
   private final ProjectCache projectCache;
+  private final ChangeNotes.Factory notesFactory;
 
   private MergeOpRepoManager orm;
   private boolean closeOrm;
@@ -71,13 +76,15 @@
       Provider<MergeOpRepoManager> repoManagerProvider,
       DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
       PermissionBackend permissionBackend,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      ChangeNotes.Factory notesFactory) {
     this.cfg = cfg;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
     this.mergeSuperSetComputation = mergeSuperSetComputation;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
+    this.notesFactory = notesFactory;
   }
 
   public static boolean wholeTopicEnabled(Config config) {
@@ -215,8 +222,23 @@
       return false;
     }
 
+    ChangeNotes notes;
     try {
-      permissionBackend.user(user).change(cd).check(ChangePermission.READ);
+      notes = cd.notes();
+    } catch (NoSuchChangeException e) {
+      // The change was found in the index but is missing in NoteDb.
+      // This can happen in systems with multiple primary nodes when the replication of the index
+      // documents is faster than the replication of the Git data.
+      // Instead of failing, create the change notes from the index data so that the read permission
+      // check can be performed successfully.
+      logger.atWarning().log(
+          "Got change %d of project %s from index, but couldn't find it in NoteDb",
+          cd.getId().get(), cd.project().get());
+      notes = notesFactory.createFromIndexedChange(cd.change());
+    }
+
+    try {
+      permissionBackend.user(user).change(notes).check(ChangePermission.READ);
       return true;
     } catch (AuthException e) {
       return false;