/*

Derby - Class org.apache.derbyTesting.perf.clients.Runner

Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 org.apache.derbyTesting.perf.clients;

import java.io.PrintStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;

/**
 * Class used for running a performance test from the command line. To learn
 * how to run the tests, invoke this command:
 * <pre>
 * java org.apache.derbyTesting.perf.clients.Runner
 * </pre>
 */
public class Runner {

    private static final String DERBY_EMBEDDED_DRIVER =
            "org.apache.derby.jdbc.EmbeddedDriver";

    private static final String DEFAULT_URL = "jdbc:derby:db;create=true";

    /** The JDBC driver class to use in the test. */
    private static String driver = DERBY_EMBEDDED_DRIVER;
    /** The JDBC connection URL to use in the test. */
    private static String url = DEFAULT_URL;
    /** Username for connecting to the database. */
    private static String user = "test";
    /** Password for connecting to the database. */
    private static String password = "test";
    /**
     * Flag which tells whether the data needed by this test should be
     * (re)created.
     */
    private static boolean init = false;
    /** The name of the type of load to use in the test. */
    private static String load; // required argument
    /** Map containing load-specific options. */
    private final static HashMap<String, String> loadOpts =
            new HashMap<String, String>();
    /** The name of the load generator to use in the test. */
    private static String generator = "b2b";
    /** The number of client threads to use in the test. */
    private static int threads = 1;
    /**
     * The number of requests to issue to the database per second (for the
     * load generators that take that as an argument).
     */
    private static int requestsPerSecond = 100;
    /** The number of seconds to spend in the warmup phase. */
    private static int warmupSec = 30;
    /** The number of seconds to collect results. */
    private static int steadySec = 60;

    /**
     * Main method which starts the Runner application.
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        try {
            parseArgs(args);
        } catch (Exception e) {
            System.err.println(e);
            printUsage(System.err);
            System.exit(1);
        }

        Class<?> clazz = Class.forName(driver);
        clazz.getConstructor().newInstance();

        if (init) {
            DBFiller filler = getDBFiller();
            Connection conn = DriverManager.getConnection(url, user, password);
            System.out.println("initializing database...");
            filler.fill(conn);
            conn.close();
        }

        Client[] clients = new Client[threads];
        for (int i = 0; i < clients.length; i++) {
            Connection c = DriverManager.getConnection(url, user, password);
            c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            clients[i] = newClient();
            clients[i].init(c);
        }

        LoadGenerator gen = getLoadGenerator();
        gen.init(clients);
        System.out.println("starting warmup...");
        gen.startWarmup();
        Thread.sleep(1000L * warmupSec);
        System.out.println("entering steady state...");
        gen.startSteadyState();
        Thread.sleep(1000L * steadySec);
        System.out.println("stopping threads...");
        gen.stop();
        gen.printReport(System.out);

        shutdownDatabase();
    }

    /**
     * Parse the command line arguments and set the state variables to
     * reflect the arguments.
     *
     * @param args the command line arguments
     */
    private static void parseArgs(String[] args) throws Exception {
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-driver")) {
                driver = args[++i];
            } else if (args[i].equals("-url")) {
                url = args[++i];
            } else if (args[i].equals("-user")) {
                user = args[++i];
            } else if (args[i].equals("-pass")) {
                password = args[++i];
            } else if (args[i].equals("-init")) {
                init = true;
            } else if (args[i].equals("-load")) {
                load = args[++i];
            } else if (args[i].equals("-load_opts")) {
                parseLoadOpts(args[++i]);
            } else if (args[i].equals("-gen")) {
                generator = args[++i];
            } else if (args[i].equals("-threads")) {
                threads = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-rate")) {
                requestsPerSecond = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-wt")) {
                warmupSec = Integer.parseInt(args[++i]);
            } else if (args[i].equals("-rt")) {
                steadySec = Integer.parseInt(args[++i]);
            } else {
                throw new Exception("invalid argument: " + args[i]);
            }
        }
        if (load == null) {
            throw new Exception("required parameter -load not specified");
        }
    }

    /**
     * Parse the load-specific options. It's a comma-separated list of options,
     * where each option is either a keyword or a (keyword, value) pair
     * separated by an equals sign (=). The parsed options will be put into the
     * map {@link #loadOpts}.
     *
     * @param optsString the comma-separated list of options
     */
    private static void parseLoadOpts(String optsString) {
        String[] opts = optsString.split(",");
        for (int i = 0; i < opts.length; i++) {
            String[] keyValue = opts[i].split("=", 2);
            if (keyValue.length == 2) {
                loadOpts.put(keyValue[0], keyValue[1]);
            } else {
                loadOpts.put(opts[i], null);
            }
        }
    }

    /**
     * Checks whether the specified option is set.
     *
     * @param option the name of the option
     * @return {@code true} if the option is set
     */
    private static boolean hasOption(String option) {
        return loadOpts.keySet().contains(option);
    }

    /**
     * Get the {@code int} value of the specified option.
     *
     * @param option the name of the option
     * @param defaultValue the value to return if the option is not set
     * @return the value of the option
     * @throws NumberFormatException if the value is not an {@code int}
     */
    static int getLoadOpt(String option, int defaultValue) {
        String val = (String) loadOpts.get(option);
        return val == null ? defaultValue : Integer.parseInt(val);
    }

    /** String to print when there are errors in the command line arguments. */
    private static final String USAGE =

"Valid parameters:\n" +
"  -driver: JDBC driver class, default: " + DERBY_EMBEDDED_DRIVER + "\n" +
"  -url: JDBC connection url, default: " +  DEFAULT_URL + "\n" +
"  -user: JDBC user name, default: test\n" +
"  -pass: JDBC user password, default: test\n" +
"  -init: initialize database (otherwise, reuse database)\n" +
"  -load: type of load, required argument, valid types:\n" +
"      * sr_select - single-record (primary key) select from table with\n" +
"                    100 000 rows. It accepts the following load-specific\n" +
"                    options (see also -load_opts):\n" +
"            - blob or clob: use BLOB or CLOB data instead of VARCHAR\n" +
"            - secondary: select on a column with a secondary (non-unique)\n" +
"              index instead of the primary key\n" +
"            - nonIndexed: select on a non-indexed column instead of the\n" +
"              primary key\n" +
"      * sr_update - single-record (primary key) update on table with\n" +
"                    100 000 rows. It accepts the same load-specific\n" +
"                    options as sr_select.\n" +
"      * sr_select_big - single-record (primary key) select from table with\n" +
"                    100 000 000 rows\n" +
"      * sr_update_big - single-record (primary key) update on table with\n" +
"                    100 000 000 rows\n" +
"      * sr_select_multi - single-record select from a random table\n" +
"                    (32 tables with a single row each)\n" +
"      * sr_update_multi - single-record update on a random table\n" +
"                    (32 tables with a single row each)\n" +
"      * index_join - join of two tables (using indexed columns)\n" +
"      * group_by - GROUP BY queries against TENKTUP1\n" +
"      * bank_tx - emulate simple bank transactions, similar to TPC-B. The\n" +
"                  following load-specific options are accepted:\n" +
"            - branches=NN: specifies the number of branches in the db\n" +
"                           (default: 1)\n" +
"            - tellersPerBranch=NN: specifies how many tellers each branch\n" +
"              in the database has (default: 10)\n" +
"            - accountsPerBranch=NN: specifies the number of accounts in\n" +
"              each branch (default: 100000)\n" +
"      * seq_gen - sequence generator concurrency. Accepts\n" +
"                    the following load-specific options (see also -load_opts):\n" +
"            - numberOfGenerators: number of sequences to create\n" +
"            - tablesPerGenerator: number of tables to create per sequence\n" +
"            - insertsPerTransaction: number of inserts to perform per transaction\n" +
"            - debugging: 1 means print debug chatter, 0 means do not print the chatter\n" +
"            - identityTest: 1 means do identity column testing, any other number \n" +
"                    means do sequence generator testing. If no identityTest is specified \n" +
"                    then sequence generator testing will be done by default \n" +
"  -load_opts: comma-separated list of load-specific options\n" +
"  -gen: load generator, default: b2b, valid types:\n" +
"      * b2b - clients perform operations back-to-back\n" +
"      * poisson - load is Poisson distributed\n" +
"  -threads: number of threads performing operations, default: 1\n" +
"  -rate: average number of transactions per second to inject when\n" +
"         load generator is \"poisson\", default: 100\n" +
"  -wt: warmup time in seconds, default: 30\n" +
"  -rt: time in seconds to collect results, default: 60";
    /**
     * Print the usage string.
     *
     * @param out the stream to print the usage string to
     */
    private static void printUsage(PrintStream out) {
        out.println(USAGE);
    }

    /**
     * Get the data type to be used for sr_select and sr_update types of load.
     *
     * @return one of the {@code java.sql.Types} data type constants
     */
    private static int getTextType() {
        boolean blob = hasOption("blob");
        boolean clob = hasOption("clob");
        if (blob && clob) {
            System.err.println("Cannot specify both 'blob' and 'clob'");
            printUsage(System.err);
            System.exit(1);
        }
        if (blob) {
            return Types.BLOB;
        }
        if (clob) {
            return Types.CLOB;
        }
        return Types.VARCHAR;
    }

    /**
     * Find the {@code DBFiller} instance for the load specified on the
     * command line.
     *
     * @return a {@code DBFiller} instance
     */
    private static DBFiller getDBFiller() {
        if (load.equals("sr_select") || load.equals("sr_update")) {
            return new SingleRecordFiller(100000, 1, getTextType(),
                                          hasOption("secondary"),
                                          hasOption("nonIndexed"));
        } else if (load.equals("sr_select_big") ||
                       load.equals("sr_update_big")) {
            return new SingleRecordFiller(100000000, 1);
        } else if (load.equals("sr_select_multi") ||
                       load.equals("sr_update_multi")) {
            return new SingleRecordFiller(1, 32);
        } else if (load.equals("index_join")) {
            return new WisconsinFiller();
        } else if (load.equals("group_by")) {
            return new WisconsinFiller(getLoadOpt("numRows", 10000));
        } else if (load.equals("bank_tx")) {
            return new BankAccountFiller(
                getLoadOpt("branches", 1),
                getLoadOpt("tellersPerBranch", 10),
                getLoadOpt("accountsPerBranch", 100000));
        } else if (load.equals("seq_gen")) {
            return new SequenceGeneratorConcurrency.Filler();
        }
        System.err.println("unknown load: " + load);
        printUsage(System.err);
        System.exit(1);
        return null;
    }

    /**
     * Create a new client for the load specified on the command line.
     *
     * @return a {@code Client} instance
     */
    private static Client newClient() {
        if (load.equals("sr_select")) {
            return new SingleRecordSelectClient(100000, 1, getTextType(),
                    hasOption("secondary"), hasOption("nonIndexed"));
        } else if (load.equals("sr_update")) {
            return new SingleRecordUpdateClient(100000, 1, getTextType(),
                    hasOption("secondary"), hasOption("nonIndexed"));
        } else if (load.equals("sr_select_big")) {
            return new SingleRecordSelectClient(100000000, 1);
        } else if (load.equals("sr_update_big")) {
            return new SingleRecordUpdateClient(100000000, 1);
        } else if (load.equals("sr_select_multi")) {
            return new SingleRecordSelectClient(1, 32);
        } else if (load.equals("sr_update_multi")) {
            return new SingleRecordUpdateClient(1, 32);
        } else if (load.equals("index_join")) {
            return new IndexJoinClient();
        } else if (load.equals("group_by")) {
            return new GroupByClient();
        } else if (load.equals("bank_tx")) {
            return new BankTransactionClient(
                getLoadOpt("branches", 1),
                getLoadOpt("tellersPerBranch", 10),
                getLoadOpt("accountsPerBranch", 100000));
        } else if (load.equals("seq_gen")) {
            return new SequenceGeneratorConcurrency.SGClient();
        }
        System.err.println("unknown load: " + load);
        printUsage(System.err);
        System.exit(1);
        return null;
    }

    /**
     * Create a load generator for the load specified on the command line.
     *
     * @return a {@code LoadGenerator} instance
     */
    private static LoadGenerator getLoadGenerator() {
        if (generator.equals("b2b")) {
            return new BackToBackLoadGenerator();
        } else if (generator.equals("poisson")) {
            double avgWaitTime = 1000d * threads / requestsPerSecond;
            return new PoissonLoadGenerator(avgWaitTime);
        }
        System.err.println("unknown load generator: " + generator);
        printUsage(System.err);
        System.exit(1);
        return null;
    }

    /**
     * Shut down the database if it is a Derby embedded database.
     */
    private static void shutdownDatabase() throws SQLException {
        if (driver.equals(DERBY_EMBEDDED_DRIVER)) {
            try {
                DriverManager.getConnection(url + ";shutdown=true");
                System.err.println("WARNING: Shutdown of database didn't " +
                                   "throw expected exception");
            } catch (SQLException e) {
                if (!"08006".equals(e.getSQLState())) {
                    System.err.println("WARNING: Shutdown of database threw " +
                                       "unexpected exception");
                    e.printStackTrace();
                }
            }
        }
    }
}
