je/test/standalone/ClosedDbEviction.java

790 lines
31 KiB
Java
Raw Permalink Normal View History

2021-06-06 17:46:45 +00:00
/*-
* 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: <nTotalTxns> argument should be larger than " +
nTxnPerRecovery + "!");
System.exit(1);
}
printArgs(args);
} catch (IllegalArgumentException e) {
System.out.println
("Usage: ClosedDbEviction [-v] -h <envHome> -init <nDbs>\n" +
"Usage: ClosedDbEviction [-v] -h <envHome> " +
"[-contention <nTotalTxns> | -evict <nTotalTxns>] " +
"[-recovery <nTxnsPerRecovery>]");
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 <offset> to
* <offset + nKeepOpenedDbs - 1>.
*/
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();
}
}