/*- * 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 com.sleepycat.je.Database; import com.sleepycat.je.DatabaseConfig; import com.sleepycat.je.DatabaseEntry; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; import com.sleepycat.je.EnvironmentStats; import com.sleepycat.je.LockMode; import com.sleepycat.je.OperationStatus; import com.sleepycat.je.StatsConfig; import com.sleepycat.je.utilint.JVMSystemUtils; /** * Applications with a large number of databases, randomly open and close * databases at any time when needed. The mapping tree nodes (roots) in closed * databases won't be evicted from cache immediately. As the applications run * over time, this could cause a lot of waste in cache or even bad performance * and OutOfMemoryError if cache overflows. * * We want to simulate such a scenario to test the efficiency of eviction of * closed databases for SR 13415, to make sure that the eviction would not * cause corruption or concurrency bugs: * + Ensure that concurrency bugs don't occur when multiple threads are trying * to close, evict and open a single database. * + Another potential problem is that the database doesn't open correctly * after being closed and evicted; * + Cache budgeting is not done correctly during eviction or re-loading of * the database after eviction. * */ public class ClosedDbEviction { private static int nDataAccessDbs = 1; private static int nRegularDbs = 100000; private static int nDbRecords = 100; private static int nInitThreads = 8; private static int nContentionThreads = 4; private static int nDbsPerSet = 5; private static int nKeepOpenedDbs = 100; private static int subDir = 3; private static boolean offHeap = false; private static int nOps[] = new int[nContentionThreads]; private static long nTxnPerRecovery = 1000000l; private static long nTotalTxns = 100000000l; private static boolean verbose = false; private static boolean init = false; private static boolean contention = false; private static boolean evict = false; private static boolean recovery = false; private static boolean runDataAccessThread = true; private static String homeDir = "./tmp"; private static Environment env = null; private static Database dataAccessDb = null; private static Database metadataDb = null; private static Database[] openDbList = new Database[nKeepOpenedDbs]; private static Random random = new Random(); private static Runtime rt = Runtime.getRuntime(); public static void main(String[] args) { try { ClosedDbEviction eviction = new ClosedDbEviction(); eviction.start(args); } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } /* Output command-line input arguments to log. */ private void printArgs(String[] args) { System.out.print("\nCommand line arguments:"); for (String arg : args) { System.out.print(' '); System.out.print(arg); } System.out.println(); } void start(String[] args) { try { if (args.length == 0) { throw new IllegalArgumentException(); } /* Parse command-line input arguments. */ for (int i = 0; i < args.length; i++) { String arg = args[i]; String arg2 = (i < args.length - 1) ? args[i + 1] : null; if (arg.equals("-v")) { verbose = true; } else if (arg.equals("-h")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } homeDir = args[++i]; } else if (arg.equals("-init")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } try { nRegularDbs = Integer.parseInt(args[++i]); } catch (NumberFormatException e) { throw new IllegalArgumentException(arg2); } init = true; } else if (arg.equals("-contention")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } try { nTotalTxns = Long.parseLong(args[++i]); } catch (NumberFormatException e) { throw new IllegalArgumentException(arg2); } contention = true; } else if (arg.equals("-evict")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } try { nTotalTxns = Long.parseLong(args[++i]); } catch (NumberFormatException e) { throw new IllegalArgumentException(arg2); } evict = true; } else if (arg.equals("-recovery")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } try { nTxnPerRecovery = Long.parseLong(args[++i]); } catch (NumberFormatException e) { throw new IllegalArgumentException(arg2); } recovery = true; } else if (arg.equals("-subDir")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } try { subDir = Integer.parseInt(args[++i]); } catch (NumberFormatException e) { throw new IllegalArgumentException(arg2); } } else if (arg.equals("-offheap")) { if (arg2 == null) { throw new IllegalArgumentException(arg); } offHeap = Boolean.parseBoolean(args[++i]); } else { throw new IllegalArgumentException(arg); } } /* Correctness self-check: nTotalTxns >= nTxnPerRecovery. */ if (nTotalTxns < nTxnPerRecovery) { System.err.println ("ERROR: argument should be larger than " + nTxnPerRecovery + "!"); System.exit(1); } printArgs(args); } catch (IllegalArgumentException e) { System.out.println ("Usage: ClosedDbEviction [-v] -h -init \n" + "Usage: ClosedDbEviction [-v] -h " + "[-contention | -evict ] " + "[-recovery ]"); e.printStackTrace(); System.exit(1); } try { if (init) { doInit(); } else if (contention) { doContention(); } else if (evict) { doEvict(); } else { System.err.println("No such argument."); System.exit(1); } } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } /** * Initialize nRegularDBs, one dataAccessDb and one metadataDB. */ private void doInit() { class InitThread extends Thread { public int id; private Environment env = null; private Database db = null; /** * Constructor used for initializing databases. */ InitThread(int id, Environment env) { this.id = id; this.env = env; } public void run() { try { DatabaseConfig dbConfig = new DatabaseConfig(); dbConfig.setAllowCreate(true); DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); for (int i = 0; i <= ((nRegularDbs + nDataAccessDbs) / nInitThreads); i++) { int dbId = id + (i * nInitThreads); int totalRecords = nDbRecords; boolean isDataAccessDb = false; String dbName = "db" + dbId; dbConfig.setDeferredWrite(dbId <= (nRegularDbs / 10)); if (dbId >= nRegularDbs) { if (dbId < (nRegularDbs + nDataAccessDbs)) { isDataAccessDb = true; dbName = "dataAccessDb"; totalRecords = 10 * nDbRecords; } else { break; } } /* Open the database. */ db = env.openDatabase(null, dbName, dbConfig); /* Insert totalRecords into database. */ for (int j = 0; j < totalRecords; j++) { key.setData(Integer.toString(j).getBytes("UTF-8")); makeData(data, j, isDataAccessDb); OperationStatus status = db.put(null, key, data); if (status != OperationStatus.SUCCESS) { System.err.println ("ERROR: failed to insert the #" + j + " key/data pair into " + db.getDatabaseName()); System.exit(1); } } db.close(); } } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } /** * Generate the data. nDataAccessDbs should have a bigger size of * data entry; regularDbs only make data entry equal to * (index + "th-dataEntry"). */ private void makeData(DatabaseEntry data, int index, boolean isDataAccessDb) throws Exception { assert (data != null) : "makeData: Null data pointer"; if (isDataAccessDb) { byte[] bytes = new byte[1024]; for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) i; } data.setData(bytes); } else { data.setData((Integer.toString(index) + "th-dataEntry"). getBytes("UTF-8")); } } } /* * Initialize "nRegularDbs" regular Dbs, one dataAccessDb and one * metaDataDb according to these rules: * - The "nRegularDBs" databases, with the dbIds range from * 0 to (nRegularDBs - 1). Each of them would have "nDbRecords". * - 10% of all "nRegularDBs" are deferredWrite databases. * - 90% of all "nRegularDBs" are regular databases. * - The dataAccessDb has "10 * nDbRecords" key/data pairs. * - The metaDataDb is to save "nRegularDbs" info for contention test. */ try { openEnv(128 * 1024 * 1024); saveMetadata(); InitThread[] threads = new InitThread[nInitThreads]; long startTime = System.currentTimeMillis(); for (int i = 0; i < threads.length; i++) { InitThread t = new InitThread(i, env); t.start(); threads[i] = t; } for (int i = 0; i < threads.length; i++) { threads[i].join(); } long endTime = System.currentTimeMillis(); if (verbose) { float elapsedSeconds = (endTime - startTime) / 1000f; float throughput = (nRegularDbs * nDbRecords) / elapsedSeconds; System.out.println ("\nInitialization Statistics Report" + "\n Run starts at: " + (new java.util.Date(startTime)) + ", finishes at: " + (new java.util.Date(endTime)) + "\n Initialized " + nRegularDbs + " databases, " + "each contains " + nDbRecords + " records." + "\n Elapsed seconds: " + elapsedSeconds + ", throughput: " + throughput + " ops/sec."); } closeEnv(); } catch (DatabaseException de) { de.printStackTrace(); System.exit(1); } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } /** * Simulate some contentions to make sure that the eviction would not * cause corruption or concurrency bugs. */ private void doContention() { class ContentionThread extends Thread { public int id; private float dataCheckPossibility = .01f; private long txns; private boolean done = false; private Database currentDb = null; private Database lastOpenedDb = null; /** * Constructor used for initializing databases. */ ContentionThread(int id, long txns) { this.id = id; this.txns = txns; } public void run() { try { /* Start dataAccessThread here. */ startDataAccessor(); /* * All contention threads try to open "nDbsPerSet" DBs * from the same set concurrently. */ while (!done) { int dbId = random.nextInt(nDbsPerSet); currentDb = env.openDatabase(null, "db" + dbId, null); if (lastOpenedDb != null) { lastOpenedDb.close(); } lastOpenedDb = currentDb; if (random.nextFloat() <= dataCheckPossibility) { verifyData(); } nOps[id]++; if (nOps[id] > txns) { if (lastOpenedDb != null) { lastOpenedDb.close(); } done = true; } } /* Stop dataAccessThread here. */ stopDataAccessor(); } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } private void startDataAccessor() { runDataAccessThread = true; } private void stopDataAccessor() { runDataAccessThread = false; } /** * Do the corruption check: just check that the data * that is present looks correct. */ private void verifyData() throws Exception { long dbCount = currentDb.count(); if (dbCount != nDbRecords) { System.err.println ("WARNING: total records in " + currentDb.getDatabaseName() + ": " + dbCount + " doesn't meet the expected value: " + nDbRecords); System.exit(1); } else { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); for (int i = 0; i < nDbRecords; i++) { key.setData(Integer.toString(i).getBytes("UTF-8")); OperationStatus status = currentDb.get(null, key, data, LockMode.DEFAULT); if (status != OperationStatus.SUCCESS) { System.err.println ("ERROR: failed to retrieve the #" + i + " key/data pair from " + currentDb.getDatabaseName()); System.exit(1); } else if (!(new String(data.getData(), "UTF-8")). equals((Integer.toString(i) + "th-dataEntry"))) { System.err.println ("ERROR: current key/data pair: " + i + "/" + (new String(data.getData(), "UTF-8")) + " doesn't match the expected: " + i + "/" + i +"th-dataEntry in " + currentDb.getDatabaseName()); System.exit(1); } } } } } class DataAccessThread extends Thread { public void run() { try { while (runDataAccessThread) { /* Access records to fill up cache. */ DatabaseEntry key = new DatabaseEntry(); key.setData(Integer. toString(random.nextInt(10 * nDbRecords)). getBytes("UTF-8")); DatabaseEntry data = new DatabaseEntry(); OperationStatus status = dataAccessDb.get(null, key, data, LockMode.DEFAULT); if (status != OperationStatus.SUCCESS) { System.err.println ("ERROR: failed to retrieve the #" + new String(key.getData(), "UTF-8") + " key/data pair from dataAccessDb."); System.exit(1); } } } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } } /* * Simulate some contentions according to following rules: * - Several threads try to open/close a set of databases repeatedly. * - The other thread will continually access records from dataAccessDb * to fill up cache. */ try { long startTime = System.currentTimeMillis(); long txns = nTotalTxns; if (recovery) { txns = nTxnPerRecovery; } for (int loop = 0; loop < nTotalTxns / txns; loop++) { /* Clear nOps[] before each run starts. */ for (int i = 0; i < nContentionThreads; i++) { nOps[i] = 0; } openEnv(1024 * 1024); readMetadata(); DataAccessThread dat = new DataAccessThread(); ContentionThread[] threads = new ContentionThread[nContentionThreads]; for (int i = 0; i < threads.length; i++) { ContentionThread t = new ContentionThread(i, txns); t.start(); threads[i] = t; } dat.start(); for (int i = 0; i < threads.length; i++) { threads[i].join(); } dat.join(); if (!checkStats(txns)) { System.err.println ("doContention: stats check failed."); System.exit(1); } closeEnv(); } long endTime = System.currentTimeMillis(); float elapsedSecs = (endTime - startTime) / 1000f; float throughput = nTotalTxns / elapsedSecs; if (verbose) { System.out.println ("\nContention Test Statistics Report" + "\n Starts at: " + (new java.util.Date(startTime)) + ", Finishes at: " + (new java.util.Date(endTime)) + "\n Total operations: " + nTotalTxns + ", Elapsed seconds: " + elapsedSecs + ", Throughput: " + throughput + " ops/sec."); } } catch (DatabaseException de) { de.printStackTrace(); System.exit(1); } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } private void doEvict() { final int offset = random.nextInt(nRegularDbs - nKeepOpenedDbs); class EvictThread extends Thread { public int id; private float dataAccessPossibility = .01f; private long txns = 0; private Database currentDb = null; private Database lastOpenedDb = null; /** * Constructor. */ public EvictThread(int id, long txns) { this.id = id; this.txns = txns; } public void run() { try { int dbId; boolean done = false; while (!done) { dbId = random.nextInt(nRegularDbs); if ((0 <= (dbId - offset)) && ((dbId - offset) < nKeepOpenedDbs)) { /* * Randomly select nKeepOpenedDbs databases opened * in a time. The dbId ranges from to * . */ if (openDbList[dbId - offset] == null) { openDbList[dbId - offset] = env.openDatabase(null, "db" + dbId, null); } } else { /* Each thread select randomly from all DBs. */ currentDb = env.openDatabase(null, "db" + dbId, null); if (random.nextFloat() < dataAccessPossibility) { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); key.setData(Integer.toString (random.nextInt(nDbRecords)). getBytes("UTF-8")); currentDb.get(null, key, data, LockMode.DEFAULT); } if (lastOpenedDb != null) { lastOpenedDb.close(); } lastOpenedDb = currentDb; } nOps[id]++; if (nOps[id] > txns) { if (lastOpenedDb != null) { lastOpenedDb.close(); } /* Close nKeepOpenedDbs before exit. */ for (int i = 0; i < nKeepOpenedDbs; i++) { currentDb = openDbList[i]; if (currentDb != null) { currentDb.close(); openDbList[i] = null; } } done = true; } } } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } } /* * Simulate some contentions according to following rules: * - Several threads try to open/close a set of databases repeatedly. * - The other thread will continually access records from dataAccessDb * to fill up cache. */ try { long startTime = System.currentTimeMillis(); long txns = nTotalTxns; if (recovery) { txns = nTxnPerRecovery; } for (int loop = 0; loop < nTotalTxns / txns; loop++) { /* Clear nOps[] before each run starts. */ for (int i = 0; i < nContentionThreads; i++) { nOps[i] = 0; } /* When using Zing JDK, the cache size should be increased. */ if (JVMSystemUtils.ZING_JVM) { openEnv(8 * 1024 * 1024); } else { openEnv(512 * 1024); } readMetadata(); EvictThread[] threads = new EvictThread[nContentionThreads]; for (int i = 0; i < threads.length; i++) { EvictThread t = new EvictThread(i, txns); t.start(); threads[i] = t; } for (int i = 0; i < threads.length; i++) { threads[i].join(); } if (!checkStats(txns)) { System.err.println("doEvict: stats check failed."); System.exit(1); } closeEnv(); } long endTime = System.currentTimeMillis(); if (verbose) { float elapsedSeconds = (endTime - startTime) / 1000f; float throughput = nTotalTxns / elapsedSeconds; System.out.println ("\nEviction Test Statistics Report" + "\n Run starts at: " + (new java.util.Date(startTime)) + ", finishes at: " + (new java.util.Date(endTime)) + "\n Total operations: " + nTotalTxns + ", Elapsed seconds: " + elapsedSeconds + ", Throughput: " + throughput + " ops/sec."); } } catch (DatabaseException de) { de.printStackTrace(); System.exit(1); } catch (Throwable e) { e.printStackTrace(); System.exit(1); } } /** * Open an Environment. */ private void openEnv(long cacheSize) throws DatabaseException { EnvironmentConfig envConfig = new EnvironmentConfig(); envConfig.setAllowCreate(true); envConfig.setCacheSize(cacheSize); if (offHeap) { /* Do not reduce main cache size, test will run too slowly. */ envConfig.setOffHeapCacheSize(cacheSize); } if (subDir > 0) { envConfig.setConfigParam (EnvironmentConfig.LOG_N_DATA_DIRECTORIES, subDir + ""); Utils.createSubDirs(new File(homeDir), subDir, true); } env = new Environment(new File(homeDir), envConfig); if (contention) { dataAccessDb = env.openDatabase(null, "dataAccessDb", null); } } /** * Check to see if stats looks correct. */ private boolean checkStats(long txns) throws DatabaseException { /* Get EnvironmentStats numbers. */ StatsConfig statsConfig = new StatsConfig(); statsConfig.setFast(true); statsConfig.setClear(true); EnvironmentStats stats = env.getStats(statsConfig); long evictedINs = stats.getNNodesExplicitlyEvicted(); long evictedRoots = stats.getNRootNodesEvicted(); long dataBytes = stats.getDataBytes(); /* Check the eviction of INs and ROOTs actually happens. */ boolean nodesCheck = (evictedINs > 0); boolean rootsCheck = (evictedRoots > 0); if (verbose) { System.out.printf ("\n\tEviction Statistics(calc txns: %d)%n" + " Data Pass/Fail%n" + " ---------- ---------%n" + "EvictedINs: %10d %9S%n" + "EvictedRoots:%10d %9S%n" + "DataBytes: %10d%n" + "jvm.maxMem: %10d%n" + "jvm.freeMem: %10d%n" + "jvm.totlMem: %10d%n", txns, evictedINs, (nodesCheck ? "PASS" : "FAIL"), evictedRoots, (rootsCheck ? "PASS" : "FAIL"), dataBytes, rt.maxMemory(), rt.freeMemory(), rt.totalMemory()); System.out.println ("The test criteria: EvictedINs > 0, EvictedRoots > 0."); } return nodesCheck && rootsCheck; } /** * Close the Databases and Environment. */ private void closeEnv() throws DatabaseException { if (dataAccessDb != null) { dataAccessDb.close(); } if (env != null) { env.close(); } } /** * Store meta-data information into metadataDb. */ private void saveMetadata() throws Exception { /* Store meta-data information into one additional database. */ DatabaseConfig dbConfig = new DatabaseConfig(); dbConfig.setAllowCreate(true); metadataDb = env.openDatabase(null, "metadataDb", dbConfig); OperationStatus status = metadataDb.put(null, new DatabaseEntry("nRegularDbs".getBytes("UTF-8")), new DatabaseEntry(Integer. toString(nRegularDbs). getBytes("UTF-8"))); if (status != OperationStatus.SUCCESS) { System.err.println ("Not able to save info into the metadata database."); System.exit(1); } metadataDb.close(); } /** * Retrieve meta-data information from metadataDb. */ private void readMetadata() throws Exception { /* Retrieve meta-data information from metadataDB. */ metadataDb = env.openDatabase(null, "metadataDb", null); DatabaseEntry key = new DatabaseEntry("nRegularDbs".getBytes("UTF-8")); DatabaseEntry data = new DatabaseEntry(); OperationStatus status = metadataDb.get(null, key, data, LockMode.DEFAULT); if (status != OperationStatus.SUCCESS) { System.err.println ("Couldn't retrieve info from the metadata database."); System.exit(1); } nRegularDbs = Integer.parseInt(new String (data.getData(), "UTF-8")); metadataDb.close(); } }