/*- * 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); } } } }