com.google.idea.blaze.base.experiments.WebExperimentSyncer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.base.experiments.WebExperimentSyncer.java

Source

/*
 * 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();
    }
}