je/test/standalone/IOErrorStress.java

499 lines
16 KiB
Java
Raw 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.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);
}
}
}
}