// Copyright 2016 Google Inc. // // 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.pubsub.flic.controllers; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.pubsub.flic.common.LatencyDistribution; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Collectors; import java.util.stream.LongStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Each subclass of Controller is responsible for instantiating and cleaning up a given environment. * When an environment is started, it adds {@link Client} objects to the clients array, which is * used to start the load test and collect results. This base class manages every * environment-agnostic part of this process. */ public abstract class Controller { protected static final Logger log = LoggerFactory.getLogger(Controller.class); public static String resourceDirectory = "target/classes/gce"; protected final List<Client> clients = new ArrayList<>(); protected final ScheduledExecutorService executor; /** * Creates the given environments and starts the virtual machines. When this function returns, * each client is guaranteed to have been connected and be network reachable, but is not started. * If an error occurred attempting to start the environment, the environment will be shut down, * and an Exception will be thrown. It is not guaranteed that we have completed shutting down when * this function returns, but it is guaranteed that we are in process. * * @param executor the executor that will be used to schedule all environment initialization tasks */ public Controller(ScheduledExecutorService executor) { this.executor = executor; } /** * Shuts down the given environment. When this function returns, each client is guaranteed to be * in process of being deleted, or else output directions on how to manually delete any potential * remaining instances if unable. * * @param t the error that caused the shutdown, or null if shutting down successfully */ public abstract void shutdown(Throwable t); /** * @return the types map */ public abstract Map<String, Map<ClientParams, Integer>> getTypes(); /** * Waits for clients to complete the load test. */ public void waitForClients() throws Throwable { try { Futures.allAsList(clients.stream() .map(Client::getDoneFuture) .collect(Collectors.toList()) ).get(); } catch (ExecutionException e) { throw e.getCause(); } } /** * Waits for publishers to complete the load test. */ public void waitForPublisherClients() throws Throwable { try { Futures.allAsList(clients.stream() .filter(c -> c.getClientType().isPublisher()) .map(Client::getDoneFuture) .collect(Collectors.toList()) ).get(); } catch (ExecutionException e) { throw e.getCause(); } } /** * Gets the current statistics for the given type. * * @param type the client type to aggregate results for * @return the results from the load test up to this point */ private LoadtestStats getStatsForClientType(Client.ClientType type) { LoadtestStats stats = new LoadtestStats(); List<Client> clientsOfType = clients.stream() .filter(c -> c.getClientType() == type).collect(Collectors.toList()); stats.runningSeconds = clientsOfType.stream().mapToLong(Client::getRunningSeconds).max().getAsLong() - Client.burnInDuration.getSeconds(); clientsOfType.stream().map(Client::getBucketValues).forEach(bucketValues -> { for (int i = 0; i < LatencyDistribution.LATENCY_BUCKETS.length; i++) { stats.bucketValues[i] += bucketValues[i]; } }); return stats; } /** * Gets the results for all available types. * * @return the map from type to result, every type running is a valid key */ public Map<Client.ClientType, LoadtestStats> getStatsForAllClientTypes() { final Map<Client.ClientType, LoadtestStats> results = new HashMap<>(); List<ListenableFuture<Void>> resultFutures = new ArrayList<>(); getTypes().values().stream() .map(Map::keySet).flatMap(Set::stream) .map(ClientParams::getClientType).distinct() .forEach(type -> { SettableFuture<Void> resultFuture = SettableFuture.create(); resultFutures.add(resultFuture); executor.submit(() -> { try { results.put(type, getStatsForClientType(type)); resultFuture.set(null); } catch (Throwable t) { resultFuture.setException(t); } }); }); try { Futures.allAsList(resultFutures).get(); } catch (ExecutionException | InterruptedException e) { log.error("Failed health check, will return results accumulated during test up to now.", e instanceof ExecutionException ? e.getCause() : e); } return results; } /** * Sends a LoadtestFramework.Start RPC to all clients to commence the load test. When this * function returns it is guaranteed that all clients have started. */ public void startClients(MessageTracker messageTracker) { SettableFuture<Void> startFuture = SettableFuture.create(); clients.forEach((client) -> executor.execute(() -> { try { client.start(messageTracker); startFuture.set(null); } catch (Throwable t) { startFuture.setException(t); } })); try { startFuture.get(); } catch (ExecutionException e) { shutdown(e.getCause()); } catch (InterruptedException e) { shutdown(e); } } /** The statistics that are exported by each load test client. */ public static class LoadtestStats { public long runningSeconds; public long[] bucketValues = new long[LatencyDistribution.LATENCY_BUCKETS.length]; /** Returns the average QPS. */ public double getQPS() { return (double) LongStream.of(bucketValues).sum() / (double) runningSeconds; } /** Returns the average throughput in MB/s. */ public double getThroughput() { return getQPS() * Client.messageSize / 1000000.0; } } }