Pre-loads and cache Jenkins CI plugins list

Getting the list of plugins available on Jenkins CI may take quite
a long time (over 10-20 sec). We do need to pre-load that information
at plugin start-up and cache it in memory.

The list of plugins is typically quite small and does not change often.
Having a 24h TTL is more than acceptable.

Change-Id: Iea38f70bdc68f1712a298270af7906a32ec9c9bd
diff --git a/BUCK b/BUCK
index 8499ba4..51378d9 100644
--- a/BUCK
+++ b/BUCK
@@ -8,6 +8,7 @@
     'Gerrit-PluginName: plugin-manager',
     'Gerrit-ApiType: plugin',
     'Gerrit-HttpModule: com.googlesource.gerrit.plugins.manager.WebModule',
+    'Gerrit-Module: com.googlesource.gerrit.plugins.manager.Module',
     'Implementation-Title: Plugin manager',
     'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/plugin-manager',
   ],
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java b/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java
index d45c16f..d3d8316 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java
@@ -27,20 +27,20 @@
 
 import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
 
 /** List plugins available for installation. */
 @RequiresCapability(GlobalCapability.VIEW_PLUGINS)
 public class ListAvailablePlugins implements RestReadView<TopLevelResource> {
-  private final PluginsCentralLoader loader;
+  private final PluginsCentralCache pluginsCache;
 
   @Inject
-  public ListAvailablePlugins(PluginsCentralLoader loader) {
-    this.loader = loader;
+  public ListAvailablePlugins(PluginsCentralCache pluginsCache) {
+    this.pluginsCache = pluginsCache;
   }
 
   @Override
@@ -52,8 +52,8 @@
     Map<String, PluginInfo> output = Maps.newTreeMap();
     List<PluginInfo> plugins;
     try {
-      plugins = loader.availablePlugins();
-    } catch (IOException e) {
+      plugins = pluginsCache.availablePlugins();
+    } catch (ExecutionException e) {
       throw new RestApiException(
           "Unable to load the list of available plugins", e);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java b/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java
new file mode 100644
index 0000000..1c44dcd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2015 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.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.internal.UniqueAnnotations;
+
+import com.googlesource.gerrit.plugins.manager.repository.JenkinsCiPluginsRepository;
+import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
+
+public class Module extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(PluginsRepository.class).to(JenkinsCiPluginsRepository.class);
+
+    install(PluginsCentralCache.module());
+
+    bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create()).to(
+        OnStartStop.class);
+
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/OnStartStop.java b/src/main/java/com/googlesource/gerrit/plugins/manager/OnStartStop.java
new file mode 100644
index 0000000..8156016
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/OnStartStop.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2015 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.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
+import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+
+public class OnStartStop implements LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(OnStartStop.class);
+
+  private final PluginsRepository pluginsRepo;
+
+  private final String pluginName;
+
+  private final PluginManagerConfig config;
+
+  @Inject
+  public OnStartStop(PluginsRepository pluginsRepo,
+      @PluginName String pluginName, PluginManagerConfig config) {
+    this.pluginsRepo = pluginsRepo;
+    this.pluginName = pluginName;
+    this.config = config;
+  }
+
+  @Override
+  public void start() {
+    if (config.isCachePreloadEnabled()) {
+      Thread preloader = new Thread(new Runnable() {
+
+        @Override
+        public void run() {
+          log.info("Start-up: pre-loading list of plugins from registry");
+          try {
+            Collection<PluginInfo> plugins =
+                pluginsRepo.list(Version.getVersion());
+            log.info("{} plugins successfully pre-loaded", plugins.size());
+          } catch (IOException e) {
+            log.error("Cannot access plugins list at this time", e);
+          }
+        }
+      });
+      preloader.setName(pluginName + "-preloader");
+      preloader.start();
+    }
+  }
+
+  @Override
+  public void stop() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerConfig.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerConfig.java
new file mode 100644
index 0000000..b7dfb63
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerConfig.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 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.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+
+public class PluginManagerConfig {
+  private static final String DEFAULT_GERRIT_CI_URL =
+      "https://gerrit-ci.gerritforge.com";
+
+  private final PluginConfig config;
+
+  @Inject
+  public PluginManagerConfig(PluginConfigFactory configFactory,
+      @PluginName String pluginName) {
+    this.config = configFactory.getFromGerritConfig(pluginName);
+  }
+
+  public String getJenkinsUrl() {
+    return config.getString("jenkinsUrl", DEFAULT_GERRIT_CI_URL);
+  }
+
+  public boolean isCachePreloadEnabled() {
+    return config.getBoolean("preload", true);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralCache.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralCache.java
new file mode 100644
index 0000000..56cb82f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralCache.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2015 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.googlesource.gerrit.plugins.manager;
+
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import com.googlesource.gerrit.plugins.manager.PluginsCentralLoader.ListKey;
+import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+public class PluginsCentralCache {
+
+  private final LoadingCache<ListKey, List<PluginInfo>> pluginsCache;
+
+  public static final String PLUGINS_LIST_CACHE_NAME = "plugins_list";
+
+  @Inject
+  public PluginsCentralCache(@Named(PLUGINS_LIST_CACHE_NAME) LoadingCache<ListKey, List<PluginInfo>> pluginsCache) {
+    this.pluginsCache = pluginsCache;
+  }
+
+  public List<PluginInfo> availablePlugins() throws ExecutionException {
+    return pluginsCache.get(ListKey.ALL);
+  }
+
+  static CacheModule module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(PluginsCentralCache.PLUGINS_LIST_CACHE_NAME, ListKey.class,
+            new TypeLiteral<List<PluginInfo>>() {}).expireAfterWrite(1,
+            TimeUnit.DAYS).loader(PluginsCentralLoader.class);
+
+        bind(PluginsCentralCache.class);
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java
index 33943af..d0385c9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java
@@ -14,18 +14,26 @@
 
 package com.googlesource.gerrit.plugins.manager;
 
+import com.google.common.cache.CacheLoader;
 import com.google.gerrit.common.Version;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import com.googlesource.gerrit.plugins.manager.PluginsCentralLoader.ListKey;
 import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
 import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
 
-import java.io.IOException;
 import java.util.List;
 
 @Singleton
-public class PluginsCentralLoader {
+public class PluginsCentralLoader extends
+    CacheLoader<ListKey, List<PluginInfo>> {
+
+  public static class ListKey {
+    static final ListKey ALL = new ListKey();
+
+    private ListKey() {}
+  }
 
   private final PluginsRepository repository;
 
@@ -34,7 +42,8 @@
     this.repository = repository;
   }
 
-  public List<PluginInfo> availablePlugins() throws IOException {
+  @Override
+  public List<PluginInfo> load(ListKey all) throws Exception {
     return repository.list(Version.getVersion());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java b/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
index c9994f1..a80ee9a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
@@ -15,15 +15,11 @@
 
 import com.google.inject.servlet.ServletModule;
 
-import com.googlesource.gerrit.plugins.manager.repository.JenkinsCiPluginsRepository;
-import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
-
 public class WebModule extends ServletModule {
 
   @Override
   protected void configureServlets() {
     bind(AvailablePluginsCollection.class);
-    bind(PluginsRepository.class).to(JenkinsCiPluginsRepository.class);
 
     serve("/available*").with(PluginManagerRestApiServlet.class);
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
index d1620b3..6148407 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
@@ -16,9 +16,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.PluginConfig;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.inject.Inject;
@@ -26,6 +23,7 @@
 import com.google.inject.Singleton;
 
 import com.googlesource.gerrit.plugins.manager.GerritVersionBranch;
+import com.googlesource.gerrit.plugins.manager.PluginManagerConfig;
 import com.googlesource.gerrit.plugins.manager.gson.SmartGson;
 import com.googlesource.gerrit.plugins.manager.gson.SmartJson;
 
@@ -35,6 +33,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
 @Singleton
@@ -43,12 +42,11 @@
   private static final Logger log = LoggerFactory
       .getLogger(JenkinsCiPluginsRepository.class);
 
-  private static final String DEFAULT_GERRIT_CI_URL =
-      "https://gerrit-ci.gerritforge.com";
-
   private static final Optional<PluginInfo> noPluginInfo = Optional.absent();
 
-  private final PluginConfig config;
+  private final PluginManagerConfig config;
+
+  private HashMap<String, List<PluginInfo>> cache = new HashMap<>();
 
   static class View {
     String name;
@@ -65,20 +63,29 @@
 
   @Inject
   public JenkinsCiPluginsRepository(Provider<SmartGson> gsonProvider,
-      PluginConfigFactory configFactory, @PluginName String pluginName) {
+      PluginManagerConfig config) {
     this.gsonProvider = gsonProvider;
-    this.config = configFactory.getFromGerritConfig(pluginName);
+    this.config = config;
   }
 
   @Override
   public List<PluginInfo> list(String gerritVersion) throws IOException {
+    List<PluginInfo> list = cache.get(gerritVersion);
+    if(list == null) {
+      list = getList(gerritVersion);
+      cache.put(gerritVersion, list);
+    }
+    return list;
+  }
+
+  private List<PluginInfo> getList(String gerritVersion) throws IOException {
     SmartGson gson = gsonProvider.get();
     String viewName = "Plugins-" + GerritVersionBranch.getBranch(gerritVersion);
     List<PluginInfo> plugins = new ArrayList<>();
 
     try {
       Job[] jobs =
-          gson.get(getJenkinsUrl() + "/view/" + viewName + "/api/json",
+          gson.get(config.getJenkinsUrl() + "/view/" + viewName + "/api/json",
               View.class).jobs;
 
       for (Job job : jobs) {
@@ -96,10 +103,6 @@
     return plugins;
   }
 
-  private String getJenkinsUrl() {
-    return config.getString("jenkinsUrl", DEFAULT_GERRIT_CI_URL);
-  }
-
   private Optional<PluginInfo> getPluginInfo(final SmartGson gson, String url)
       throws IOException {
     SmartJson jobDetails = gson.get(url + "/api/json");