/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 1997, 2010 Oracle and/or its affiliates.  All rights reserved.
 *
 * $Id$
 */

package db;

import com.sleepycat.db.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import java.util.GregorianCalendar;

//
// This program implements a basic TPC/B driver program.  To create the
// TPC/B database, run with the -i (init) flag.  The number of records
// with which to populate the account, history, branch, and teller tables
// is specified by the a, s, b, and t flags respectively.  To run a TPC/B
// test, use the n flag to indicate a number of transactions to run in
// each thread and -T to specify the number of threads.
//
class TpcbExample {
    public static final int TELLERS_PER_BRANCH = 10;
    public static final int ACCOUNTS_PER_TELLER = 10000;
    public static final int HISTORY_PER_BRANCH = 2592000;

    //
    // The default configuration that adheres to TPCB scaling rules requires
    // nearly 3 GB of space.  To avoid requiring that much space for testing,
    // we set the parameters much lower.  If you want to run a valid 10 TPS
    // configuration, uncomment the VALID_SCALING configuration
    //

    // VALID_SCALING configuration
    /*
      public static final int ACCOUNTS = 1000000;
      public static final int BRANCHES = 10;
      public static final int TELLERS = 100;
      public static final int HISTORY = 25920000;
    */

    // TINY configuration
    /*
      public static final int ACCOUNTS = 1000;
      public static final int BRANCHES = 10;
      public static final int TELLERS = 100;
      public static final int HISTORY = 10000;
    */

    // Default configuration
    public static final int ACCOUNTS = 100000;
    public static final int BRANCHES = 10;
    public static final int TELLERS = 100;
    public static final int HISTORY = 259200;

    public static final int HISTORY_LEN = 100;
    public static final int RECLEN = 100;
    public static final int BEGID = 1000000;

    // used by random_id()
    public static final int ACCOUNT = 0;
    public static final int BRANCH = 1;
    public static final int TELLER = 2;

    public static boolean verbose = false;
    public static final String progname = "TpcbExample";    // Program name.

    Environment dbenv;
    int accounts, branches, tellers, history;

    public TpcbExample(File home,
                       int accounts, int branches, int tellers, int history,
                       int cachesize, boolean noSync)
        throws DatabaseException, FileNotFoundException {

        this.accounts = accounts;
        this.branches = branches;
        this.tellers = tellers;
        this.history = history;

        EnvironmentConfig config = new EnvironmentConfig();
        config.setErrorStream(System.err);
        config.setErrorPrefix(progname);
        config.setLockDetectMode(LockDetectMode.DEFAULT);
        config.setCacheSize(cachesize == 0 ? 4 * 1024 * 1024 : cachesize);
        config.setTxnNoSync(noSync);
        config.setLockDetectMode(LockDetectMode.DEFAULT);
        config.setAllowCreate(true);

        config.setInitializeCache(true);
        config.setTransactional(true);
        config.setInitializeLocking(true);
        config.setInitializeLogging(true);

        dbenv = new Environment(home, config);
    }

    public void close()
        throws DatabaseException {

        try {
            if (dbenv != null)
                dbenv.close();
        } finally {
            dbenv = null;
        }
    }

    //
    // Initialize the database to the number of accounts, branches,
    // history records, and tellers given to the constructor.
    //
    public void populate() {
        Database dbp = null;

        int err;
        int balance, idnum;
        int end_anum, end_bnum, end_tnum;
        int start_anum, start_bnum, start_tnum;
        int h_nelem;

        idnum = BEGID;
        balance = 500000;

        h_nelem = accounts;

        try {
            DatabaseConfig config = new DatabaseConfig();
            config.setType(DatabaseType.HASH);
            config.setHashNumElements(h_nelem);
            config.setAllowCreate(true);
            dbp = dbenv.openDatabase(null, "account", null, config);
        } catch (Exception e1) {
            // can be DatabaseException or FileNotFoundException
            errExit(e1, "Open of account file failed");
        }

        start_anum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "account");
        idnum += h_nelem;
        end_anum = idnum - 1;
        try {
            dbp.close();
        } catch (DatabaseException e2) {
            errExit(e2, "Account file close failed");
        }

        if (verbose)
            System.out.println("Populated accounts: " +
                               String.valueOf(start_anum) + " - " +
                               String.valueOf(end_anum));

        //
        // Since the number of branches is very small, we want to use very
        // small pages and only 1 key per page.  This is the poor-man's way
        // of getting key locking instead of page locking.
        //
        h_nelem = (int)branches;

        try {
            DatabaseConfig config = new DatabaseConfig();
            config.setType(DatabaseType.HASH);
            config.setHashNumElements(h_nelem);
            config.setHashFillFactor(1);
            config.setPageSize(512);
            config.setAllowCreate(true);
            dbp = dbenv.openDatabase(null, "branch", null, config);
        } catch (Exception e3) {
            // can be DatabaseException or FileNotFoundException
            errExit(e3, "Branch file create failed");
        }

        start_bnum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "branch");
        idnum += h_nelem;
        end_bnum = idnum - 1;

        try {
            dbp.close();
        } catch (DatabaseException dbe4) {
            errExit(dbe4, "Close of branch file failed");
        }

        if (verbose)
            System.out.println("Populated branches: " +
                               String.valueOf(start_bnum) + " - " +
                               String.valueOf(end_bnum));

        //
        // In the case of tellers, we also want small pages, but we'll let
        // the fill factor dynamically adjust itself.
        //
        h_nelem = (int)tellers;

        try {
            DatabaseConfig config = new DatabaseConfig();
            config.setType(DatabaseType.HASH);
            config.setHashNumElements(h_nelem);
            config.setHashFillFactor(0);
            config.setPageSize(512);
            config.setAllowCreate(true);
            dbp = dbenv.openDatabase(null, "teller", null, config);
        } catch (Exception e5) {
            // can be DatabaseException or FileNotFoundException
            errExit(e5, "Teller file create failed");
        }

        start_tnum = idnum;
        populateTable(dbp, idnum, balance, h_nelem, "teller");
        idnum += h_nelem;
        end_tnum = idnum - 1;

        try {
            dbp.close();
        } catch (DatabaseException e6) {
            errExit(e6, "Close of teller file failed");
        }

        if (verbose)
            System.out.println("Populated tellers: " +
                               String.valueOf(start_tnum) + " - " +
                               String.valueOf(end_tnum));

        try {
            DatabaseConfig config = new DatabaseConfig();
            config.setType(DatabaseType.RECNO);
            config.setRecordLength(HISTORY_LEN);
            config.setAllowCreate(true);
            dbp = dbenv.openDatabase(null, "history", null, config);
        } catch (Exception e7) {
            // can be DatabaseException or FileNotFoundException
            errExit(e7, "Create of history file failed");
        }

        populateHistory(dbp);

        try {
            dbp.close();
        } catch (DatabaseException e8) {
            errExit(e8, "Close of history file failed");
        }
    }

    public void populateTable(Database dbp,
                              int start_id, int balance, int nrecs, String msg) {
        Defrec drec = new Defrec();

        DatabaseEntry kdbt = new DatabaseEntry(drec.data);
        kdbt.setSize(4);                  // sizeof(int)
        DatabaseEntry ddbt = new DatabaseEntry(drec.data);
        ddbt.setSize(drec.data.length);   // uses whole array

        try {
            for (int i = 0; i < nrecs; i++) {
                kdbt.setRecordNumber(start_id + (int)i);
                drec.set_balance(balance);
                dbp.putNoOverwrite(null, kdbt, ddbt);
            }
        } catch (DatabaseException dbe) {
            System.err.println("Failure initializing " + msg + " file: " +
                               dbe.toString());
            System.exit(1);
        }
    }

    public void populateHistory(Database dbp) {
        Histrec hrec = new Histrec();
        hrec.set_amount(10);

        byte[] arr = new byte[4];                  // sizeof(int)
        int i;
        DatabaseEntry kdbt = new DatabaseEntry(arr);
        kdbt.setSize(arr.length);
        DatabaseEntry ddbt = new DatabaseEntry(hrec.data);
        ddbt.setSize(hrec.data.length);

        try {
            for (i = 1; i <= history; i++) {
                kdbt.setRecordNumber(i);

                hrec.set_aid(random_id(ACCOUNT));
                hrec.set_bid(random_id(BRANCH));
                hrec.set_tid(random_id(TELLER));

                dbp.append(null, kdbt, ddbt);
            }
        } catch (DatabaseException dbe) {
            errExit(dbe, "Failure initializing history file");
        }
    }

    static Random rand = new Random();
    public static int random_int(int lo, int hi) {
        int t = rand.nextInt();
        if (t < 0)
            t = -t;
        int ret = (int)(((double)t / ((double)(Integer.MAX_VALUE) + 1)) *
            (hi - lo + 1));
        ret += lo;
        return (ret);
    }

    public int random_id(int type) {
        int min, max, num;

        max = min = BEGID;
        num = accounts;
        switch(type) {
        case TELLER:
            min += branches;
            num = tellers;
            // fallthrough
        case BRANCH:
            if (type == BRANCH)
                num = branches;
            min += accounts;
            // fallthrough
        case ACCOUNT:
            max = min + num - 1;
        }
        return (random_int(min, max));
    }

    // The byte order is our choice.
    //
    static long get_int_in_array(byte[] array, int offset) {
        return
            ((0xff & array[offset + 0]) << 0)  |
            ((0xff & array[offset + 1]) << 8)  |
            ((0xff & array[offset + 2]) << 16) |
            ((0xff & array[offset + 3]) << 24);
    }

    // Note: Value needs to be long to avoid sign extension
    static void set_int_in_array(byte[] array, int offset, long value) {
        array[offset + 0] = (byte)((value >> 0) & 0xff);
        array[offset + 1] = (byte)((value >> 8) & 0xff);
        array[offset + 2] = (byte)((value >> 16) & 0xff);
        array[offset + 3] = (byte)((value >> 24) & 0xff);
    }

    // round 'd' to 'scale' digits, and return result as string
    static String showRounded(double d, int scale) {
        return new BigDecimal(d).
            setScale(scale, BigDecimal.ROUND_HALF_DOWN).toString();
    }

    public void run(int ntxns, int threads) {
        double gtps;
        int txns, failed;
        long curtime, starttime;
        TxnThread[] txnList = new TxnThread[threads];
        for (int i = 0; i < threads; i++)
            txnList[i] = new TxnThread("Thread " + String.valueOf(i), ntxns);

        starttime = (new Date()).getTime();
        for (int i = 0; i < threads; i++)
            txnList[i].start();
        for (int i = 0; i < threads; i++)
            try {
                txnList[i].join();
            } catch (Exception e1) {
                errExit(e1, "join failed");
            }

        curtime = (new Date()).getTime();
        txns = failed = 0;
        for (int i = 0; i < threads; i++) {
            txns += txnList[i].txns;
            failed += txnList[i].failed;
        }
        gtps = (double)(txns - failed) /
            ((curtime - starttime) / 1000.0);
        System.out.print("\nTotal: " +
                         String.valueOf(txns) + " txns " +
                         String.valueOf(failed) + " failed ");
        System.out.println(showRounded(gtps, 2) + " TPS");
    }

    class TxnThread extends Thread {
        private int ntxns;       /* Number of txns we were asked to run. */
        public int txns, failed; /* Number that succeeded / failed. */
        private Database adb, bdb, hdb, tdb;

        public TxnThread(String name, int ntxns) {
            super(name);
            this.ntxns = ntxns;
        }

        public void run() {
            double gtps, itps;
            int n, ret;
            long start_time, end_time;

            //
            // Open the database files.
            //
            int err;
            try {
                DatabaseConfig config = new DatabaseConfig();
                config.setTransactional(true);
                adb = dbenv.openDatabase(null, "account", null, config);
                bdb = dbenv.openDatabase(null, "branch", null, config);
                tdb = dbenv.openDatabase(null, "teller", null, config);
                hdb = dbenv.openDatabase(null, "history", null, config);
            } catch (DatabaseException dbe) {
                TpcbExample.errExit(dbe, "Open of db files failed");
            } catch (FileNotFoundException fnfe) {
                TpcbExample.errExit(fnfe, "Open of db files failed, missing file");
            }

            start_time = (new Date()).getTime();
            for (txns = n = ntxns, failed = 0; n-- > 0;)
                if ((ret = txn()) != 0)
                    failed++;
            end_time = (new Date()).getTime();
            if (end_time == start_time)
                end_time++;

            System.out.println(getName() + ": " + (long)txns + " txns: " +
                failed + " failed, " + TpcbExample.showRounded(
                (txns - failed) / (double)(end_time - start_time), 2) + " TPS");

            try {
                adb.close();
                bdb.close();
                tdb.close();
                hdb.close();
            } catch (DatabaseException dbe2) {
                TpcbExample.errExit(dbe2, "Close of db files failed");
            }
        }

        //
        // XXX Figure out the appropriate way to pick out IDs.
        //
        int txn() {
            Cursor acurs = null;
            Cursor bcurs = null;
            Cursor hcurs = null;
            Cursor tcurs = null;
            Transaction t = null;

            Defrec rec = new Defrec();
            Histrec hrec = new Histrec();
            int account, branch, teller;

            DatabaseEntry d_dbt = new DatabaseEntry();
            DatabaseEntry d_histdbt = new DatabaseEntry();
            DatabaseEntry k_dbt = new DatabaseEntry();
            DatabaseEntry k_histdbt = new DatabaseEntry();

            account = TpcbExample.this.random_id(TpcbExample.ACCOUNT);
            branch = TpcbExample.this.random_id(TpcbExample.BRANCH);
            teller = TpcbExample.this.random_id(TpcbExample.TELLER);

            // The history key will not actually be retrieved,
            // but it does need to be set to something.
            byte[] hist_key = new byte[4];
            k_histdbt.setData(hist_key);
            k_histdbt.setSize(4 /* == sizeof(int)*/);

            byte[] key_bytes = new byte[4];
            k_dbt.setData(key_bytes);
            k_dbt.setSize(4 /* == sizeof(int)*/);

            d_dbt.setData(rec.data);
            d_dbt.setUserBuffer(rec.length(), true);

            hrec.set_aid(account);
            hrec.set_bid(branch);
            hrec.set_tid(teller);
            hrec.set_amount(10);
            // Request 0 bytes since we're just positioning.
            d_histdbt.setPartial(0, 0, true);

            // START PER-TRANSACTION TIMING.
            //
            // Technically, TPCB requires a limit on response time, you only
            // get to count transactions that complete within 2 seconds.
            // That's not an issue for this sample application -- regardless,
            // here's where the transaction begins.
            try {
                t = dbenv.beginTransaction(null, null);

                acurs = adb.openCursor(t, null);
                bcurs = bdb.openCursor(t, null);
                tcurs = tdb.openCursor(t, null);
                hcurs = hdb.openCursor(t, null);

                // Account record
                k_dbt.setRecordNumber(account);
                if (acurs.getSearchKey(k_dbt, d_dbt, null) != OperationStatus.SUCCESS)
                    throw new Exception("acurs get failed");
                rec.set_balance(rec.get_balance() + 10);
                acurs.putCurrent(d_dbt);

                // Branch record
                k_dbt.setRecordNumber(branch);
                if (bcurs.getSearchKey(k_dbt, d_dbt, null) != OperationStatus.SUCCESS)
                    throw new Exception("bcurs get failed");
                rec.set_balance(rec.get_balance() + 10);
                bcurs.putCurrent(d_dbt);

                // Teller record
                k_dbt.setRecordNumber(teller);
                if (tcurs.getSearchKey(k_dbt, d_dbt, null) != OperationStatus.SUCCESS)
                    throw new Exception("ccurs get failed");
                rec.set_balance(rec.get_balance() + 10);
                tcurs.putCurrent(d_dbt);

                // History record
                d_histdbt.setPartial(0, 0, false);
                d_histdbt.setData(hrec.data);
                d_histdbt.setUserBuffer(hrec.length(), true);
                if (hdb.append(t, k_histdbt, d_histdbt) != OperationStatus.SUCCESS)
                    throw new DatabaseException("put failed");

                acurs.close();
                acurs = null;
                bcurs.close();
                bcurs = null;
                tcurs.close();
                tcurs = null;
                hcurs.close();
                hcurs = null;

                // null out t in advance; if the commit fails,
                // we don't want to abort it in the catch clause.
                Transaction tmptxn = t;
                t = null;
                tmptxn.commit();

                // END TIMING
                return (0);
            } catch (Exception e) {
                try {
                    if (acurs != null)
                        acurs.close();
                    if (bcurs != null)
                        bcurs.close();
                    if (tcurs != null)
                        tcurs.close();
                    if (hcurs != null)
                        hcurs.close();
                    if (t != null)
                        t.abort();
                } catch (DatabaseException dbe) {
                    // not much we can do here.
                }

                if (TpcbExample.this.verbose) {
                    System.out.println("Transaction A=" + String.valueOf(account) +
                                       " B=" + String.valueOf(branch) +
                                       " T=" + String.valueOf(teller) +
                                       " failed");
                    System.out.println("Reason: " + e.toString());
                }
                return (-1);
            }
        }
    }

    private static void usage() {
        System.err.println(
               "usage: TpcbExample [-fiv] [-a accounts] [-b branches]\n" +
               "                   [-c cachesize] [-h home] [-n transactions]\n" +
               "                   [-T threads] [-S seed] [-s history] [-t tellers]");
        System.exit(1);
    }

    private static void invarg(String str) {
        System.err.println("TpcbExample: invalid argument: " + str);
        System.exit(1);
    }

    public static void errExit(Exception err, String s) {
        System.err.print(progname + ": ");
        if (s != null) {
            System.err.print(s + ": ");
        }
        System.err.println(err.toString());
        System.exit(1);
    }

    public static void main(String[] argv) throws java.io.IOException {
        File home = new File("TESTDIR");
        int accounts = ACCOUNTS;
        int branches = BRANCHES;
        int tellers = TELLERS;
        int history = HISTORY;
        int threads = 1;
        int mpool = 0;
        int ntxns = 0;
        boolean iflag = false;
        boolean txn_no_sync = false;
        long seed = (new GregorianCalendar()).get(Calendar.SECOND);

        for (int i = 0; i < argv.length; ++i) {
            if (argv[i].equals("-a")) {
                // Number of account records
                if ((accounts = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-b")) {
                // Number of branch records
                if ((branches = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-c")) {
                // Cachesize in bytes
                if ((mpool = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-f")) {
                // Fast mode: no txn sync.
                txn_no_sync = true;
            } else if (argv[i].equals("-h")) {
                // DB  home.
                home = new File(argv[++i]);
            } else if (argv[i].equals("-i")) {
                // Initialize the test.
                iflag = true;
            } else if (argv[i].equals("-n")) {
                // Number of transactions
                if ((ntxns = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-S")) {
                // Random number seed.
                seed = Long.parseLong(argv[++i]);
                if (seed <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-s")) {
                // Number of history records
                if ((history = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-T")) {
                // Number of threads
                if ((threads = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-t")) {
                // Number of teller records
                if ((tellers = Integer.parseInt(argv[++i])) <= 0)
                    invarg(argv[i]);
            } else if (argv[i].equals("-v")) {
                // Verbose option.
                verbose = true;
            } else {
                usage();
            }
        }

        rand.setSeed((int)seed);

        // Initialize the database environment.
        // Must be done in within a try block.
        //
        TpcbExample app = null;
        try {
            app = new TpcbExample(home, accounts, branches, tellers, history,
                                  mpool, iflag || txn_no_sync);
        } catch (Exception e1) {
            errExit(e1, "initializing environment failed");
        }

        if (verbose)
            System.out.println((long)accounts + " Accounts, " +
                               String.valueOf(branches) + " Branches, " +
                               String.valueOf(tellers) + " Tellers, " +
                               String.valueOf(history) + " History");

        if (iflag) {
            if (ntxns != 0)
                usage();
            app.populate();
        } else {
            if (ntxns == 0)
                usage();
            app.run(ntxns, threads);
        }

        // Shut down the application.

        try {
            app.close();
        } catch (DatabaseException dbe2) {
            errExit(dbe2, "appexit failed");
        }

        System.exit(0);
    }
};

// Simulate the following C struct:
// struct Defrec {
//     u_int32_t   id;
//     u_int32_t   balance;
//     u_int8_t    pad[RECLEN - sizeof(int) - sizeof(int)];
// };

class Defrec {
    public Defrec() {
        data = new byte[TpcbExample.RECLEN];
    }

    public int length() {
        return TpcbExample.RECLEN;
    }

    public long get_id() {
        return TpcbExample.get_int_in_array(data, 0);
    }

    public void set_id(long value) {
        TpcbExample.set_int_in_array(data, 0, value);
    }

    public long get_balance() {
        return TpcbExample.get_int_in_array(data, 4);
    }

    public void set_balance(long value) {
        TpcbExample.set_int_in_array(data, 4, value);
    }

    static {
        Defrec d = new Defrec();
        d.set_balance(500000);
    }

    public byte[] data;
}

// Simulate the following C struct:
// struct Histrec {
//     u_int32_t   aid;
//     u_int32_t   bid;
//     u_int32_t   tid;
//     u_int32_t   amount;
//     u_int8_t    pad[RECLEN - 4 * sizeof(u_int32_t)];
// };

class Histrec {
    public Histrec() {
        data = new byte[TpcbExample.RECLEN];
    }

    public int length() {
        return TpcbExample.RECLEN;
    }

    public long get_aid() {
        return TpcbExample.get_int_in_array(data, 0);
    }

    public void set_aid(long value) {
        TpcbExample.set_int_in_array(data, 0, value);
    }

    public long get_bid() {
        return TpcbExample.get_int_in_array(data, 4);
    }

    public void set_bid(long value) {
        TpcbExample.set_int_in_array(data, 4, value);
    }

    public long get_tid() {
        return TpcbExample.get_int_in_array(data, 8);
    }

    public void set_tid(long value) {
        TpcbExample.set_int_in_array(data, 8, value);
    }

    public long get_amount() {
        return TpcbExample.get_int_in_array(data, 12);
    }

    public void set_amount(long value) {
        TpcbExample.set_int_in_array(data, 12, value);
    }

    public byte[] data;
}
