Merge "Make RevisionNote public for checks plugin"
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 18aab18..d15d0dd 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
+import static java.util.concurrent.TimeUnit.MINUTES;
 
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.index.IndexConfig;
@@ -55,6 +56,8 @@
  */
 public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
     implements DynamicOptions.BeanReceiver, PluginDefinedAttributesFactory {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   /**
    * Register a ChangeAttributeFactory in a config Module like this:
    *
@@ -67,9 +70,7 @@
 
   private final Provider<CurrentUser> userProvider;
   private final ChangeNotes.Factory notesFactory;
-  private final DynamicMap<ChangeAttributeFactory> attributeFactories;
-  private final Multimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin =
-      HashMultimap.create();
+  private final ImmutableListMultimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin;
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final Provider<AnonymousUser> anonymousUserProvider;
@@ -105,11 +106,16 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.notesFactory = notesFactory;
-    this.attributeFactories = attributeFactories;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.anonymousUserProvider = anonymousUserProvider;
-    setupAttributeFactories();
+
+    ImmutableListMultimap.Builder<String, ChangeAttributeFactory> factoriesBuilder =
+        ImmutableListMultimap.builder();
+    // Eagerly call Extension#get() rather than storing Extensions, since that method invokes the
+    // Provider on every call, which could be expensive if we invoke it once for every change.
+    attributeFactories.forEach(e -> factoriesBuilder.put(e.getPluginName(), e.get()));
+    attributeFactoriesByPlugin = factoriesBuilder.build();
   }
 
   @Override
@@ -133,25 +139,18 @@
     return dynamicBeans.get(plugin);
   }
 
-  public void setupAttributeFactories() {
-    for (String plugin : attributeFactories.plugins()) {
-      for (Provider<ChangeAttributeFactory> provider :
-          attributeFactories.byPlugin(plugin).values()) {
-        attributeFactoriesByPlugin.put(plugin, provider.get());
-      }
-    }
-  }
-
   @Override
   public List<PluginDefinedInfo> create(ChangeData cd) {
-    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size());
+    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactoriesByPlugin.size());
     for (Map.Entry<String, ChangeAttributeFactory> e : attributeFactoriesByPlugin.entries()) {
       String plugin = e.getKey();
       PluginDefinedInfo pda = null;
       try {
         pda = e.getValue().create(cd, this, plugin);
       } catch (RuntimeException ex) {
-        /* Eat runtime exceptions so that queries don't fail. */
+        // Log once a minute, to avoid spamming logs with one stack trace per change.
+        logger.atWarning().atMostEvery(1, MINUTES).withCause(ex).log(
+            "error populating attribute on change %s from plugin %s", cd.getId(), plugin);
       }
       if (pda != null) {
         pda.name = plugin;