org.apache.hadoop.hbase.util.LoadTestTool.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.util.LoadTestTool.java

Source

/*
 * 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.hadoop.hbase.util;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.reflect.Constructor;
import java.net.InetAddress;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;

import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.PerformanceEvaluation;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.io.compress.Compression;
import org.apache.hadoop.hbase.io.crypto.Cipher;
import org.apache.hadoop.hbase.io.crypto.Encryption;
import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos;
import org.apache.hadoop.hbase.regionserver.BloomType;
import org.apache.hadoop.hbase.security.EncryptionUtil;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.access.AccessControlClient;
import org.apache.hadoop.hbase.util.test.LoadTestDataGenerator;
import org.apache.hadoop.hbase.util.test.LoadTestDataGeneratorWithACL;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.util.ToolRunner;

/**
 * A command-line utility that reads, writes, and verifies data. Unlike
 * {@link PerformanceEvaluation}, this tool validates the data written,
 * and supports simultaneously writing and reading the same set of keys.
 */
public class LoadTestTool extends AbstractHBaseTool {

    private static final Log LOG = LogFactory.getLog(LoadTestTool.class);
    private static final String COLON = ":";

    /** Table name for the test */
    private TableName tableName;

    /** Table name to use of not overridden on the command line */
    protected static final String DEFAULT_TABLE_NAME = "cluster_test";

    /** Column family used by the test */
    public static byte[] COLUMN_FAMILY = Bytes.toBytes("test_cf");

    /** Column families used by the test */
    protected static final byte[][] COLUMN_FAMILIES = { COLUMN_FAMILY };

    /** The default data size if not specified */
    protected static final int DEFAULT_DATA_SIZE = 64;

    /** The number of reader/writer threads if not specified */
    protected static final int DEFAULT_NUM_THREADS = 20;

    /** Usage string for the load option */
    protected static final String OPT_USAGE_LOAD = "<avg_cols_per_key>:<avg_data_size>" + "[:<#threads="
            + DEFAULT_NUM_THREADS + ">]";

    /** Usage string for the read option */
    protected static final String OPT_USAGE_READ = "<verify_percent>[:<#threads=" + DEFAULT_NUM_THREADS + ">]";

    /** Usage string for the update option */
    protected static final String OPT_USAGE_UPDATE = "<update_percent>[:<#threads=" + DEFAULT_NUM_THREADS
            + ">][:<#whether to ignore nonce collisions=0>]";

    protected static final String OPT_USAGE_BLOOM = "Bloom filter type, one of "
            + Arrays.toString(BloomType.values());

    protected static final String OPT_USAGE_COMPRESSION = "Compression type, " + "one of "
            + Arrays.toString(Compression.Algorithm.values());

    public static final String OPT_DATA_BLOCK_ENCODING_USAGE = "Encoding algorithm (e.g. prefix "
            + "compression) to use for data blocks in the test column family, " + "one of "
            + Arrays.toString(DataBlockEncoding.values()) + ".";

    private static final String OPT_BLOOM = "bloom";
    private static final String OPT_COMPRESSION = "compression";
    public static final String OPT_DATA_BLOCK_ENCODING = HColumnDescriptor.DATA_BLOCK_ENCODING.toLowerCase();

    public static final String OPT_INMEMORY = "in_memory";
    public static final String OPT_USAGE_IN_MEMORY = "Tries to keep the HFiles of the CF "
            + "inmemory as far as possible.  Not guaranteed that reads are always served from inmemory";

    public static final String OPT_GENERATOR = "generator";
    public static final String OPT_GENERATOR_USAGE = "The class which generates load for the tool."
            + " Any args for this class can be passed as colon separated after class name";

    protected static final String OPT_KEY_WINDOW = "key_window";
    protected static final String OPT_WRITE = "write";
    protected static final String OPT_MAX_READ_ERRORS = "max_read_errors";
    protected static final String OPT_MULTIPUT = "multiput";
    protected static final String OPT_NUM_KEYS = "num_keys";
    protected static final String OPT_READ = "read";
    protected static final String OPT_START_KEY = "start_key";
    protected static final String OPT_TABLE_NAME = "tn";
    protected static final String OPT_ZK_QUORUM = "zk";
    protected static final String OPT_ZK_PARENT_NODE = "zk_root";
    protected static final String OPT_SKIP_INIT = "skip_init";
    protected static final String OPT_INIT_ONLY = "init_only";
    protected static final String NUM_TABLES = "num_tables";
    protected static final String OPT_REGIONS_PER_SERVER = "regions_per_server";
    protected static final String OPT_BATCHUPDATE = "batchupdate";
    protected static final String OPT_UPDATE = "update";

    protected static final String OPT_ENCRYPTION = "encryption";
    protected static final String OPT_ENCRYPTION_USAGE = "Enables transparent encryption on the test table, one of "
            + Arrays.toString(Encryption.getSupportedCiphers());

    protected static final long DEFAULT_START_KEY = 0;

    /** This will be removed as we factor out the dependency on command line */
    protected CommandLine cmd;

    protected MultiThreadedWriter writerThreads = null;
    protected MultiThreadedReader readerThreads = null;
    protected MultiThreadedUpdater updaterThreads = null;

    protected long startKey, endKey;

    protected boolean isWrite, isRead, isUpdate;

    // Column family options
    protected DataBlockEncoding dataBlockEncodingAlgo;
    protected Compression.Algorithm compressAlgo;
    protected BloomType bloomType;
    private boolean inMemoryCF;

    private User userOwner;
    // Writer options
    protected int numWriterThreads = DEFAULT_NUM_THREADS;
    protected int minColsPerKey, maxColsPerKey;
    protected int minColDataSize = DEFAULT_DATA_SIZE, maxColDataSize = DEFAULT_DATA_SIZE;
    protected boolean isMultiPut;

    // Updater options
    protected int numUpdaterThreads = DEFAULT_NUM_THREADS;
    protected int updatePercent;
    protected boolean ignoreConflicts = false;
    protected boolean isBatchUpdate;

    // Reader options
    private int numReaderThreads = DEFAULT_NUM_THREADS;
    private int keyWindow = MultiThreadedReader.DEFAULT_KEY_WINDOW;
    private int maxReadErrors = MultiThreadedReader.DEFAULT_MAX_ERRORS;
    private int verifyPercent;

    private int numTables = 1;
    private int regionsPerServer = HBaseTestingUtility.DEFAULT_REGIONS_PER_SERVER;

    private String superUser;

    private String userNames = "user1, user2, user3, user4";
    //This file is used to read authentication information in secure clusters.
    private String authnFileName;

    // TODO: refactor LoadTestToolImpl somewhere to make the usage from tests less bad,
    //       console tool itself should only be used from console.
    protected boolean isSkipInit = false;
    protected boolean isInitOnly = false;

    protected Cipher cipher = null;

    protected String[] splitColonSeparated(String option, int minNumCols, int maxNumCols) {
        String optVal = cmd.getOptionValue(option);
        String[] cols = optVal.split(COLON);
        if (cols.length < minNumCols || cols.length > maxNumCols) {
            throw new IllegalArgumentException(
                    "Expected at least " + minNumCols + " columns but no more than " + maxNumCols
                            + " in the colon-separated value '" + optVal + "' of the " + "-" + option + " option");
        }
        return cols;
    }

    protected int getNumThreads(String numThreadsStr) {
        return parseInt(numThreadsStr, 1, Short.MAX_VALUE);
    }

    /**
     * Apply column family options such as Bloom filters, compression, and data
     * block encoding.
     */
    protected void applyColumnFamilyOptions(TableName tableName, byte[][] columnFamilies) throws IOException {
        HBaseAdmin admin = new HBaseAdmin(conf);
        HTableDescriptor tableDesc = admin.getTableDescriptor(tableName);
        LOG.info("Disabling table " + tableName);
        admin.disableTable(tableName);
        for (byte[] cf : columnFamilies) {
            HColumnDescriptor columnDesc = tableDesc.getFamily(cf);
            boolean isNewCf = columnDesc == null;
            if (isNewCf) {
                columnDesc = new HColumnDescriptor(cf);
            }
            if (bloomType != null) {
                columnDesc.setBloomFilterType(bloomType);
            }
            if (compressAlgo != null) {
                columnDesc.setCompressionType(compressAlgo);
            }
            if (dataBlockEncodingAlgo != null) {
                columnDesc.setDataBlockEncoding(dataBlockEncodingAlgo);
            }
            if (inMemoryCF) {
                columnDesc.setInMemory(inMemoryCF);
            }
            if (cipher != null) {
                byte[] keyBytes = new byte[cipher.getKeyLength()];
                new SecureRandom().nextBytes(keyBytes);
                columnDesc.setEncryptionType(cipher.getName());
                columnDesc.setEncryptionKey(EncryptionUtil.wrapKey(conf, User.getCurrent().getShortName(),
                        new SecretKeySpec(keyBytes, cipher.getName())));
            }
            if (isNewCf) {
                admin.addColumn(tableName, columnDesc);
            } else {
                admin.modifyColumn(tableName, columnDesc);
            }
        }
        LOG.info("Enabling table " + tableName);
        admin.enableTable(tableName);
    }

    @Override
    protected void addOptions() {
        addOptWithArg(OPT_ZK_QUORUM, "ZK quorum as comma-separated host names " + "without port numbers");
        addOptWithArg(OPT_ZK_PARENT_NODE, "name of parent znode in zookeeper");
        addOptWithArg(OPT_TABLE_NAME, "The name of the table to read or write");
        addOptWithArg(OPT_WRITE, OPT_USAGE_LOAD);
        addOptWithArg(OPT_READ, OPT_USAGE_READ);
        addOptWithArg(OPT_UPDATE, OPT_USAGE_UPDATE);
        addOptNoArg(OPT_INIT_ONLY, "Initialize the test table only, don't do any loading");
        addOptWithArg(OPT_BLOOM, OPT_USAGE_BLOOM);
        addOptWithArg(OPT_COMPRESSION, OPT_USAGE_COMPRESSION);
        addOptWithArg(OPT_DATA_BLOCK_ENCODING, OPT_DATA_BLOCK_ENCODING_USAGE);
        addOptWithArg(OPT_MAX_READ_ERRORS,
                "The maximum number of read errors "
                        + "to tolerate before terminating all reader threads. The default is "
                        + MultiThreadedReader.DEFAULT_MAX_ERRORS + ".");
        addOptWithArg(OPT_KEY_WINDOW,
                "The 'key window' to maintain between "
                        + "reads and writes for concurrent write/read workload. The default " + "is "
                        + MultiThreadedReader.DEFAULT_KEY_WINDOW + ".");

        addOptNoArg(OPT_MULTIPUT,
                "Whether to use multi-puts as opposed to " + "separate puts for every column in a row");
        addOptNoArg(OPT_BATCHUPDATE,
                "Whether to use batch as opposed to " + "separate updates for every column in a row");
        addOptNoArg(OPT_INMEMORY, OPT_USAGE_IN_MEMORY);
        addOptWithArg(OPT_GENERATOR, OPT_GENERATOR_USAGE);

        addOptWithArg(OPT_NUM_KEYS, "The number of keys to read/write");
        addOptWithArg(OPT_START_KEY, "The first key to read/write " + "(a 0-based index). The default value is "
                + DEFAULT_START_KEY + ".");
        addOptNoArg(OPT_SKIP_INIT, "Skip the initialization; assume test table " + "already exists");

        addOptWithArg(NUM_TABLES,
                "A positive integer number. When a number n is speicfied, load test "
                        + "tool  will load n table parallely. -tn parameter value becomes "
                        + "table name prefix. Each table name is in format <tn>_1...<tn>_n");

        addOptWithArg(OPT_REGIONS_PER_SERVER, "A positive integer number. When a number n is specified, load test "
                + "tool will create the test table with n regions per server");

        addOptWithArg(OPT_ENCRYPTION, OPT_ENCRYPTION_USAGE);
    }

    @Override
    protected void processOptions(CommandLine cmd) {
        this.cmd = cmd;

        tableName = TableName.valueOf(cmd.getOptionValue(OPT_TABLE_NAME, DEFAULT_TABLE_NAME));

        isWrite = cmd.hasOption(OPT_WRITE);
        isRead = cmd.hasOption(OPT_READ);
        isUpdate = cmd.hasOption(OPT_UPDATE);
        isInitOnly = cmd.hasOption(OPT_INIT_ONLY);

        if (!isWrite && !isRead && !isUpdate && !isInitOnly) {
            throw new IllegalArgumentException(
                    "Either -" + OPT_WRITE + " or " + "-" + OPT_UPDATE + "-" + OPT_READ + " has to be specified");
        }

        if (isInitOnly && (isRead || isWrite || isUpdate)) {
            throw new IllegalArgumentException(OPT_INIT_ONLY + " cannot be specified with" + " either -" + OPT_WRITE
                    + " or -" + OPT_UPDATE + " or -" + OPT_READ);
        }

        if (!isInitOnly) {
            if (!cmd.hasOption(OPT_NUM_KEYS)) {
                throw new IllegalArgumentException(OPT_NUM_KEYS + " must be specified in " + "read or write mode");
            }
            startKey = parseLong(cmd.getOptionValue(OPT_START_KEY, String.valueOf(DEFAULT_START_KEY)), 0,
                    Long.MAX_VALUE);
            long numKeys = parseLong(cmd.getOptionValue(OPT_NUM_KEYS), 1, Long.MAX_VALUE - startKey);
            endKey = startKey + numKeys;
            isSkipInit = cmd.hasOption(OPT_SKIP_INIT);
            System.out.println("Key range: [" + startKey + ".." + (endKey - 1) + "]");
        }

        parseColumnFamilyOptions(cmd);

        if (isWrite) {
            String[] writeOpts = splitColonSeparated(OPT_WRITE, 2, 3);

            int colIndex = 0;
            minColsPerKey = 1;
            maxColsPerKey = 2 * Integer.parseInt(writeOpts[colIndex++]);
            int avgColDataSize = parseInt(writeOpts[colIndex++], 1, Integer.MAX_VALUE);
            minColDataSize = avgColDataSize / 2;
            maxColDataSize = avgColDataSize * 3 / 2;

            if (colIndex < writeOpts.length) {
                numWriterThreads = getNumThreads(writeOpts[colIndex++]);
            }

            isMultiPut = cmd.hasOption(OPT_MULTIPUT);

            System.out.println("Multi-puts: " + isMultiPut);
            System.out.println("Columns per key: " + minColsPerKey + ".." + maxColsPerKey);
            System.out.println("Data size per column: " + minColDataSize + ".." + maxColDataSize);
        }

        if (isUpdate) {
            String[] mutateOpts = splitColonSeparated(OPT_UPDATE, 1, 3);
            int colIndex = 0;
            updatePercent = parseInt(mutateOpts[colIndex++], 0, 100);
            if (colIndex < mutateOpts.length) {
                numUpdaterThreads = getNumThreads(mutateOpts[colIndex++]);
            }
            if (colIndex < mutateOpts.length) {
                ignoreConflicts = parseInt(mutateOpts[colIndex++], 0, 1) == 1;
            }

            isBatchUpdate = cmd.hasOption(OPT_BATCHUPDATE);

            System.out.println("Batch updates: " + isBatchUpdate);
            System.out.println("Percent of keys to update: " + updatePercent);
            System.out.println("Updater threads: " + numUpdaterThreads);
            System.out.println("Ignore nonce conflicts: " + ignoreConflicts);
        }

        if (isRead) {
            String[] readOpts = splitColonSeparated(OPT_READ, 1, 2);
            int colIndex = 0;
            verifyPercent = parseInt(readOpts[colIndex++], 0, 100);
            if (colIndex < readOpts.length) {
                numReaderThreads = getNumThreads(readOpts[colIndex++]);
            }

            if (cmd.hasOption(OPT_MAX_READ_ERRORS)) {
                maxReadErrors = parseInt(cmd.getOptionValue(OPT_MAX_READ_ERRORS), 0, Integer.MAX_VALUE);
            }

            if (cmd.hasOption(OPT_KEY_WINDOW)) {
                keyWindow = parseInt(cmd.getOptionValue(OPT_KEY_WINDOW), 0, Integer.MAX_VALUE);
            }

            System.out.println("Percent of keys to verify: " + verifyPercent);
            System.out.println("Reader threads: " + numReaderThreads);
        }

        numTables = 1;
        if (cmd.hasOption(NUM_TABLES)) {
            numTables = parseInt(cmd.getOptionValue(NUM_TABLES), 1, Short.MAX_VALUE);
        }
        regionsPerServer = HBaseTestingUtility.DEFAULT_REGIONS_PER_SERVER;
        if (cmd.hasOption(OPT_REGIONS_PER_SERVER)) {
            regionsPerServer = parseInt(cmd.getOptionValue(OPT_REGIONS_PER_SERVER), 1, Integer.MAX_VALUE);
            conf.setInt(HBaseTestingUtility.REGIONS_PER_SERVER_KEY, regionsPerServer);
        }
        System.out.println("Regions per server: " + regionsPerServer);
    }

    private void parseColumnFamilyOptions(CommandLine cmd) {
        String dataBlockEncodingStr = cmd.getOptionValue(OPT_DATA_BLOCK_ENCODING);
        dataBlockEncodingAlgo = dataBlockEncodingStr == null ? null
                : DataBlockEncoding.valueOf(dataBlockEncodingStr);

        String compressStr = cmd.getOptionValue(OPT_COMPRESSION);
        compressAlgo = compressStr == null ? Compression.Algorithm.NONE
                : Compression.Algorithm.valueOf(compressStr);

        String bloomStr = cmd.getOptionValue(OPT_BLOOM);
        bloomType = bloomStr == null ? null : BloomType.valueOf(bloomStr);

        inMemoryCF = cmd.hasOption(OPT_INMEMORY);
        if (cmd.hasOption(OPT_ENCRYPTION)) {
            cipher = Encryption.getCipher(conf, cmd.getOptionValue(OPT_ENCRYPTION));
        }
    }

    public void initTestTable() throws IOException {
        HBaseTestingUtility.createPreSplitLoadTestTable(conf, tableName, COLUMN_FAMILY, compressAlgo,
                dataBlockEncodingAlgo);
        applyColumnFamilyOptions(tableName, COLUMN_FAMILIES);
    }

    @Override
    protected int doWork() throws IOException {
        if (numTables > 1) {
            return parallelLoadTables();
        } else {
            return loadTable();
        }
    }

    protected int loadTable() throws IOException {
        if (cmd.hasOption(OPT_ZK_QUORUM)) {
            conf.set(HConstants.ZOOKEEPER_QUORUM, cmd.getOptionValue(OPT_ZK_QUORUM));
        }
        if (cmd.hasOption(OPT_ZK_PARENT_NODE)) {
            conf.set(HConstants.ZOOKEEPER_ZNODE_PARENT, cmd.getOptionValue(OPT_ZK_PARENT_NODE));
        }

        if (isInitOnly) {
            LOG.info("Initializing only; no reads or writes");
            initTestTable();
            return 0;
        }

        if (!isSkipInit) {
            initTestTable();
        }
        LoadTestDataGenerator dataGen = null;
        if (cmd.hasOption(OPT_GENERATOR)) {
            String[] clazzAndArgs = cmd.getOptionValue(OPT_GENERATOR).split(COLON);
            dataGen = getLoadGeneratorInstance(clazzAndArgs[0]);
            String[] args;
            if (dataGen instanceof LoadTestDataGeneratorWithACL) {
                LOG.info("Using LoadTestDataGeneratorWithACL");
                if (User.isHBaseSecurityEnabled(conf)) {
                    LOG.info("Security is enabled");
                    authnFileName = clazzAndArgs[1];
                    superUser = clazzAndArgs[2];
                    userNames = clazzAndArgs[3];
                    args = Arrays.copyOfRange(clazzAndArgs, 2, clazzAndArgs.length);
                    Properties authConfig = new Properties();
                    authConfig.load(this.getClass().getClassLoader().getResourceAsStream(authnFileName));
                    try {
                        addAuthInfoToConf(authConfig, conf, superUser, userNames);
                    } catch (IOException exp) {
                        LOG.error(exp);
                        return EXIT_FAILURE;
                    }
                    userOwner = User.create(loginAndReturnUGI(conf, superUser));
                } else {
                    superUser = clazzAndArgs[1];
                    userNames = clazzAndArgs[2];
                    args = Arrays.copyOfRange(clazzAndArgs, 1, clazzAndArgs.length);
                    userOwner = User.createUserForTesting(conf, superUser, new String[0]);
                }
            } else {
                args = clazzAndArgs.length == 1 ? new String[0]
                        : Arrays.copyOfRange(clazzAndArgs, 1, clazzAndArgs.length);
            }
            dataGen.initialize(args);
        } else {
            // Default DataGenerator is MultiThreadedAction.DefaultDataGenerator
            dataGen = new MultiThreadedAction.DefaultDataGenerator(minColDataSize, maxColDataSize, minColsPerKey,
                    maxColsPerKey, COLUMN_FAMILY);
        }

        if (User.isHBaseSecurityEnabled(conf) && userOwner != null) {
            LOG.info("Granting permissions for user " + userOwner.getShortName());
            AccessControlProtos.Permission.Action[] actions = { AccessControlProtos.Permission.Action.ADMIN,
                    AccessControlProtos.Permission.Action.CREATE, AccessControlProtos.Permission.Action.READ,
                    AccessControlProtos.Permission.Action.WRITE };
            try {
                AccessControlClient.grant(conf, tableName, userOwner.getShortName(), null, null, actions);
            } catch (Throwable e) {
                LOG.fatal("Error in granting permission for the user " + userOwner.getShortName(), e);
                return EXIT_FAILURE;
            }
        }

        if (userNames != null) {
            // This will be comma separated list of expressions.
            String users[] = userNames.split(",");
            User user = null;
            if (User.isHBaseSecurityEnabled(conf)) {
                for (String userStr : users) {
                    user = User.create(loginAndReturnUGI(conf, userStr));
                    LOG.info("Granting READ permission for the user " + user.getShortName());
                    AccessControlProtos.Permission.Action[] actions = {
                            AccessControlProtos.Permission.Action.READ };
                    try {
                        AccessControlClient.grant(conf, tableName, user.getShortName(), null, null, actions);
                    } catch (Throwable e) {
                        LOG.fatal("Error in granting READ permission for the user " + user.getShortName(), e);
                        return EXIT_FAILURE;
                    }
                }
            } else {
                for (String userStr : users) {
                    user = User.createUserForTesting(conf, userStr, new String[0]);
                }
            }
        }

        if (isWrite) {
            if (userOwner != null) {
                writerThreads = new MultiThreadedWriterWithACL(dataGen, conf, tableName, userOwner);
            } else {
                writerThreads = new MultiThreadedWriter(dataGen, conf, tableName);
            }
            writerThreads.setMultiPut(isMultiPut);
        }

        if (isUpdate) {
            if (userOwner != null) {
                updaterThreads = new MultiThreadedUpdaterWithACL(dataGen, conf, tableName, updatePercent, userOwner,
                        userNames);
            } else {
                updaterThreads = new MultiThreadedUpdater(dataGen, conf, tableName, updatePercent);
            }
            updaterThreads.setBatchUpdate(isBatchUpdate);
            updaterThreads.setIgnoreNonceConflicts(ignoreConflicts);
        }

        if (isRead) {
            if (userOwner != null) {
                readerThreads = new MultiThreadedReaderWithACL(dataGen, conf, tableName, verifyPercent, userNames);
            } else {
                readerThreads = new MultiThreadedReader(dataGen, conf, tableName, verifyPercent);
            }
            readerThreads.setMaxErrors(maxReadErrors);
            readerThreads.setKeyWindow(keyWindow);
        }

        if (isUpdate && isWrite) {
            LOG.info("Concurrent write/update workload: making updaters aware of the " + "write point");
            updaterThreads.linkToWriter(writerThreads);
        }

        if (isRead && (isUpdate || isWrite)) {
            LOG.info("Concurrent write/read workload: making readers aware of the " + "write point");
            readerThreads.linkToWriter(isUpdate ? updaterThreads : writerThreads);
        }

        if (isWrite) {
            System.out.println("Starting to write data...");
            writerThreads.start(startKey, endKey, numWriterThreads);
        }

        if (isUpdate) {
            LOG.info("Starting to mutate data...");
            System.out.println("Starting to mutate data...");
            // TODO : currently append and increment operations not tested with tags
            // Will update this aftet it is done
            updaterThreads.start(startKey, endKey, numUpdaterThreads);
        }

        if (isRead) {
            System.out.println("Starting to read data...");
            readerThreads.start(startKey, endKey, numReaderThreads);
        }

        if (isWrite) {
            writerThreads.waitForFinish();
        }

        if (isUpdate) {
            updaterThreads.waitForFinish();
        }

        if (isRead) {
            readerThreads.waitForFinish();
        }

        boolean success = true;
        if (isWrite) {
            success = success && writerThreads.getNumWriteFailures() == 0;
        }
        if (isUpdate) {
            success = success && updaterThreads.getNumWriteFailures() == 0;
        }
        if (isRead) {
            success = success && readerThreads.getNumReadErrors() == 0 && readerThreads.getNumReadFailures() == 0;
        }
        return success ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    private LoadTestDataGenerator getLoadGeneratorInstance(String clazzName) throws IOException {
        try {
            Class<?> clazz = Class.forName(clazzName);
            Constructor<?> constructor = clazz.getConstructor(int.class, int.class, int.class, int.class,
                    byte[][].class);
            return (LoadTestDataGenerator) constructor.newInstance(minColDataSize, maxColDataSize, minColsPerKey,
                    maxColsPerKey, COLUMN_FAMILIES);
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    public static byte[] generateData(final Random r, int length) {
        byte[] b = new byte[length];
        int i = 0;

        for (i = 0; i < (length - 8); i += 8) {
            b[i] = (byte) (65 + r.nextInt(26));
            b[i + 1] = b[i];
            b[i + 2] = b[i];
            b[i + 3] = b[i];
            b[i + 4] = b[i];
            b[i + 5] = b[i];
            b[i + 6] = b[i];
            b[i + 7] = b[i];
        }

        byte a = (byte) (65 + r.nextInt(26));
        for (; i < length; i++) {
            b[i] = a;
        }
        return b;
    }

    public static void main(String[] args) {
        new LoadTestTool().doStaticMain(args);
    }

    /**
     * When NUM_TABLES is specified, the function starts multiple worker threads
     * which individually start a LoadTestTool instance to load a table. Each
     * table name is in format <tn>_<index>. For example, "-tn test -num_tables 2"
     * , table names will be "test_1", "test_2"
     *
     * @throws IOException
     */
    private int parallelLoadTables() throws IOException {
        // create new command args
        String tableName = cmd.getOptionValue(OPT_TABLE_NAME, DEFAULT_TABLE_NAME);
        String[] newArgs = null;
        if (!cmd.hasOption(LoadTestTool.OPT_TABLE_NAME)) {
            newArgs = new String[cmdLineArgs.length + 2];
            newArgs[0] = "-" + LoadTestTool.OPT_TABLE_NAME;
            newArgs[1] = LoadTestTool.DEFAULT_TABLE_NAME;
            for (int i = 0; i < cmdLineArgs.length; i++) {
                newArgs[i + 2] = cmdLineArgs[i];
            }
        } else {
            newArgs = cmdLineArgs;
        }

        int tableNameValueIndex = -1;
        for (int j = 0; j < newArgs.length; j++) {
            if (newArgs[j].endsWith(OPT_TABLE_NAME)) {
                tableNameValueIndex = j + 1;
            } else if (newArgs[j].endsWith(NUM_TABLES)) {
                // change NUM_TABLES to 1 so that each worker loads one table
                newArgs[j + 1] = "1";
            }
        }

        // starting to load multiple tables
        List<WorkerThread> workers = new ArrayList<WorkerThread>();
        for (int i = 0; i < numTables; i++) {
            String[] workerArgs = newArgs.clone();
            workerArgs[tableNameValueIndex] = tableName + "_" + (i + 1);
            WorkerThread worker = new WorkerThread(i, workerArgs);
            workers.add(worker);
            LOG.info(worker + " starting");
            worker.start();
        }

        // wait for all workers finish
        LOG.info("Waiting for worker threads to finish");
        for (WorkerThread t : workers) {
            try {
                t.join();
            } catch (InterruptedException ie) {
                IOException iie = new InterruptedIOException();
                iie.initCause(ie);
                throw iie;
            }
            checkForErrors();
        }

        return EXIT_SUCCESS;
    }

    // If an exception is thrown by one of worker threads, it will be
    // stored here.
    protected AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();

    private void workerThreadError(Throwable t) {
        thrown.compareAndSet(null, t);
    }

    /**
     * Check for errors in the writer threads. If any is found, rethrow it.
     */
    private void checkForErrors() throws IOException {
        Throwable thrown = this.thrown.get();
        if (thrown == null)
            return;
        if (thrown instanceof IOException) {
            throw (IOException) thrown;
        } else {
            throw new RuntimeException(thrown);
        }
    }

    class WorkerThread extends Thread {
        private String[] workerArgs;

        WorkerThread(int i, String[] args) {
            super("WorkerThread-" + i);
            workerArgs = args;
        }

        @Override
        public void run() {
            try {
                int ret = ToolRunner.run(HBaseConfiguration.create(), new LoadTestTool(), workerArgs);
                if (ret != 0) {
                    throw new RuntimeException("LoadTestTool exit with non-zero return code.");
                }
            } catch (Exception ex) {
                LOG.error("Error in worker thread", ex);
                workerThreadError(ex);
            }
        }
    }

    private void addAuthInfoToConf(Properties authConfig, Configuration conf, String owner, String userList)
            throws IOException {
        List<String> users = Arrays.asList(userList.split(","));
        users.add(owner);
        for (String user : users) {
            String keyTabFileConfKey = "hbase." + user + ".keytab.file";
            String principalConfKey = "hbase." + user + ".kerberos.principal";
            if (!authConfig.containsKey(keyTabFileConfKey) || !authConfig.containsKey(principalConfKey)) {
                throw new IOException("Authentication configs missing for user : " + user);
            }
        }
        for (String key : authConfig.stringPropertyNames()) {
            conf.set(key, authConfig.getProperty(key));
        }
        LOG.debug("Added authentication properties to config successfully.");
    }

    public static UserGroupInformation loginAndReturnUGI(Configuration conf, String username) throws IOException {
        String hostname = InetAddress.getLocalHost().getHostName();
        String keyTabFileConfKey = "hbase." + username + ".keytab.file";
        String keyTabFileLocation = conf.get(keyTabFileConfKey);
        String principalConfKey = "hbase." + username + ".kerberos.principal";
        String principal = SecurityUtil.getServerPrincipal(conf.get(principalConfKey), hostname);
        if (keyTabFileLocation == null || principal == null) {
            LOG.warn("Principal or key tab file null for : " + principalConfKey + ", " + keyTabFileConfKey);
        }
        UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal,
                keyTabFileLocation);
        return ugi;
    }
}