/*- * Copyright (C) 2002, 2017, Oracle and/or its affiliates. All rights reserved. * * This file was distributed by Oracle as part of a version of Oracle Berkeley * DB Java Edition made available at: * * http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html * * Please see the LICENSE file included in the top-level directory of the * appropriate version of Oracle Berkeley DB Java Edition for a copy of the * license and additional information. */ import java.io.File; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.Transaction; import com.sleepycat.je.rep.RepInternal; import com.sleepycat.je.rep.ReplicatedEnvironment; import com.sleepycat.je.rep.ReplicationConfig; import com.sleepycat.je.rep.utilint.RepTestUtils; import com.sleepycat.je.rep.utilint.RepTestUtils.RepEnvInfo; import com.sleepycat.je.utilint.LoggerUtils; import com.sleepycat.je.utilint.TracerFormatter; import com.sleepycat.je.utilint.VLSN; import com.sleepycat.persist.EntityStore; import com.sleepycat.persist.PrimaryIndex; /** * Exercises replica-only, master-only, and replica/master hybrid failover. */ public class FailoverTest { private static final boolean verbose = Boolean.getBoolean("verbose"); private static final TracerFormatter dateFormat = new TracerFormatter(); /* Time to wait for each node to syncup to the master. */ private static final long NODE_SYNC_MS = 60 * 1000; private RepEnvInfo[] repEnvInfo; /* -------------------Configurable params----------------*/ /* Environment home root for whole replication group. */ private File envRoot; /* Replication group size. */ private int nNodes = 5; /* Database size. */ private int dbSize = 2000; /* Steady state will finish after doing this number of transactions. */ private int steadyTxns = 60000; /* * Specify the JE log file size and checkpoint bytes to provoke log * cleaning. */ private long logFileSize = 409600; private long checkpointBytes = 1000000; /* * More than enough disk space to hold the data set and allow cleaning. * Note that repeated syncups can prevent file deletion and more space * may be needed for outlier cases. */ private long maxDisk = 500 * 1000000; /* Select a new master after doing this number of operations. */ private int txnsPerRound = 100; private static final int opsPerTxn = 20; /* Cycle through the array nodes when provoking failovers. */ private int roundRobinIdx; /* A value to use to create new records. */ private int nextVal = dbSize + 1; /* Used for generating random keys. */ private final Random random = new Random(); /* Determines whether master, replica, or hybrid failover is tested. */ private FailoverAgent failoverAgent; private int subDir = 0; private boolean offHeap = false; private Logger logger = LoggerUtils.getLoggerFixedPrefix(getClass(), "FailoverTest"); private FailoverTest() { } public static void main(String args[]) { try { FailoverTest test = new FailoverTest(); test.parseArgs(args); test.doRampup(); test.doSteadyState(); if (verbose) { System.err.println("Test done"); } } catch (Throwable t) { t.printStackTrace(System.err); System.exit(1); } } /** * Grow the data store to the appropriate size for the steady state * portion of the test. */ private void doRampup() throws Exception { /* * Clean up from previous runs. This test will not succeed if there is * anything in the test environments. */ RepTestUtils.removeRepEnvironments(envRoot); final long mainCacheSize; final long offHeapCacheSize; if (offHeap) { mainCacheSize = nNodes * 10 * 1024 * 1024; offHeapCacheSize = 100 * 1024 * 1024; } else { mainCacheSize = 0; offHeapCacheSize = 0; } EnvironmentConfig envConfig = Utils.createEnvConfig( logFileSize, maxDisk, checkpointBytes, subDir, mainCacheSize, offHeapCacheSize); /* * We have a lot of environments open in a single process, so reduce * the cache size lest we run out of file descriptors. */ envConfig.setConfigParam(EnvironmentConfig.LOG_FILE_CACHE_SIZE, "30"); repEnvInfo = RepTestUtils.setupEnvInfos(envRoot, nNodes, envConfig); /* Increase timeout to avoid InsufficientAcksException. */ for (RepEnvInfo info : repEnvInfo) { info.getRepConfig().setReplicaAckTimeout(50, TimeUnit.SECONDS); } if (subDir > 0) { RepTestUtils.createRepSubDirs(repEnvInfo, subDir); } ReplicatedEnvironment master = Utils.getMaster(repEnvInfo); EntityStore store = Utils.openStore(master, Utils.DB_NAME); RepTestData.insertData(store, dbSize); Utils.doSyncAndCheck(repEnvInfo); } /** * Execute a steady stream of work until the steady state phase is over. */ private void doSteadyState() throws Exception { if (verbose) { System.err.println("Steady state starting"); } int round = 0; failoverAgent.init(); while (true) { round++; if (verbose) { System.err.println ("Round " + round); } /* * If doWork returns false, it means the steady state stage is * over. In this loop, the master stays up. The replicas are * killed in a round robin fashion. No need to sync up between * rounds; avoiding the syncup and check of data makes for more * variation. */ if (!oneRoundWorkAndFailover(round)) { if (verbose) { System.err.println("Steady state is over, ending test."); } break; } } /* * Shutdown all nodes and sync them up using DbSync. Then check * for node equality. */ ReplicatedEnvironment master = Utils.getMaster(repEnvInfo); VLSN lastTxnEnd = RepInternal.getNonNullRepImpl(master).getVLSNIndex(). getRange().getLastTxnEnd(); for (RepEnvInfo info : repEnvInfo) { info.closeEnv(); } /* * Run DbSync for each node. This is done concurrently in a thread per * node, but the syncups are competing for resources, so the time to * syncup and gain consistency for one node could in the worst case be * as long as the time to do it serially for all nodes. */ long maxTimeoutMs = NODE_SYNC_MS * nNodes; String maxTimeoutStr = String.valueOf(maxTimeoutMs) + " ms"; for (RepEnvInfo info : repEnvInfo) { info.getRepConfig().setConfigParam( ReplicationConfig.ENV_CONSISTENCY_TIMEOUT, maxTimeoutStr); info.getRepConfig().setConfigParam( ReplicationConfig.ENV_SETUP_TIMEOUT, maxTimeoutStr); } RepTestUtils.syncupGroup(maxTimeoutMs, repEnvInfo); /* Re-open them to check node equality. */ for (RepEnvInfo info : repEnvInfo) { info.getEnvConfig().setConfigParam("je.env.runCleaner", "false"); info.getEnvConfig().setConfigParam("je.env.runINCompressor", "false"); } RepTestUtils.restartGroup(repEnvInfo); RepTestUtils.checkNodeEquality(lastTxnEnd, verbose, repEnvInfo); RepTestUtils.checkUtilizationProfile(repEnvInfo); for (RepEnvInfo info : repEnvInfo) { info.closeEnv(); } /* Use DbSpace to check utilization. */ } /** * One round of work: the master executes updates. The replicas are killed * off in the middle of a round, so that they die on a non-commit boundary, * and need to do some rollback. * * @return false if the rampup stage has finished. */ private boolean oneRoundWorkAndFailover(int round) throws Exception { boolean runAble = true; // Utils.getMaster re-opens the whole group. Maybe we should try // letting the killed nodes stay down longer. ReplicatedEnvironment master = Utils.getMaster(repEnvInfo); for (RepEnvInfo rep : repEnvInfo) { logger.info( RepInternal.getNonNullRepImpl(rep.getEnv()).dumpState()); } EntityStore dbStore = Utils.openStore(master, Utils.DB_NAME); PrimaryIndex primaryIndex = dbStore.getPrimaryIndex(Integer.class, RepTestData.class); for (int whichTxn = 0; whichTxn < txnsPerRound; whichTxn++) { Transaction txn = master.beginTransaction(null, null); for (int i = 0; i < opsPerTxn; i++) { /* Do a random update here. */ int key = random.nextInt(dbSize); RepTestData data = new RepTestData(); data.setKey(key); data.setData(nextVal++); data.setName(RepTestData.generateNameField(key)); primaryIndex.put(txn, data); } /* * Kill some set of nodes once during this round, in the middle * of a transaction. */ if (failoverAgent.killOnIteration(whichTxn)) { roundRobinIdx = failoverAgent.shutdownNodes(round, whichTxn, roundRobinIdx); } if (master.isValid()) { /* * This txn may be invalid if the master was killed by the * failover agent. */ txn.commit(); } /* Check whether the steady stage should break. */ if (--steadyTxns == 0) { runAble = false; break; } } try { dbStore.close(); } catch (RuntimeException e) { if (master.isValid()) { throw e; } } return runAble; } private void setFailoverAgent(String modeValue) throws IllegalArgumentException { if (modeValue.equalsIgnoreCase("replica")) { failoverAgent = new ReplicaFailover(); } else if (modeValue.equalsIgnoreCase("master")) { failoverAgent = new MasterFailover(); } else if (modeValue.equalsIgnoreCase("hybrid")) { failoverAgent = new HybridFailover(); } else { throw new IllegalArgumentException (modeValue + " is not a legal value for -mode. " + " Only master | replica | hybrid is accepted."); } } private void parseArgs(String args[]) throws Exception { for (int i = 0; i < args.length; i++) { boolean moreArgs = i < args.length - 1; if (args[i].equals("-h") && moreArgs) { envRoot = new File(args[++i]); } else if (args[i].equals("-repGroupSize") && moreArgs) { nNodes = Integer.parseInt(args[++i]); } else if (args[i].equals("-dbSize") && moreArgs) { dbSize = Integer.parseInt(args[++i]); } else if (args[i].equals("-logFileSize") && moreArgs) { logFileSize = Long.parseLong(args[++i]); } else if (args[i].equals("-maxDisk") && moreArgs) { maxDisk = Long.parseLong(args[++i]); } else if (args[i].equals("-steadyTxns") && moreArgs) { steadyTxns = Integer.parseInt(args[++i]); } else if (args[i].equals("-txnsPerRound") && moreArgs) { txnsPerRound = Integer.parseInt(args[++i]); } else if (args[i].equals("-checkpointBytes") && moreArgs) { checkpointBytes = Long.parseLong(args[++i]); } else if (args[i].equals("-mode") && moreArgs) { setFailoverAgent(args[++i]); } else if (args[i].equals("-subDir") && moreArgs) { subDir = Integer.parseInt(args[++i]); } else if (args[i].equals("-offheap") && moreArgs) { offHeap = Boolean.parseBoolean(args[++i]); } else { usage("Unknown arg: " + args[i]); } } if (nNodes <= 2) { throw new IllegalArgumentException ("Replication group size should > 2!"); } if (steadyTxns < txnsPerRound) { throw new IllegalArgumentException ("steadyTxns should be larger than txnsPerRound!"); } if (failoverAgent == null) { usage("-mode must be specified"); } } private void usage(String error) { if (error != null) { System.err.println(error); } System.err.println ("java " + getClass().getName() + "\n" + " [-h ]\n" + " [-mode \n" + " [-repGroupSize ]\n" + " [-dbSize ]\n" + " [-logFileSize ]\n" + " [-checkpointBytes ]\n" + " [-steadyTxns ]\n" + " [-txnsPerRound