Hello Thomas,

I'm encountering a consistent H2 corruption problem. From my testing, I 
believe it involves the caching of select distinct query results. I am 
seeing corruption occur consistently when a table reaches 
MAX_MEMORY_ROWS_DISTINCT (default 10k) and a select distinct with inner 
join is done on the table. Changing the value of this setting reliably 
changes the corruption point.

Attached is the smallest reproducible test case I could create. The 
MAX_MEMORY_ROWS_DISTINCT can be set below 500 but due to timing it may not 
corrupt. Albeit nonsensical, the join on the second table in conjunction 
with the select distinct appears a necessary factor in reproducing the bug. 
The corruption always occurs after the maximum number of rows exceeds 
MAX_MEMORY_ROWS_DISTINCT.

The corruption has been encountered on versions 1.3.163/170/172 of H2. Once 
the database has reached this state it cannot be dumped.
 
Thanks

-- 
You received this message because you are subscribed to the Google Groups "H2 
Database" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/h2-database?hl=en-US.
For more options, visit https://groups.google.com/groups/opt_out.


import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.atomic.AtomicBoolean;

import org.h2.jdbc.JdbcSQLException;

/**
 * A repeatable test case of corruption surrounding the MAX_MEMORY_ROWS_DISTINCT setting of H2
 * 
 */
public class H2CorruptionTest {

	private static final String TEST_DB = "./h2.test";
	private static final String TEST_TABLE_NAME = "t_tbl1";
	private static final String TEST_TABLE_NAME2 = "t_tbl2";
	private static final Integer MEMORY_DISTINCT_ROWS = 500;

	private static final int MAX_CONNECTION_ATTEMPTS = 3;

	private static int ageCounter = 0;

	private static AtomicBoolean finished = new AtomicBoolean(false);

	public static void doWork() throws Exception {
		File dbFile = new File(TEST_DB + ".h2.db");
		if (dbFile.delete()) {
			System.out.println("Deleted old database");
		}

		Connection conn = openConnection();
		conn.setAutoCommit(false);

		addTable(conn);

		startSelectThread();

		for (int i = 1; i <= MEMORY_DISTINCT_ROWS * 4; ++i) {
			if (i % 10 == 0) {
				System.out.println("Inserted " + i + " rows");
			}

			beginTrans(conn);
			try {
				// Insert a new row
				doInsert(conn);
				commitTrans(conn);

			} catch (Exception e) {
				executeSQL("ROLLBACK;", conn);

				throw new RuntimeException(e);
			}
		}

		finished.set(true);
	}

	private static void beginTrans(Connection conn) throws Exception {
		executeSQL("BEGIN;", conn);
	}

	private static void commitTrans(Connection conn) throws Exception {
		executeSQL("COMMIT;", conn);
	}

	private static void rollbackTrans(Connection conn) throws Exception {
		executeSQL("ROLLBACK;", conn);
	}

	private static Thread startSelectThread() {
		Thread t = new Thread() {

			@Override
			public void run() {
				System.out.println("\n\n---- Starting select thread ----");
				try {
					Connection conn = openConnection();

					while (!finished.get()) {
						beginTrans(conn);
						try {
							doDistinctSelect(conn);
							commitTrans(conn);
						} finally {
							rollbackTrans(conn);
						}
					} // End while
				} catch (Throwable t) {
					System.err.println("Error in select thread: " + t);
					throw new RuntimeException(t);
				} finally {
					System.out.println("---- Select thread exiting ----");
				}
			}
		};

		t.setDaemon(true);
		t.start();
		return t;
	}

	private static void doInsert(Connection conn) throws Exception {
		StringBuilder sqlBuilder = new StringBuilder();
		sqlBuilder.append("INSERT INTO ").append(TEST_TABLE_NAME);
		sqlBuilder.append(" (age) values (");
		sqlBuilder.append(ageCounter++ % (MEMORY_DISTINCT_ROWS * 2)).append(")");

		executeSQL(sqlBuilder.toString(), conn);

		sqlBuilder = new StringBuilder();
		sqlBuilder.append("INSERT INTO ").append(TEST_TABLE_NAME2);
		sqlBuilder.append(" (age_fk) values (");
		sqlBuilder.append(ageCounter % (MEMORY_DISTINCT_ROWS * 2)).append(")");

		executeSQL(sqlBuilder.toString(), conn);
	}

	private static int doDistinctSelect(Connection conn) throws Exception {
		String sql = "SELECT distinct(age) FROM " + TEST_TABLE_NAME //
				+ " INNER JOIN " + TEST_TABLE_NAME2 //
				+ " ON age=age_fk";

		PreparedStatement ps = conn.prepareStatement(sql);
		ResultSet rs = ps.executeQuery();

		try {
			rs.getArray(1);
		} catch (JdbcSQLException e) {
			// Intentionally empty;
		}

		rs.last();
		int numRows = rs.getRow();

		ps.close();

		return numRows;
	}

	private static void addTable(Connection conn) throws Exception {
		StringBuilder sqlBuilder = new StringBuilder();
		sqlBuilder.append("CREATE TABLE ").append(TEST_TABLE_NAME);
		sqlBuilder.append(" (id INT PRIMARY KEY AUTO_INCREMENT NOT NULL,");
		sqlBuilder.append("age INTEGER NOT NULL)");

		executeSQL(sqlBuilder.toString(), conn);

		sqlBuilder = new StringBuilder();
		sqlBuilder.append("CREATE TABLE ").append(TEST_TABLE_NAME2);
		sqlBuilder.append(" (id2 INT PRIMARY KEY AUTO_INCREMENT NOT NULL,");
		sqlBuilder.append("age_fk INTEGER NOT NULL)");

		executeSQL(sqlBuilder.toString(), conn);
	}

	private static Connection openConnection() throws Exception {
		Connection conn = null;

		String url = "jdbc:h2:"
				+ TEST_DB //
				+ ";DEFAULT_LOCK_TIMEOUT=30000;WRITE_DELAY=0;ACCESS_MODE_DATA=rwd;DB_CLOSE_ON_EXIT=FALSE;MAX_MEMORY_ROWS_DISTINCT=" //
				+ MEMORY_DISTINCT_ROWS;

		int retries = 0;
		Exception failedConnectionException = null;
		while (conn == null && retries < MAX_CONNECTION_ATTEMPTS) {
			try {
				System.out.println("Connecting to database: " + TEST_DB);
				System.out.println("Connection url: " + url);
				conn = DriverManager.getConnection(url, "test", "pass");
				failedConnectionException = null;
			} catch (Exception e) {
				failedConnectionException = e;
				System.err.println("  connection denied. cause: " + e.getMessage());
			} finally {
				retries++;
			}
		}

		// bail out if necessary
		if (failedConnectionException != null) {
			throw failedConnectionException;
		}

		return conn;
	}

	private static void executeSQL(String sql, Connection conn) throws Exception {
		PreparedStatement ps = conn.prepareStatement(sql);
		ps.execute();
		ps.close();
	}

	public static void main(String[] args) throws Exception {
		doWork();
	}
}

Reply via email to