package com.indeed.proctor.webapp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.indeed.proctor.common.ProctorSpecification;
import com.indeed.proctor.common.Serializers;
import com.indeed.proctor.common.SpecificationResult;
import com.indeed.proctor.webapp.db.Environment;
import com.indeed.proctor.webapp.model.AppVersion;
import com.indeed.proctor.webapp.model.ProctorClientApplication;
import com.indeed.proctor.webapp.model.RemoteSpecificationResult;
import com.indeed.proctor.webapp.util.threads.LogOnUncaughtExceptionHandler;
import com.indeed.util.core.DataLoadingTimerTask;
import com.indeed.util.core.Pair;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
/**
* @author parker
*/
public class RemoteProctorSpecificationSource extends DataLoadingTimerTask implements ProctorSpecificationSource {
private static final Logger LOGGER = Logger.getLogger(RemoteProctorSpecificationSource.class);
private static final ObjectMapper OBJECT_MAPPER = Serializers.strict();
@Autowired(required=false)
private ProctorClientSource clientSource = new DefaultClientSource();
private final int httpTimeout;
private final ExecutorService httpExecutor;
private volatile Map<Environment, ImmutableMap<AppVersion, RemoteSpecificationResult>> cache_ = Maps.newConcurrentMap();
public RemoteProctorSpecificationSource(int httpTimeout,
int executorThreads) {
super(RemoteProctorSpecificationSource.class.getSimpleName());
this.httpTimeout = httpTimeout;
Preconditions.checkArgument(httpTimeout > 0, "verificationTimeout > 0");
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("proctor-specification-source-Thread-%d")
.setUncaughtExceptionHandler(new LogOnUncaughtExceptionHandler())
.build();
this.httpExecutor = Executors.newFixedThreadPool(executorThreads, threadFactory);
}
@Override
public RemoteSpecificationResult getRemoteResult(final Environment environment,
final AppVersion version) {
final ImmutableMap<AppVersion, RemoteSpecificationResult> results = cache_.get(environment);
if(results != null && results.containsKey(version)) {
return results.get(version);
}
return RemoteSpecificationResult.newBuilder(version).build(Collections.<ProctorClientApplication>emptyList());
}
@Override
public Map<AppVersion, RemoteSpecificationResult> loadAllSpecifications(Environment environment) {
final ImmutableMap<AppVersion, RemoteSpecificationResult> cache = cache_.get(environment);
if(cache == null) {
return Collections.emptyMap();
} else {
return cache;
}
}
@Override
public Map<AppVersion, ProctorSpecification> loadAllSuccessfulSpecifications(Environment environment) {
final ImmutableMap<AppVersion, RemoteSpecificationResult> cache = cache_.get(environment);
if(cache == null) {
return Collections.emptyMap();
} else {
final Map<AppVersion, RemoteSpecificationResult> success = Maps.filterEntries(cache, new Predicate<Map.Entry<AppVersion, RemoteSpecificationResult>>() {
@Override
public boolean apply(@Nullable Map.Entry<AppVersion, RemoteSpecificationResult> input) {
return input.getValue().isSuccess();
}
});
return Maps.transformValues(success, new Function<RemoteSpecificationResult, ProctorSpecification>() {
@Override
public ProctorSpecification apply(@Nullable RemoteSpecificationResult input) {
return input.getSpecificationResult().getSpecification();
}
});
}
}
@Override
public Set<AppVersion> activeClients(final Environment environment, final String testName) {
final Map<AppVersion, RemoteSpecificationResult> specifications = cache_.get(environment);
if(specifications == null) {
return Collections.emptySet();
}
final Set<AppVersion> clients = Sets.newHashSet();
for(Map.Entry<AppVersion, RemoteSpecificationResult> entry : specifications.entrySet()) {
final AppVersion version = entry.getKey();
final RemoteSpecificationResult rr = entry.getValue();
if(rr.isSuccess()) {
final ProctorSpecification specification = rr.getSpecificationResult().getSpecification();
if(containsTest(specification, testName)) {
clients.add(version);
}
}
}
return clients;
}
@Override
public Set<String> activeTests(final Environment environment) {
final Map<AppVersion, RemoteSpecificationResult> specifications = cache_.get(environment);
if(specifications == null) {
return Collections.emptySet();
}
final Set<String> tests = Sets.newHashSet();
for(Map.Entry<AppVersion, RemoteSpecificationResult> entry : specifications.entrySet()) {
final RemoteSpecificationResult remoteResult = entry.getValue();
if(remoteResult.isSuccess()) {
tests.addAll(remoteResult.getSpecificationResult().getSpecification().getTests().keySet());
}
}
return tests;
}
@Override
public boolean load() {
final boolean success = refreshInternalCache();
if(success) {
setDataVersion(new Date().toString());
}
return success;
}
private boolean refreshInternalCache() {
// TODO (parker) 9/6/12 - run all of these in parallel instead of in series
boolean devSuccess = refreshInternalCache(Environment.WORKING);
boolean qaSuccess = refreshInternalCache(Environment.QA);
boolean productionSuccess = refreshInternalCache(Environment.PRODUCTION);
return devSuccess && qaSuccess && productionSuccess;
}
private boolean refreshInternalCache(Environment environment) {
LOGGER.info("Refreshing internal list of ProctorSpecifications");
final List<ProctorClientApplication> clients = clientSource.loadClients(environment);
final Map<AppVersion, Future<RemoteSpecificationResult>> futures = Maps.newLinkedHashMap();
final ImmutableMap.Builder<AppVersion, RemoteSpecificationResult> allResults = ImmutableMap.builder();
final Set<AppVersion> appVersionsToCheck = Sets.newLinkedHashSet();
final Set<AppVersion> skippedAppVersions = Sets.newLinkedHashSet();
// Accumulate all clients that have equivalent AppVersion (APPLICATION_COMPARATOR)
final ImmutableListMultimap.Builder<AppVersion, ProctorClientApplication> builder = ImmutableListMultimap.builder();
for (final ProctorClientApplication client : clients) {
final AppVersion appVersion = new AppVersion(client.getApplication(), client.getVersion());
builder.put(appVersion, client);
}
final ImmutableListMultimap<AppVersion, ProctorClientApplication> apps = builder.build();
for(final AppVersion appVersion : apps.keySet()) {
appVersionsToCheck.add(appVersion);
final List<ProctorClientApplication> callableClients = apps.get(appVersion);
assert callableClients.size() > 0;
futures.put(appVersion, httpExecutor.submit(new Callable<RemoteSpecificationResult>() {
@Override
public RemoteSpecificationResult call() throws Exception {
return internalGet(appVersion, callableClients, httpTimeout);
}
}));
}
while (!futures.isEmpty()) {
try {
Thread.sleep(10);
} catch (final InterruptedException e) {
LOGGER.error("Oh heavens", e);
}
for (final Iterator<Map.Entry<AppVersion, Future<RemoteSpecificationResult>>> iterator = futures.entrySet().iterator(); iterator.hasNext();) {
final Map.Entry<AppVersion, Future<RemoteSpecificationResult>> entry = iterator.next();
final AppVersion appVersion = entry.getKey();
final Future<RemoteSpecificationResult> future = entry.getValue();
if (future.isDone()) {
iterator.remove();
try {
final RemoteSpecificationResult result = future.get();
allResults.put(appVersion, result);
if (result.isSkipped()) {
skippedAppVersions.add(result.getVersion());
appVersionsToCheck.remove(result.getVersion());
} else if (result.isSuccess()) {
appVersionsToCheck.remove(result.getVersion());
}
} catch (final InterruptedException e) {
LOGGER.error("Interrupted getting " + appVersion, e);
} catch (final ExecutionException e) {
final Throwable cause = e.getCause();
LOGGER.error("Unable to fetch " + appVersion, cause);
}
}
}
}
synchronized (cache_) {
cache_.put(environment, allResults.build());
}
// TODO (parker) 9/6/12 - Fail if we do not have 1 specification for each <Application>.<Version>
// should we update the cache?
if(!appVersionsToCheck.isEmpty()) {
LOGGER.warn("Failed to load any specification for the following AppVersions: " + Joiner.on(",").join(appVersionsToCheck));
}
if (!skippedAppVersions.isEmpty()) {
LOGGER.info("Skipped checking specification for the following AppVersions (/private/proctor/specification returned 404): " + Joiner.on(",").join(skippedAppVersions));
}
return appVersionsToCheck.isEmpty();
}
public void shutdown() {
httpExecutor.shutdownNow();
}
private static RemoteSpecificationResult internalGet(final AppVersion version, final List<ProctorClientApplication> clients, final int timeout) {
// TODO (parker) 2/7/13 - priority queue them based on AUS datacenter, US DC, etc
final LinkedList<ProctorClientApplication> remaining = Lists.newLinkedList(clients);
final RemoteSpecificationResult.Builder results = RemoteSpecificationResult.newBuilder(version);
while(remaining.peek() != null) {
final ProctorClientApplication client = remaining.poll();
// really stupid method of pinging 1 of the applications.
final Pair<Integer, SpecificationResult> result = internalGet(client, timeout);
final int statusCode = result.getFirst();
final SpecificationResult specificationResult = result.getSecond();
if(specificationResult.getSpecification() == null) {
if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) {
LOGGER.info("Client " + client.getBaseApplicationUrl() + " /private/proctor/specification returned 404 - skipping");
results.skipped(client, specificationResult);
break;
}
// Don't yell too load, the error is handled
LOGGER.info("Failed to read specification from: " + client.getBaseApplicationUrl() + " : " + specificationResult.getError());
results.failed(client, specificationResult);
} else {
results.success(client, specificationResult);
break;
}
}
return results.build(remaining);
}
// @Nonnull
private static Pair<Integer, SpecificationResult> internalGet(final ProctorClientApplication client, final int timeout) {
URL url = null;
int statusCode = -1;
InputStream inputStream = null;
try {
url = getSpecificationUrl(client);
LOGGER.info("Trying to read specification for " + client.getApplication() + " from " + url.toString() + " using timeout " + timeout + " ms");
final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setReadTimeout(timeout);
urlConnection.setConnectTimeout(timeout);
statusCode = urlConnection.getResponseCode();
inputStream = urlConnection.getInputStream();
// map from testName => list of bucket names
final SpecificationResult result = OBJECT_MAPPER.readValue(inputStream, SpecificationResult.class);
return new Pair<Integer, SpecificationResult>(statusCode, result);
} catch (Throwable t) {
final SpecificationResult result = new SpecificationResult();
final StringWriter sw = new StringWriter();
final PrintWriter writer = new PrintWriter(sw);
t.printStackTrace(writer);
result.setError(t.getMessage());
result.setException(sw.toString());
return new Pair<Integer, SpecificationResult>(statusCode, result);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (final IOException e) {
LOGGER.error("Unable to close stream to " + url, e);
}
}
}
private static boolean containsTest(final ProctorSpecification specification, final String testName) {
return specification.getTests().containsKey(testName);
}
/**
* This needs to be moved to a separate checker class implementing some interface
*/
private static URL getSpecificationUrl(final ProctorClientApplication client) throws MalformedURLException {
final String urlStr = client.getBaseApplicationUrl() + "/private/proctor/specification";
return new URL(urlStr);
}
}