mirror of
https://github.com/berkeleydb/je.git
synced 2024-11-15 01:46:24 +00:00
498 lines
16 KiB
Java
498 lines
16 KiB
Java
/*-
|
||
* 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.io.FileNotFoundException;
|
||
import java.io.IOError;
|
||
import java.io.IOException;
|
||
import java.io.InterruptedIOException;
|
||
import java.io.RandomAccessFile;
|
||
import java.io.SyncFailedException;
|
||
import java.util.Random;
|
||
import java.util.concurrent.atomic.AtomicBoolean;
|
||
|
||
import com.sleepycat.bind.tuple.IntegerBinding;
|
||
import com.sleepycat.je.CheckpointConfig;
|
||
import com.sleepycat.je.Database;
|
||
import com.sleepycat.je.DatabaseConfig;
|
||
import com.sleepycat.je.DatabaseEntry;
|
||
import com.sleepycat.je.Durability;
|
||
import com.sleepycat.je.Environment;
|
||
import com.sleepycat.je.EnvironmentConfig;
|
||
import com.sleepycat.je.ExceptionEvent;
|
||
import com.sleepycat.je.ExceptionListener;
|
||
import com.sleepycat.je.LockConflictException;
|
||
import com.sleepycat.je.Transaction;
|
||
import com.sleepycat.je.log.FileManager;
|
||
import com.sleepycat.je.util.DbVerify;
|
||
import com.sleepycat.je.util.DbVerifyLog;
|
||
import com.sleepycat.je.utilint.TracerFormatter;
|
||
|
||
/**
|
||
* A stress test to simulate IO errors (exceptions thrown by RandomAccessFile)
|
||
* to ensure log corruption does not occur.
|
||
*/
|
||
public class IOErrorStress {
|
||
|
||
private static final String EXCEPTION_MSG = "Generated by IOErrorStress";
|
||
private static final TracerFormatter DATE_FORMAT = new TracerFormatter();
|
||
private static final String DB_NAME = "foo";
|
||
private static final int DEFAULT_LOAD_THREADS = 10;
|
||
private static final int DEFAULT_CLEANER_THREADS = 2;
|
||
private static final int DEFAULT_DURATION_MINUTES = 15;
|
||
private static final int DEFAULT_CACHE_MB = 10;
|
||
private static final String DEFAULT_HOME_DIR = "tmp";
|
||
|
||
public static void main(final String[] args) {
|
||
try {
|
||
printArgs(args);
|
||
final IOErrorStress test = new IOErrorStress(args);
|
||
test.runTest();
|
||
System.exit(0);
|
||
} catch (Throwable e) {
|
||
e.printStackTrace(System.out);
|
||
System.exit(-1);
|
||
}
|
||
}
|
||
|
||
private Environment env;
|
||
private Database db;
|
||
private String homeDir = DEFAULT_HOME_DIR;
|
||
private int nLoadThreads = DEFAULT_LOAD_THREADS;
|
||
private int nCleanerThreads = DEFAULT_CLEANER_THREADS;
|
||
private int durationMinutes = DEFAULT_DURATION_MINUTES;
|
||
private int cacheMb = DEFAULT_CACHE_MB;
|
||
private volatile boolean injectErrors = false;
|
||
private final Random[] loadRandoms;
|
||
|
||
private IOErrorStress(String[] args) {
|
||
|
||
/* Parse arguments. */
|
||
for (int i = 0; i < args.length; i += 1) {
|
||
final String arg = args[i];
|
||
final boolean moreArgs = i < args.length - 1;
|
||
if (arg.equals("-h") && moreArgs) {
|
||
homeDir = args[++i];
|
||
} else if (arg.equals("-threads") && moreArgs) {
|
||
nLoadThreads = Integer.parseInt(args[++i]);
|
||
} else if (arg.equals("-cleaners") && moreArgs) {
|
||
nCleanerThreads = Integer.parseInt(args[++i]);
|
||
} else if (arg.equals("-minutes") && moreArgs) {
|
||
durationMinutes = Integer.parseInt(args[++i]);
|
||
} else if (arg.equals("-cacheMB") && moreArgs) {
|
||
cacheMb = Integer.parseInt(args[++i]);
|
||
} else {
|
||
throw new IllegalArgumentException("Unknown arg: " + arg);
|
||
}
|
||
}
|
||
|
||
/* Initialize Random generators for load threads. */
|
||
loadRandoms = new Random[nLoadThreads];
|
||
for (int i = 0; i < nLoadThreads; i += 1) {
|
||
loadRandoms[i] = new Random(i * 1000);
|
||
}
|
||
|
||
/* Set factory for generating IO errors. Is initially disabled. */
|
||
FileManager.fileFactory = new FileManager.FileFactory() {
|
||
|
||
@Override
|
||
public RandomAccessFile createFile(final File envHome,
|
||
final String name,
|
||
final String mode)
|
||
throws FileNotFoundException {
|
||
|
||
return new ErrorInjector(name, mode);
|
||
}
|
||
};
|
||
}
|
||
|
||
private static void printArgs(String[] args) {
|
||
System.out.print("\nCommand line arguments:");
|
||
for (String arg : args) {
|
||
System.out.print(' ');
|
||
System.out.print(arg);
|
||
}
|
||
System.out.println();
|
||
}
|
||
|
||
private void runTest()
|
||
throws Throwable {
|
||
|
||
final long durationMs = durationMinutes * 60 * 1000;
|
||
final long endTime = System.currentTimeMillis() + durationMs;
|
||
|
||
/*
|
||
* Create env before starting to inject errors. Problems in the code
|
||
* path for the initial env creation are not as important to test, and
|
||
* these failures would prevent the test from proceeding.
|
||
*/
|
||
openEnv();
|
||
env.close();
|
||
env = null;
|
||
|
||
injectErrors = true;
|
||
while (System.currentTimeMillis() < endTime) {
|
||
try {
|
||
log("Open environment");
|
||
openEnv();
|
||
log("Verify logs...");
|
||
try {
|
||
final DbVerifyLog verifier = new DbVerifyLog(env);
|
||
verifier.verifyAll();
|
||
} catch (final Throwable e) {
|
||
log("***DbVerifyLog failed!");
|
||
throw e;
|
||
}
|
||
log("Verify database...");
|
||
injectErrors = false;
|
||
try {
|
||
final DbVerify verifier =
|
||
new DbVerify(env, DB_NAME, false /*quiet*/);
|
||
if (!verifier.verify(System.out)) {
|
||
throw new IllegalStateException
|
||
("***DbVerify failed: result is false!");
|
||
}
|
||
} catch (final Throwable e) {
|
||
log("***DbVerify failed: see exception!");
|
||
throw e;
|
||
} finally {
|
||
injectErrors = true;
|
||
}
|
||
openDb();
|
||
log("Run operations...");
|
||
if (runLoadThreads()) {
|
||
throw new IllegalStateException
|
||
("***Load threads failed: see exception(s)!");
|
||
}
|
||
closeDbAndEnv();
|
||
} catch (final Throwable e) {
|
||
if (!isGeneratedException(e)) {
|
||
log("***Unexpected exception!");
|
||
throw e;
|
||
}
|
||
log("Generated exception, continuing...");
|
||
e.printStackTrace(System.out);
|
||
closeDbAndEnv();
|
||
}
|
||
}
|
||
|
||
log("Test succeeded: no errors after " + durationMinutes + " minutes");
|
||
}
|
||
|
||
private boolean runLoadThreads() {
|
||
|
||
final AtomicBoolean unexpectedEx = new AtomicBoolean(false);
|
||
final int nThreads = loadRandoms[0].nextInt(nLoadThreads) + 1;
|
||
final Thread[] threads = new Thread[nThreads];
|
||
|
||
for (int i = 0; i < nThreads; i += 1) {
|
||
final Random rnd = loadRandoms[i];
|
||
threads[i] = new Thread() {
|
||
@Override
|
||
public void run() {
|
||
if (runLoad(rnd)) {
|
||
unexpectedEx.set(true);
|
||
}
|
||
}
|
||
};
|
||
threads[i].start();
|
||
}
|
||
|
||
for (int i = 0; i < nThreads; i += 1) {
|
||
try {
|
||
threads[i].join();
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace(System.out);
|
||
unexpectedEx.set(true);
|
||
}
|
||
}
|
||
|
||
return unexpectedEx.get();
|
||
}
|
||
|
||
/**
|
||
* Perform DB writes until an exception is thrown.
|
||
*
|
||
* @return true if an unexpected exception was thrown.
|
||
*/
|
||
private boolean runLoad(Random rnd) {
|
||
|
||
final DatabaseEntry key = new DatabaseEntry();
|
||
final DatabaseEntry value = new DatabaseEntry();
|
||
|
||
boolean unexpectedEx = false;
|
||
|
||
while (true) {
|
||
try {
|
||
IntegerBinding.intToEntry(rnd.nextInt(100000), key);
|
||
IntegerBinding.intToEntry(rnd.nextInt(), value);
|
||
|
||
final Transaction txn = env.beginTransaction(null, null);
|
||
try {
|
||
db.put(txn, key, value);
|
||
txn.commit(rnd.nextBoolean() ?
|
||
Durability.COMMIT_NO_SYNC :
|
||
Durability.COMMIT_WRITE_NO_SYNC);
|
||
} finally {
|
||
txn.abort();
|
||
}
|
||
|
||
if (rnd.nextInt(1000) == 0) {
|
||
env.checkpoint(new CheckpointConfig().setForce(true));
|
||
}
|
||
} catch (LockConflictException e) {
|
||
log("Lock conflict, continuing...");
|
||
e.printStackTrace(System.out);
|
||
} catch (Throwable e) {
|
||
if (isGeneratedException(e)) {
|
||
log("Generated exception, continuing...");
|
||
e.printStackTrace(System.out);
|
||
} else {
|
||
log("***Unexpected exception!");
|
||
e.printStackTrace(System.out);
|
||
unexpectedEx = true;
|
||
}
|
||
if (!env.isValid()) {
|
||
return unexpectedEx;
|
||
}
|
||
/*
|
||
* Continue to see if log becomes corrupted by writing in an
|
||
* environment where a problem occurred but the environment was
|
||
* not invalidated.
|
||
*/
|
||
}
|
||
}
|
||
}
|
||
|
||
private static boolean isGeneratedException(Throwable e) {
|
||
while (e != null) {
|
||
if (e.getMessage() != null &&
|
||
e.getMessage().contains(EXCEPTION_MSG)) {
|
||
return true;
|
||
}
|
||
e = e.getCause();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private void openEnv() {
|
||
if (env != null) {
|
||
throw new IllegalStateException();
|
||
}
|
||
final EnvironmentConfig config = new EnvironmentConfig();
|
||
config.setAllowCreate(true);
|
||
config.setTransactional(true);
|
||
config.setConfigParam(EnvironmentConfig.CLEANER_THREADS,
|
||
String.valueOf(nCleanerThreads));
|
||
config.setCacheSize(cacheMb * (1024 * 1024));
|
||
|
||
/* Temporary measures to try to find the cause of corruptions. */
|
||
// config.setConfigParam(EnvironmentConfig.LOG_VERIFY_CHECKSUMS, "true");
|
||
// DbInternal.setCreateEP(config, false);
|
||
|
||
config.setExceptionListener(new ExceptionListener() {
|
||
@Override
|
||
public void exceptionThrown(final ExceptionEvent event) {
|
||
if (isGeneratedException(event.getException())) {
|
||
log("Generated exception, continuing..." +
|
||
"\n" + event);
|
||
} else {
|
||
log("***Unexpected exception!" +
|
||
"\n" + event);
|
||
System.exit(-1);
|
||
}
|
||
}
|
||
});
|
||
|
||
env = new Environment(new File(homeDir), config);
|
||
}
|
||
|
||
private void openDb() {
|
||
if (db != null) {
|
||
throw new IllegalStateException();
|
||
}
|
||
final DatabaseConfig config = new DatabaseConfig();
|
||
config.setAllowCreate(true);
|
||
config.setTransactional(true);
|
||
db = env.openDatabase(null, DB_NAME, config);
|
||
}
|
||
|
||
private void closeDbAndEnv() {
|
||
if (db != null) {
|
||
try {
|
||
db.close();
|
||
} catch (final Throwable e) {
|
||
log("Ignoring exception closing db...");
|
||
e.printStackTrace(System.out);
|
||
} finally {
|
||
db = null;
|
||
}
|
||
}
|
||
if (env != null) {
|
||
try {
|
||
log("Closing environment...");
|
||
env.close();
|
||
} catch (final Throwable e) {
|
||
log("Ignoring exception closing env...");
|
||
e.printStackTrace(System.out);
|
||
} finally {
|
||
env = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void log(final String msg) {
|
||
System.out.println(DATE_FORMAT.getDate(System.currentTimeMillis()) +
|
||
" " + msg);
|
||
}
|
||
|
||
private class ErrorInjector extends FileManager.DefaultRandomAccessFile {
|
||
|
||
/* The larger the value, the less likely an error is generated. */
|
||
private static final int ERROR_PROPABILITY = 1000000;
|
||
|
||
private final Random rnd = new Random();
|
||
|
||
public ErrorInjector(final String name, final String mode)
|
||
throws FileNotFoundException {
|
||
|
||
super(name, mode);
|
||
}
|
||
|
||
@Override
|
||
public void close()
|
||
throws IOException {
|
||
|
||
generateError();
|
||
super.close();
|
||
}
|
||
|
||
@Override
|
||
public long getFilePointer()
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.getFilePointer();
|
||
}
|
||
|
||
|
||
@Override
|
||
public long length()
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.length();
|
||
}
|
||
|
||
@Override
|
||
public int read()
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.read();
|
||
}
|
||
|
||
@Override
|
||
public int read(final byte[] b,
|
||
final int off,
|
||
final int len)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.read(b, off, len);
|
||
}
|
||
|
||
@Override
|
||
public int read(final byte[] b)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.read(b);
|
||
}
|
||
|
||
@Override
|
||
public void seek(final long pos)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
super.seek(pos);
|
||
}
|
||
|
||
@Override
|
||
public void setLength(final long newLength)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
super.setLength(newLength);
|
||
}
|
||
|
||
@Override
|
||
public int skipBytes(final int n)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
return super.skipBytes(n);
|
||
}
|
||
|
||
@Override
|
||
public void write(final byte[] b)
|
||
throws IOException {
|
||
|
||
write(b, 0, b.length);
|
||
}
|
||
|
||
@Override
|
||
public void write(final byte[] b,
|
||
final int off,
|
||
final int len)
|
||
throws IOException {
|
||
|
||
for (int i = off; i < off + len; ++i) {
|
||
write(b[i]);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void write(final int b)
|
||
throws IOException {
|
||
|
||
generateError();
|
||
super.write(b);
|
||
}
|
||
|
||
private void generateError()
|
||
throws IOException {
|
||
|
||
if (!injectErrors) {
|
||
return;
|
||
}
|
||
|
||
switch (rnd.nextInt(ERROR_PROPABILITY)) {
|
||
case 0:
|
||
throw new IOException(EXCEPTION_MSG);
|
||
case 10:
|
||
throw new IOError(new IOException(EXCEPTION_MSG));
|
||
case 20:
|
||
throw new OutOfMemoryError(EXCEPTION_MSG);
|
||
case 30:
|
||
throw new SyncFailedException(EXCEPTION_MSG);
|
||
case 40:
|
||
throw new InterruptedIOException(EXCEPTION_MSG);
|
||
case 50:
|
||
throw new RuntimeException(EXCEPTION_MSG);
|
||
case 60:
|
||
throw new Error(EXCEPTION_MSG);
|
||
}
|
||
}
|
||
}
|
||
}
|