| /* |
| * Copyright 2016 The Bazel Authors. All rights reserved. |
| * |
| * 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.idea.blaze.base.experiments; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Maps; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.ListenableScheduledFuture; |
| import com.google.common.util.concurrent.ListeningScheduledExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.gson.JsonParseException; |
| import com.google.idea.blaze.base.util.SerializationUtil; |
| import com.intellij.openapi.application.PathManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.util.io.HttpRequests; |
| import org.jetbrains.io.JsonReaderEx; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.*; |
| |
| /** |
| * A singleton class that retrieves the experiments from the experiments service</a>. |
| * |
| * The first time {@link #getExperimentValues()} is called, fresh data will be retrieved in the |
| * current thread. Thereafter, data will be retrieved every 5 minutes in a background thread. If |
| * there is a failure retrieving data, new attempts will be made every minute. |
| */ |
| class WebExperimentSyncer { |
| private static final String DEFAULT_EXPERIMENT_URL = |
| "https://intellij-experiments.appspot.com/api/experiments"; |
| private static final String EXPERIMENTS_URL_PROPERTY = "blaze.experiments.url"; |
| |
| private static final int SUCESSFUL_DOWNLOAD_DELAY_MINUTES = 5; |
| private static final int DOWNLOAD_FAILURE_DELAY_MINUTES = 1; |
| |
| private static final String CACHE_FILE_NAME = "blaze.experiments.cache.dat"; |
| |
| private static final Logger LOG = Logger.getInstance(WebExperimentSyncer.class); |
| |
| private static final WebExperimentSyncer INSTANCE = new WebExperimentSyncer(); |
| |
| // null indicates no fetch attempt has been made. After the first attempt, this will always be a |
| // (possibly empty) map. |
| private Map<String, String> experimentValues = null; |
| |
| private ListeningScheduledExecutorService executor = |
| MoreExecutors.listeningDecorator( |
| MoreExecutors.getExitingScheduledExecutorService( |
| new ScheduledThreadPoolExecutor(1), 0, TimeUnit.SECONDS)); |
| |
| private WebExperimentSyncer() {} |
| |
| public static WebExperimentSyncer getInstance() { |
| return INSTANCE; |
| } |
| |
| /** |
| * Get the last-retrieved set of experiment values. |
| * |
| * The first time this method is called, an attempt to retrieve the values will take place. |
| * Thereafter, the values will be retrieved every five minutes on a background thread and this |
| * method will return the most recent successfully retrieved values. |
| */ |
| public synchronized Map<String, String> getExperimentValues() { |
| if (experimentValues == null) { |
| initialize(); |
| } |
| |
| return experimentValues; |
| } |
| |
| private synchronized void setExperimentValues(HashMap<String, String> experimentValues) { |
| this.experimentValues = experimentValues; |
| saveCache(experimentValues); |
| } |
| |
| /** |
| * Fetch and process the experiments on the current thread. |
| */ |
| private void initialize() { |
| ListenableFuture<String> response = |
| MoreExecutors.sameThreadExecutor().submit(new WebExperimentsDownloader()); |
| response.addListener( |
| new WebExperimentsResultProcessor(response, false), MoreExecutors.sameThreadExecutor()); |
| |
| // Failed to fetch, try to load cache from disk |
| if (experimentValues == null) { |
| experimentValues = loadCache(); |
| } |
| |
| // There must have been an error retrieving the experiments. |
| if (experimentValues == null) { |
| experimentValues = ImmutableMap.of(); |
| } |
| } |
| |
| private void scheduleNextRefresh(boolean refreshWasSuccessful) { |
| int delayInMinutes = |
| refreshWasSuccessful ? SUCESSFUL_DOWNLOAD_DELAY_MINUTES : DOWNLOAD_FAILURE_DELAY_MINUTES; |
| ListenableScheduledFuture<String> refreshResults = |
| executor.schedule(new WebExperimentsDownloader(), delayInMinutes, TimeUnit.MINUTES); |
| refreshResults.addListener( |
| new WebExperimentsResultProcessor(refreshResults, true), MoreExecutors.sameThreadExecutor()); |
| } |
| |
| private static class WebExperimentsDownloader implements Callable<String> { |
| |
| @Override |
| public String call() throws Exception { |
| LOG.debug("About to fetch experiments."); |
| return HttpRequests.request( |
| System.getProperty(EXPERIMENTS_URL_PROPERTY, DEFAULT_EXPERIMENT_URL)) |
| .readString(null /* progress indicator */); |
| } |
| } |
| |
| private class WebExperimentsResultProcessor implements Runnable { |
| |
| private final Future<String> resultFuture; |
| private final boolean triggerExperimentsReload; |
| |
| private WebExperimentsResultProcessor(Future<String> resultFuture, |
| boolean triggerExperimentsReload) { |
| this.resultFuture = resultFuture; |
| this.triggerExperimentsReload = triggerExperimentsReload; |
| } |
| |
| @Override |
| public void run() { |
| LOG.debug("Experiments fetched. Processing results."); |
| try { |
| HashMap<String, String> mapBuilder = Maps.newHashMap(); |
| String result = resultFuture.get(); |
| try (JsonReaderEx reader = new JsonReaderEx(result)) { |
| reader.beginObject(); |
| while (reader.hasNext()) { |
| String experimentName = reader.nextName(); |
| String experimentValue = reader.nextString(); |
| mapBuilder.put(experimentName, experimentValue); |
| } |
| } |
| setExperimentValues(mapBuilder); |
| |
| if (triggerExperimentsReload) { |
| ExperimentService.getInstance().reloadExperiments(); |
| } |
| LOG.debug("Successfully fetched experiments: " + getExperimentValues()); |
| scheduleNextRefresh(true /* refreshWasSuccessful */); |
| } catch (InterruptedException | ExecutionException | JsonParseException e) { |
| LOG.debug("Error fetching experiments", e); |
| scheduleNextRefresh(false /* refreshWasSuccessful */); |
| } |
| } |
| } |
| |
| private static void saveCache(HashMap<String, String> experiments) { |
| try { |
| SerializationUtil.saveToDisk(getCacheFile(), experiments); |
| } catch (IOException e) { |
| LOG.warn("Could not save experiments cache to disk: " + getCacheFile()); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static HashMap<String, String> loadCache() { |
| try { |
| return (HashMap<String, String>)SerializationUtil.loadFromDisk(getCacheFile(), ImmutableList.of()); |
| } |
| catch (IOException e) { |
| // This is normal, we might be offline and have never loaded the cache. |
| LOG.info("Could not load experiments file: " + getCacheFile()); |
| } |
| return null; |
| } |
| |
| private static File getCacheFile() { |
| return new File(new File(PathManager.getSystemPath(), "blaze"), CACHE_FILE_NAME).getAbsoluteFile(); |
| } |
| } |