/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.test.synth;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import org.h2.test.TestAll;
import org.h2.test.TestBase;
import org.h2.test.utils.SelfDestructor;
import org.h2.tools.Backup;
import org.h2.tools.DeleteDbFiles;
import org.h2.util.IOUtils;
import org.h2.util.StringUtils;
/**
* Tests database recovery by destroying a process that writes to the database.
*/
public abstract class TestHalt extends TestBase {
/**
* This bit flag means insert operations should be performed.
*/
protected static final int OP_INSERT = 1;
/**
* This bit flag means delete operations should be performed.
*/
protected static final int OP_DELETE = 2;
/**
* This bit flag means update operations should be performed.
*/
protected static final int OP_UPDATE = 4;
/**
* This bit flag means select operations should be performed.
*/
protected static final int OP_SELECT = 8;
/**
* This bit flag means operations should be written to the transaction log
* immediately.
*/
protected static final int FLAG_NO_DELAY = 1;
/**
* This bit flag means the test should use LOB values.
*/
protected static final int FLAG_LOBS = 2;
private static final String DATABASE_NAME = "halt";
private static final String TRACE_FILE_NAME = "haltTrace.trace.db";
/**
* The current operations bit mask.
*/
protected int operations;
/**
* The current flags bit mask.
*/
protected int flags;
/**
* The current test value, for example the number of rows.
*/
protected int value;
/**
* The database connection.
*/
protected Connection conn;
/**
* The pseudo random number generator used for this test.
*/
protected Random random = new Random();
private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss ");
private int errorId;
private int sequenceId;
/**
* Initialize the test.
*/
abstract void controllerInit() throws SQLException;
/**
* Check if the database is consistent after a simulated database crash.
*/
abstract void controllerCheckAfterCrash() throws SQLException;
/**
* Wait for some time after the application has been started.
*/
abstract void controllerWaitAfterAppStart() throws Exception;
/**
* Start the application.
*/
abstract void processAppStart() throws SQLException;
/**
* Run the application.
*/
abstract void processAppRun() throws SQLException;
public void test() {
for (int i = 0;; i++) {
operations = OP_INSERT | i;
flags = i >> 4;
// flags |= FLAG_NO_DELAY; // | FLAG_LOBS;
try {
controllerTest();
} catch (Throwable t) {
System.out.println("Error: " + t);
t.printStackTrace();
}
}
}
Connection getConnection() throws SQLException {
org.h2.Driver.load();
String url = "jdbc:h2:" + getBaseDir() + "/halt";
// String url = "jdbc:h2:" + baseDir + "/halt;TRACE_LEVEL_FILE=3";
return DriverManager.getConnection(url, "sa", "sa");
}
/**
* The second process starts the application and executes random operations.
*/
void processRunRandom() throws SQLException {
connect();
try {
traceOperation("connected, operations:" + operations + " flags:" + flags + " value:" + value);
processAppStart();
System.out.println("READY");
System.out.println("READY");
System.out.println("READY");
processAppRun();
traceOperation("done");
} catch (Exception e) {
traceOperation("run", e);
}
disconnect();
}
private void connect() throws SQLException {
try {
traceOperation("connecting");
conn = getConnection();
} catch (SQLException e) {
traceOperation("connect", e);
e.printStackTrace();
throw e;
}
}
/**
* Print a trace message to the trace file.
*
* @param s the message
*/
protected void traceOperation(String s) {
traceOperation(s, null);
}
/**
* Print a trace message to the trace file.
*
* @param s the message
* @param e the exception or null
*/
protected void traceOperation(String s, Exception e) {
FileWriter writer = null;
try {
File f = new File(getBaseDir() + "/" + TRACE_FILE_NAME);
f.getParentFile().mkdirs();
writer = new FileWriter(f, true);
PrintWriter w = new PrintWriter(writer);
s = dateFormat.format(new Date()) + ": " + s;
w.println(s);
if (e != null) {
e.printStackTrace(w);
}
} catch (IOException e2) {
e2.printStackTrace();
} finally {
IOUtils.closeSilently(writer);
}
}
/**
* Run one test. The controller starts the process, waits, kills the
* process, and checks if everything is ok.
*/
void controllerTest() throws Exception {
traceOperation("delete database -----------------------------");
DeleteDbFiles.execute(getBaseDir(), DATABASE_NAME, true);
new File(getBaseDir() + "/" + TRACE_FILE_NAME).delete();
connect();
controllerInit();
disconnect();
for (int i = 0; i < 10; i++) {
traceOperation("backing up " + sequenceId);
Backup.execute(getBaseDir() + "/haltSeq" + sequenceId + ".zip", getBaseDir(), null, true);
sequenceId++;
// int operations = OP_INSERT;
// OP_DELETE = 1, OP_UPDATE = 2, OP_SELECT = 4;
// int flags = FLAG_NODELAY;
// FLAG_NO_DELAY = 1, FLAG_AUTO_COMMIT = 2, FLAG_SMALL_CACHE = 4;
int testValue = random.nextInt(1000);
// for Derby and HSQLDB
// String classPath = "-cp
// .;D:/data/java/hsqldb.jar;D:/data/java/derby.jar";
String selfDestruct = SelfDestructor.getPropertyString(60);
String[] procDef = { "java", selfDestruct,
"-cp", getClassPath(),
getClass().getName(), "" + operations, "" + flags, "" + testValue};
traceOperation("start: " + StringUtils.arrayCombine(procDef, ' '));
Process p = Runtime.getRuntime().exec(procDef);
InputStream in = p.getInputStream();
OutputCatcher catcher = new OutputCatcher(in);
catcher.start();
String s = catcher.readLine(5 * 60 * 1000);
if (s == null) {
throw new IOException("No reply from process, command: " + StringUtils.arrayCombine(procDef, ' '));
} else if (s.startsWith("READY")) {
traceOperation("got reply: " + s);
}
controllerWaitAfterAppStart();
p.destroy();
p.waitFor();
try {
traceOperation("backing up " + sequenceId);
Backup.execute(getBaseDir() + "/haltSeq" + sequenceId + ".zip", getBaseDir(), null, true);
// new File(BASE_DIR + "/haltSeq" + (sequenceId-20) +
// ".zip").delete();
connect();
controllerCheckAfterCrash();
} catch (Exception e) {
File zip = new File(getBaseDir() + "/haltSeq" + sequenceId + ".zip");
File zipId = new File(getBaseDir() + "/haltSeq" + sequenceId + "-" + errorId + ".zip");
zip.renameTo(zipId);
printTime("ERROR: " + sequenceId + " " + errorId + " " + e.toString());
e.printStackTrace();
errorId++;
} finally {
sequenceId++;
disconnect();
}
}
}
/**
* Close the database connection normally.
*/
protected void disconnect() {
try {
traceOperation("disconnect");
conn.close();
} catch (Exception e) {
traceOperation("disconnect", e);
}
}
// public Connection getConnectionHSQLDB() throws SQLException {
// File lock = new File("test.lck");
// while (lock.exists()) {
// lock.delete();
// System.gc();
// }
// Class.forName("org.hsqldb.jdbcDriver");
// return DriverManager.getConnection("jdbc:hsqldb:test", "sa", "");
// }
// public Connection getConnectionDerby() throws SQLException {
// File lock = new File("test3/db.lck");
// while (lock.exists()) {
// lock.delete();
// System.gc();
// }
// Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
// try {
// return DriverManager.getConnection(
// "jdbc:derby:test3;create=true", "sa", "sa");
// } catch (SQLException e) {
// Exception e2 = e;
// do {
// e.printStackTrace();
// e = e.getNextException();
// } while (e != null);
// throw e2;
// }
// }
// void disconnectHSQLDB() {
// try {
// conn.createStatement().execute("SHUTDOWN");
// } catch (Exception e) {
// // ignore
// }
// // super.disconnect();
// }
// void disconnectDerby() {
// // super.disconnect();
// try {
// Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
// DriverManager.getConnection(
// "jdbc:derby:;shutdown=true", "sa", "sa");
// } catch (Exception e) {
// // ignore
// }
// }
/**
* Create a random string with the specified length.
*
* @param len the number of characters
* @return the random string
*/
protected String getRandomString(int len) {
StringBuilder buff = new StringBuilder();
for (int i = 0; i < len; i++) {
buff.append('a' + random.nextInt(20));
}
return buff.toString();
}
public TestBase init(TestAll conf) throws Exception {
super.init(conf);
return this;
}
}