com.mgmtp.perfload.core.console.LtConsole.java Source code

Java tutorial

Introduction

Here is the source code for com.mgmtp.perfload.core.console.LtConsole.java

Source

/*
 * Copyright (c) 2002-2014 mgm technology partners GmbH
 *
 * 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.mgmtp.perfload.core.console;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList;
import com.mgmtp.perfload.core.clientserver.client.Client;
import com.mgmtp.perfload.core.clientserver.client.ClientMessageListener;
import com.mgmtp.perfload.core.clientserver.client.DefaultClient;
import com.mgmtp.perfload.core.common.clientserver.Payload;
import com.mgmtp.perfload.core.common.clientserver.PayloadType;
import com.mgmtp.perfload.core.common.config.ProcessConfig;
import com.mgmtp.perfload.core.common.config.ProcessKey;
import com.mgmtp.perfload.core.common.config.TestConfig;
import com.mgmtp.perfload.core.common.config.TestJar;
import com.mgmtp.perfload.core.common.config.TestplanConfig;
import com.mgmtp.perfload.core.common.config.XmlConfigReader;
import com.mgmtp.perfload.core.common.util.MemoryInfo;
import com.mgmtp.perfload.core.common.util.MemoryInfo.Unit;
import com.mgmtp.perfload.core.common.util.StatusInfo;
import com.mgmtp.perfload.core.console.meta.LtMetaInfo;
import com.mgmtp.perfload.core.console.meta.LtMetaInfoHandler;
import com.mgmtp.perfload.core.console.model.Daemon;
import com.mgmtp.perfload.core.console.status.FileStatusTransformer;
import com.mgmtp.perfload.core.console.status.StatusHandler;
import com.mgmtp.perfload.core.console.status.StatusTransformer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.PrintWriter;
import java.io.Serializable;
import java.net.ConnectException;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static com.google.common.collect.Maps.newHashMapWithExpectedSize;
import static org.apache.commons.io.IOUtils.closeQuietly;

/**
 * Represents perfLoad's management console. This class is responsible for running and controlling
 * load tests.
 * 
 * @author rnaegele
 */
public final class LtConsole {

    private static final Logger LOG = LoggerFactory.getLogger(LtConsole.class);

    private final Map<Integer, Client> clients = newHashMapWithExpectedSize(4);

    private volatile CountDownLatch connectLatch;
    private volatile CountDownLatch jarLatch;
    private volatile CountDownLatch readyLatch;
    private volatile CountDownLatch doneLatch;
    private volatile CountDownLatch clientCountLatch;

    private final TestplanConfig config;
    private final ExecutorService execService;
    private final StatusHandler statusHandler = new StatusHandler();
    private final StatusTransformer statusTransformer;
    private final LtMetaInfoHandler metaInfoHandler;
    private final List<Daemon> daemons;
    private final boolean shutdownDaemons;
    private final boolean abortTest;
    private final long loadProfileTestTimeout;

    private volatile int clientCount;
    private volatile boolean testSuccessful = true;

    private ZonedDateTime startTimestamp;
    private ZonedDateTime finishTimestamp;

    /**
     * Creates a new instance.
     * 
     * @param config
     *            the test configuration
     * @param execService
     *            {@link ExecutorService} handling asynchronous tasks such as status queue polling
     * @param metaInfoHandler
     *            creates and writes out meta information about the test for reporting use
     * @param daemons
     *            daemon hosts/ports for the test
     * @param shutdownDaemons
     *            if {@code true}, daemons are shut down after the test
     * @param loadProfileTestTimeout
     *            timeout in milliseconds for aborting a load profile test, calculated by adding
     *            this value to the start time of the last load profile event (zero for no timeout)
     */
    public LtConsole(final TestplanConfig config, final ExecutorService execService,
            final StatusTransformer statusTransformer, final LtMetaInfoHandler metaInfoHandler,
            final List<Daemon> daemons, final boolean shutdownDaemons, final boolean abortTest,
            final long loadProfileTestTimeout) {
        this.config = config;
        this.execService = execService;
        this.statusTransformer = statusTransformer;
        this.metaInfoHandler = metaInfoHandler;
        this.daemons = daemons;
        this.shutdownDaemons = shutdownDaemons;
        this.abortTest = abortTest;
        this.loadProfileTestTimeout = loadProfileTestTimeout;

        int totalProcessCount = config.getTotalProcessCount();
        connectLatch = new CountDownLatch(totalProcessCount);
        jarLatch = new CountDownLatch(daemons.size() * config.getTestJars().size());
        readyLatch = new CountDownLatch(totalProcessCount);
        doneLatch = new CountDownLatch(totalProcessCount);

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                if (doneLatch != null && doneLatch.getCount() > 0) {
                    LOG.warn("Aborting test...");
                    abortTest();
                    try {
                        while (connectLatch.getCount() > 0) {
                            // Need to count down connectLatch because test terminated abnormally
                            connectLatch.countDown();
                        }
                        while (readyLatch.getCount() > 0) {
                            // Need to count down readyLatch because test terminated abnormally
                            readyLatch.countDown();
                        }
                        doneLatch.countDown();
                    } catch (Exception ex) {
                        LOG.error(ex.getMessage(), ex);
                    }
                    for (Client client : clients.values()) {
                        LOG.info("Disconnecting from daemon {}", client.getClientId());
                        client.disconnect();
                    }
                    LOG.info("Done.");
                }
            }
        });
    }

    /**
     * Executes the load test carrying out the following steps:
     * <ol>
     * <li>load test configuration</li>
     * <li>set up daemon clients</li>
     * <li>connect to daemons</li>
     * <li>send test jars to daemons</li>
     * <li>create test processes</li>
     * <li>send configuration to test processes</li>
     * <li>run test</li>
     * <li>disconnect from daemons</li>
     * </ol>
     */
    public void execute() throws Exception {
        LOG.info("Console started.");

        setUpDaemonClients();
        connectToDaemons();

        if (abortTest) {
            abortTest();
            while (clientCount != clients.size()) {
                LOG.info("Waiting for test to be aborted...");
                fetchClientCount();
                Thread.sleep(2000L);
            }
            while (doneLatch.getCount() > 0) {
                doneLatch.countDown();
            }
            LOG.info("Test aborted!");
        } else {
            sendJars();
            createTestProcesses();
            sendConfiguration();
            runTest();
            runStatusTransformer();

            if (loadProfileTestTimeout > 0) {
                addAbortionTimeoutWatcher();
            }
        }

        disconnectFromDaemons();
        if (!abortTest && metaInfoHandler != null) {
            LOG.info("Creating meta information...");
            LtMetaInfo metaInfo = metaInfoHandler.createMetaInformation(startTimestamp, finishTimestamp, config,
                    daemons);
            PrintWriter pr = null;
            try {
                File metaFile = new File("perfload.meta.utf8.props");
                LOG.info("Dumping meta information to '{}'...", metaFile);
                pr = new PrintWriter(metaFile, "UTF-8");
                metaInfoHandler.dumpMetaInfo(metaInfo, pr);
            } finally {
                closeQuietly(pr);
            }
        }

        LOG.info("Exiting...");
    }

    private void setUpDaemonClients() {
        LOG.info("Setting up daemon clients.");

        ClientMessageListener listener = new ClientMessageListener() {
            @Override
            public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) {
                Payload payload = (Payload) e.getMessage();
                LOG.debug("Received payload: " + payload);

                switch (payload.getPayloadType()) {
                case ERROR:
                    LOG.warn("Aborting test...");
                    testSuccessful = false;
                    abortTest();
                    break;
                case JAR:
                    jarLatch.countDown();
                    break;
                case TEST_PROC_CONNECTED:
                    connectLatch.countDown();
                    break;
                case TEST_PROC_DISCONNECTED:
                    LOG.info("Client process disconnected: {}", payload.<Serializable>getContent());
                    break;
                case TEST_PROC_READY:
                    readyLatch.countDown();
                    break;
                case TEST_PROC_STARTED: {
                    ProcessConfig procConf = payload.getContent();
                    LOG.info("Test process created: {}", procConf);
                    break;
                }
                case TEST_PROC_TERMINATED: {
                    ProcessConfig procConf = payload.getContent();
                    LOG.info("Test process terminated: {}", procConf);
                    while (connectLatch.getCount() > 0) {
                        // Need to count down connectLatch because test terminated abnormally
                        connectLatch.countDown();
                    }
                    while (readyLatch.getCount() > 0) {
                        // Need to count down readyLatch because test terminated abnormally
                        readyLatch.countDown();
                    }
                    doneLatch.countDown();
                    break;
                }
                case CLIENT_COUNT:
                    int count = payload.getContent();
                    clientCount += count;
                    clientCountLatch.countDown();
                    break;
                case STATUS:
                    StatusInfo si = payload.getContent();
                    LOG.debug("Received status info: {}", si);
                    statusHandler.addStatusInfo(si);
                    break;
                default:
                    //
                }

                LOG.debug(MemoryInfo.getMemoryInfo(Unit.KIBIBYTES));
            }
        };

        for (Daemon daemon : daemons) {
            int daemonId = daemon.getId();
            String clientId = "console" + (abortTest ? "Aborter" : "") + daemonId;
            Client client = new DefaultClient(clientId, daemon.getHost(), daemon.getPort());
            client.addClientMessageListener(listener);
            clients.put(daemonId, client);
        }
    }

    private void connectToDaemons() throws ConnectException {
        LOG.info("Connecting to daemons...");

        boolean connected = true;
        for (Client client : clients.values()) {
            LOG.info("Connecting to daemon {}", client.getClientId());
            client.connect();
            if (!client.isConnected()) {
                connected = false;
                LOG.error("Could not connect to daemon: {}", client.getClientId());
            }
        }

        if (!connected) {
            // set to null so the shutdown hook will not try to abort and disconnect
            doneLatch = null;
            throw new ConnectException("Could not connect to at least one daemon.");
        }
    }

    private void sendJars() throws InterruptedException, TimeoutException {
        List<TestJar> testJars = config.getTestJars();
        if (testJars.isEmpty()) {
            while (jarLatch.getCount() > 0) {
                jarLatch.countDown();
            }
        }

        for (Daemon daemon : daemons) {
            int daemonId = daemon.getId();
            LOG.info("Transferring jars to daemon {}", daemonId);
            Client client = clients.get(daemonId);

            // Send jars separately in order to save memory
            for (TestJar jar : testJars) {
                LOG.debug("Transferring jar file: {}", jar.getName());
                // Await sending of the message. Otherwise jars might pile up in memory causing an OOME.
                if (!client.sendMessage(new Payload(PayloadType.JAR, jar)).await(30L, TimeUnit.SECONDS)) {
                    throw new TimeoutException("Timeout waiting for jar to be sent.");
                }
            }
        }
    }

    private void createTestProcesses() throws InterruptedException, TimeoutException {
        awaitLatch(jarLatch, "Timeout waiting for jars to be transferred to daemons.");

        LOG.info("Starting test processes...");
        for (ProcessConfig pc : config.getProcessConfigs()) {
            LOG.debug("Starting test process: {}", pc);
            clients.get(pc.getDaemonId()).sendMessage(new Payload(PayloadType.CREATE_TEST_PROC, pc));
        }
    }

    private void sendConfiguration() throws InterruptedException, TimeoutException {
        awaitLatch(connectLatch, "Timeout waiting for configuration to be sent to clients.");

        LOG.info("Transferring test configuration");

        for (Daemon daemon : daemons) {
            int daemonId = daemon.getId();
            // Send test config
            LOG.debug("Transferring test configuration to daemon {}", daemonId);
            Client client = clients.get(daemonId);

            for (Entry<ProcessKey, TestConfig> entry : config.getTestConfigs().entrySet()) {
                if (entry.getKey().getDaemonId() == daemonId) {
                    client.sendMessage(new Payload(PayloadType.CONFIG, entry.getValue()));
                }
            }
        }
    }

    private void runTest() throws InterruptedException, TimeoutException {
        awaitLatch(readyLatch, "Timeout waiting until test is ready to be started.");

        startTimestamp = ZonedDateTime.now();
        LOG.info("Running test...");

        Payload payload = new Payload(PayloadType.START);
        for (Client client : clients.values()) {
            LOG.debug("Sending START signal to daemon {}", client.getClientId());
            client.sendMessage(payload);
        }
    }

    private void runStatusTransformer() {
        if (statusTransformer != null) {
            execService.submit(new Runnable() {
                @Override
                public void run() {
                    while (!Thread.currentThread().isInterrupted()) {
                        try {
                            statusTransformer.execute(statusHandler.getStatusInfoMap(),
                                    statusHandler.getThreadActivitiesMap());
                            Thread.sleep(1000L);
                        } catch (InterruptedException ex) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            });
        }
    }

    private void disconnectFromDaemons() throws InterruptedException {
        doneLatch.await();
        finishTimestamp = ZonedDateTime.now();

        LOG.info("Waiting for status polling to complete...");

        Thread.sleep(5000L);
        execService.shutdownNow();

        LOG.info("Disconnecting from daemons...");
        for (Client client : clients.values()) {
            LOG.info("Disconnecting from daemon {}", client.getClientId());
            client.sendMessage(new Payload(PayloadType.CONSOLE_DISCONNECTING));

            if (shutdownDaemons) {
                LOG.info("Shutting down daemon {}", client.getClientId());
                client.sendMessage(new Payload(PayloadType.SHUTDOWN_DAEMON));
            }

            client.disconnect();
        }
    }

    private void abortTest() {
        LOG.info("Aborting test...");

        Payload payload = new Payload(PayloadType.ABORT);
        for (Client client : clients.values()) {
            LOG.debug("Sending ABORT signal to daemon {}", client.getClientId());
            client.sendMessage(payload);
        }
    }

    private void fetchClientCount() throws InterruptedException {
        clientCountLatch = new CountDownLatch(clients.size());
        clientCount = 0;

        for (Client client : clients.values()) {
            client.sendMessage(new Payload(PayloadType.CLIENT_COUNT));
        }
        clientCountLatch.await(1L, TimeUnit.MINUTES);
    }

    private void awaitLatch(final CountDownLatch latch, final String message)
            throws InterruptedException, TimeoutException {
        if (!latch.await(2L, TimeUnit.MINUTES)) {
            throw new TimeoutException(message);
        }
    }

    public boolean isTestSuccessful() {
        return testSuccessful;
    }

    private void addAbortionTimeoutWatcher() {
        Runnable abortionWatcher = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(config.getStartTimeOfLastEvent() + loadProfileTestTimeout);
                    if (doneLatch.getCount() > 0) { // if zero, test is already done
                        LOG.warn("Load profile test timed out!");
                        abortTest();
                    }
                } catch (InterruptedException e) {
                    // this happens when the sleep is interrupted and an abortion is not necessary
                }
            }
        };
        execService.submit(abortionWatcher);
    }

    public static void main(final String[] args) {
        JCommander jCmd = null;
        try {
            LtConsoleArgs cliArgs = new LtConsoleArgs();
            jCmd = new JCommander(cliArgs);
            jCmd.parse(args);

            LOG.info("Starting perfLoad Console...");
            LOG.info(cliArgs.toString());

            int daemonCount = cliArgs.daemons.size();
            List<Daemon> daemons = newArrayListWithCapacity(daemonCount);
            for (int i = 0; i < daemonCount; i++) {
                // daemon id is 1-based
                int daemonId = i + 1;
                daemons.add(Daemon.fromHostAndPort(daemonId, cliArgs.daemons.get(i)));
            }
            daemons = ImmutableList.copyOf(daemons);

            XmlConfigReader configReader = new XmlConfigReader(new File("."), cliArgs.testplan);
            TestplanConfig config = configReader.readConfig();
            int totalThreadCount = config.getTotalThreadCount();
            StatusTransformer transformer = new FileStatusTransformer(totalThreadCount, new File("ltStatus.txt"),
                    new File("ltThreads.txt"), "UTF-8");

            LtMetaInfoHandler metaInfoHandler = new LtMetaInfoHandler();
            LtConsole console = new LtConsole(config, Executors.newCachedThreadPool(), transformer, metaInfoHandler,
                    daemons, cliArgs.shutdownDaemons, cliArgs.abort, TimeUnit.MINUTES.toMillis(cliArgs.timeout));
            console.execute();
        } catch (ParameterException ex) {
            jCmd.usage();
            System.exit(1);
        } catch (Exception ex) {
            LOG.error(ex.getMessage(), ex);
            System.exit(-1);
        }
    }
}