/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package tests.java.sql; import SQLite.Database; import SQLite.Function; import SQLite.FunctionContext; import dalvik.annotation.TestTargets; import dalvik.annotation.TestLevel; import dalvik.annotation.TestTargetNew; import dalvik.annotation.TestTargetClass; import junit.extensions.TestSetup; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import tests.support.Support_SQL; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Collection; import java.util.Collections; import java.util.HashSet; /** * Functional test for the Statement.setQueryTimeout() method. Adopted from * Apache Derby project (Apache License 2.0). * * TODO Test requires transaction isolation to be supported. => Ticket 69 * * This test consists of four parts: 1. Executes a SELECT * query in 4 different threads concurrently. The query calls a user-defined, * server-side function which delays the execution, so that it takes several * seconds even though the data volume is really low. The fetch operations take * longer time than the timeout value set. Hence, this part tests getting * timeouts from calls to ResultSet.next(). Two connections are used, two * threads execute their statement in the context of one connection, the other * two threads in the context of the other connection. Of the 4 threads, only * one executes its statement with a timeout value. This way, the test ensures * that the correct statement is affected by setQueryTimeout(), regardless of * what connection/transaction it and other statements are executed in the * context of. * * 2. Executes an INSERT query in multiple threads. This part tests * getting timeouts from calls to Statement.execute(). Each thread executes the * query in the context of a separate connection. There is no point in executing * multiple statements on the same connection; since only one statement per * connection executes at a time, there will be no interleaving of execution * between them (contrary to the first part of this test, where calls to * ResultSet.next() may be interleaved between the different threads). Half of * the threads execute their statement with a timeout value set, this is to * verify that the correct statements are affected by the timeout, while the * other statements execute to completion. * 3. Sets an invalid (negative) * timeout. Verifies that the correct exception is thrown. * 4. Tests that the query timeout value is not forgotten after the execution of a statement. */ @TestTargetClass(Statement.class) public class QueryTimeoutTest extends TestCase { private static Statement statement; private static final int TIMEOUT = 1; // In seconds private static final int CONNECTIONS = 100; private static Connection[] connections = new Connection[CONNECTIONS]; private static void printSQLException(SQLException e) { while (e != null) { e.printStackTrace(); e = e.getNextException(); } } /** * This Exception class is used for getting fail-fast behaviour in this * test. There is no point in wasting cycles running a test to the end when * we know that it has failed. In order to enable chaining of exceptions in * J2ME, this class defines its own "cause", duplicating existing * functionality in J2SE. */ private static class TestFailedException extends Exception { private Throwable cause; public TestFailedException(Throwable t) { super(); cause = t; } public TestFailedException(String message) { super(message); cause = null; } public TestFailedException(String message, Throwable t) { super(message); cause = t; } public String toString() { if (cause != null) { return super.toString() + ": " + cause.toString(); } else { return super.toString(); } } public void printStackTrace() { super.printStackTrace(); if (cause != null) { if (cause instanceof SQLException) { QueryTimeoutTest.printSQLException((SQLException) cause); } else { cause.printStackTrace(); } } } } /** * Used for executing the SQL statements for setting up this test (the * preparation phase). The queries testing setQueryTimeout() are run by the * StatementExecutor class. */ private static void exec(Connection connection, String queryString, Collection ignoreExceptions) throws TestFailedException { Statement statement = null; try { statement = connection.createStatement(); System.out.println(" Executing "+queryString); statement.execute(queryString); } catch (SQLException e) { String sqlState = e.getSQLState(); if (!ignoreExceptions.contains(sqlState)) { throw new TestFailedException(e); // See finally block below } } finally { if (statement != null) { try { statement.close(); } catch (SQLException ee) { // This will discard an exception possibly thrown above :-( // But we don't worry too much about this, since: // 1. This is just a test // 2. We don't expect close() to throw // 3. If it does, this will be inspected by a developer throw new TestFailedException(ee); } } } } // Convenience method private static void exec(Connection connection, String queryString) throws TestFailedException { exec(connection, queryString, Collections.EMPTY_SET); } private static void dropTables(Connection conn, String tablePrefix) throws TestFailedException { Collection ignore = new HashSet(); //ignore.add("42Y55"); exec(conn, "drop table if exists " + tablePrefix + "_orig;", ignore); exec(conn, "drop table if exists " + tablePrefix + "_copy;", ignore); } private static void prepareTables(Connection conn, String tablePrefix) throws TestFailedException { System.out.println("Initializing tables with prefix " + tablePrefix); dropTables(conn, tablePrefix); exec(conn, "create table " + tablePrefix + "_orig (a int)"); exec(conn, "create table " + tablePrefix + "_copy (a int)"); for (int i = 0; i < 7; i++) { exec(conn, "insert into " + tablePrefix + "_orig" + " values ("+i+");"); } } private static String getFetchQuery(String tablePrefix) { /** * The reason for using the mod function here is to force at least one * invocation of ResultSet.next() to read more than one row from the * table before returning. This is necessary since timeout is checked * only when reading rows from base tables, and when the first row is * read, the query still has not exceeded the timeout. */ return "select a from " + tablePrefix + "_orig where mod(DELAY(1,a),3)=0"; } private static String getExecQuery(String tablePrefix) { return "insert into " + tablePrefix + "_copy select a from " + tablePrefix + "_orig where DELAY(1,1)=1"; } private static class StatementExecutor extends Thread { private PreparedStatement statement; private boolean doFetch; private int timeout; private SQLException sqlException; private String name; private long highestRunTime; public StatementExecutor(PreparedStatement statement, boolean doFetch, int timeout) { this.statement = statement; this.doFetch = doFetch; this.timeout = timeout; highestRunTime = 0; sqlException = null; if (timeout > 0) { try { statement.setQueryTimeout(timeout); } catch (SQLException e) { sqlException = e; } } } private void setHighestRunTime(long runTime) { synchronized (this) { highestRunTime = runTime; } } public long getHighestRunTime() { synchronized (this) { return highestRunTime; } } private boolean fetchRow(ResultSet resultSet) throws SQLException { long startTime = System.currentTimeMillis(); boolean hasNext = resultSet.next(); long endTime = System.currentTimeMillis(); long runTime = endTime - startTime; if (runTime > highestRunTime) setHighestRunTime(runTime); return hasNext; } public void run() { if (sqlException != null) return; ResultSet resultSet = null; try { if (doFetch) { long startTime = System.currentTimeMillis(); resultSet = statement.executeQuery(); long endTime = System.currentTimeMillis(); setHighestRunTime(endTime - startTime); while (fetchRow(resultSet)) { yield(); } } else { long startTime = System.currentTimeMillis(); statement.execute(); long endTime = System.currentTimeMillis(); setHighestRunTime(endTime - startTime); } } catch (SQLException e) { synchronized (this) { sqlException = e; } } finally { if (resultSet != null) { try { resultSet.close(); } catch (SQLException ex) { if (sqlException != null) { System.err.println("Discarding previous exception"); sqlException.printStackTrace(); } sqlException = ex; } } } } public SQLException getSQLException() { synchronized (this) { return sqlException; } } } /** * This method compares a thrown SQLException's SQLState value to an * expected SQLState. If they do not match, a TestFailedException is thrown * with the given message string. */ private static void expectException(String expectSqlState, SQLException sqlException, String failMsg) throws TestFailedException { if (sqlException == null) { throw new TestFailedException(failMsg); } else { String sqlState = sqlException.getSQLState(); if (!expectSqlState.equals(sqlState)) { throw new TestFailedException(sqlException); } } } // A convenience method which wraps a SQLException private static PreparedStatement prepare(Connection conn, String query) throws TestFailedException { try { return conn.prepareStatement(query); } catch (SQLException e) { throw new TestFailedException(e); } } /** * Part 1 of this test. */ @TestTargetNew( level = TestLevel.PARTIAL_COMPLETE, notes = "Testing timeout with fetch operations", method = "setQueryTimeout", args = {int.class} ) public static void testTimeoutWithFetch() throws TestFailedException { System.out.println("Testing timeout with fetch operations"); Connection conn1 = connections[0]; Connection conn2 = connections[1]; try { conn1.setAutoCommit(false); conn2.setAutoCommit(false); } catch (SQLException e) { throw new TestFailedException("Unexpected Exception", e); } // The idea with these 4 statements is as follows: // A - should time out // B - different stmt on the same connection; should NOT time out // C - different stmt on different connection; should NOT time out // D - here just to create equal contention on conn1 and conn2 PreparedStatement statementA = prepare(conn1, getFetchQuery("t")); PreparedStatement statementB = prepare(conn1, getFetchQuery("t")); PreparedStatement statementC = prepare(conn2, getFetchQuery("t")); PreparedStatement statementD = prepare(conn2, getFetchQuery("t")); StatementExecutor[] statementExecutor = new StatementExecutor[4]; statementExecutor[0] = new StatementExecutor(statementA, true, TIMEOUT); statementExecutor[1] = new StatementExecutor(statementB, true, 0); statementExecutor[2] = new StatementExecutor(statementC, true, 0); statementExecutor[3] = new StatementExecutor(statementD, true, 0); for (int i = 3; i >= 0; --i) { statementExecutor[i].start(); } for (int i = 0; i < 4; ++i) { try { statementExecutor[i].join(); } catch (InterruptedException e) { throw new TestFailedException("Should never happen", e); } } /** * Actually, there is no guarantee that setting a query timeout for a * statement will actually cause a timeout, even if execution of the * statement takes longer than the specified timeout. However, these * queries execute significantly longer than the specified query * timeout. Also, the cancellation mechanism implemented should be quite * responsive. In sum, we expect the statement to always time out. If it * does not time out, however, we print the highest execution time for * the query, as an assistance in determining why it failed. Compare the * number to the TIMEOUT constant in this class (note that the TIMEOUT * constant is in seconds, while the execution time is in milliseconds). */ expectException("XCL52", statementExecutor[0].getSQLException(), "fetch did not time out. Highest execution time: " + statementExecutor[0].getHighestRunTime() + " ms"); System.out.println("Statement 0 timed out"); for (int i = 1; i < 4; ++i) { SQLException sqlException = statementExecutor[i].getSQLException(); if (sqlException != null) { throw new TestFailedException("Unexpected exception in " + i, sqlException); } System.out.println("Statement " + i + " completed"); } try { statementA.close(); statementB.close(); statementC.close(); statementD.close(); conn1.commit(); conn2.commit(); } catch (SQLException e) { throw new TestFailedException(e); } } /** * * @test {@link java.sql.Statement#setQueryTimeout(int) } * * Part two of this test. */ @TestTargetNew( level = TestLevel.PARTIAL_COMPLETE, notes = "test timeout with st.exec()", method = "setQueryTimeout", args = {int.class} ) public static void testTimeoutWithExec() throws TestFailedException { System.out.println("Testing timeout with an execute operation"); for (int i = 0; i < connections.length; ++i) { try { connections[i].setAutoCommit(true); } catch (SQLException e) { throw new TestFailedException("Unexpected Exception", e); } } PreparedStatement statements[] = new PreparedStatement[connections.length]; for (int i = 0; i < statements.length; ++i) { statements[i] = prepare(connections[i], getExecQuery("t")); } StatementExecutor[] executors = new StatementExecutor[statements.length]; for (int i = 0; i < executors.length; ++i) { int timeout = (i % 2 == 0) ? TIMEOUT : 0; executors[i] = new StatementExecutor(statements[i], false, timeout); } for (int i = 0; i < executors.length; ++i) { executors[i].start(); } for (int i = 0; i < executors.length; ++i) { try { executors[i].join(); } catch (InterruptedException e) { throw new TestFailedException("Should never happen", e); } } /** * Actually, there is no guarantee that setting a query timeout for a * statement will actually cause a timeout, even if execution of the * statement takes longer than the specified timeout. However, these * queries execute significantly longer than the specified query * timeout. Also, the cancellation mechanism implemented should be quite * responsive. In sum, we expect the statement to always time out. If it * does not time out, however, we print the highest execution time for * the query, as an assistance in determining why it failed. Compare the * number to the TIMEOUT constant in this class (note that the TIMEOUT * constant is in seconds, while the execution time is in milliseconds). */ for (int i = 0; i < executors.length; ++i) { int timeout = (i % 2 == 0) ? TIMEOUT : 0; if (timeout > 0) { expectException("XCL52", executors[i].getSQLException(), "exec did not time out. Execution time: " + executors[i].getHighestRunTime() + " ms"); } else { SQLException sqlException = executors[i].getSQLException(); if (sqlException != null) { throw new TestFailedException(sqlException); } } } System.out .println("Statements that should time out timed out, and statements that should complete completed"); for (int i = 0; i < statements.length; ++i) { try { statements[i].close(); } catch (SQLException e) { throw new TestFailedException(e); } } } /** * * @test {@link java.sql.Statement#setQueryTimeout(int) } * */ @TestTargetNew( level = TestLevel.PARTIAL_COMPLETE, notes = "Testing setting a negative timeout value", method = "setQueryTimeout", args = {int.class} ) public static void testInvalidTimeoutValue(Connection conn) throws TestFailedException { try { conn.setAutoCommit(true); } catch (SQLException e) { throw new TestFailedException("Unexpected Exception", e); } // Create statement PreparedStatement stmt = null; try { stmt = conn.prepareStatement("select * from sys.systables"); } catch (SQLException e) { throw new TestFailedException("Unexpected Exception", e); } // Set (invalid) timeout value - expect exception try { stmt.setQueryTimeout(-1); } catch (SQLException e) { expectException("XJ074", e, "negative timeout value should give exception"); } System.out .println("Negative timeout value caused exception, as expected"); // Execute the statement and fetch result ResultSet rs = null; try { rs = stmt.executeQuery(); System.out.println("Execute returned a ResultSet"); rs.close(); } catch (SQLException e) { throw new TestFailedException("Unexpected Exception", e); } finally { try { stmt.close(); } catch (SQLException e) { // This will discard an exception possibly thrown above :-( // But we don't worry too much about this, since: // 1. This is just a test // 2. We don't expect close() to throw // 3. If it does, this will be inspected by a developer throw new TestFailedException("close should not throw", e); } } } /** * * @test {@link java.sql.Statement#setQueryTimeout(int) } * * Part two of this test. */ @TestTargetNew( level = TestLevel.PARTIAL_COMPLETE, notes = "timeout with executeUpdate call", method = "setQueryTimeout", args = {int.class} ) public static void testTimeoutWithExecuteUpdate() throws TestFailedException { System.out.println("Testing timeout with executeUpdate call."); try { Statement stmt = connections[0].createStatement(); stmt.setQueryTimeout(TIMEOUT); stmt.executeUpdate(getExecQuery("t")); } catch (SQLException sqle) { expectException("XCL52", sqle, "Should have timed out."); } } /** Test for DERBY-1692. */ @TestTargetNew( level = TestLevel.PARTIAL_COMPLETE, notes = "Testing that Statement considers timeout.", method = "setQueryTimeout", args = {int.class} ) public static void testRememberTimeoutValue() throws TestFailedException { String sql = getFetchQuery("t"); try { Statement stmt = connections[0].createStatement(); statementRemembersTimeout(stmt); PreparedStatement ps = connections[0].prepareStatement(sql); statementRemembersTimeout(ps); CallableStatement cs = connections[0].prepareCall(sql); statementRemembersTimeout(cs); } catch (SQLException sqle) { throw new TestFailedException("Unexpected Exception", sqle); } } public static void statementRemembersTimeout(Statement stmt) throws SQLException, TestFailedException { System.out.println("Testing that Statement remembers timeout."); stmt.setQueryTimeout(1); for (int i = 0; i < 3; i++) { try { ResultSet rs = stmt.executeQuery(getFetchQuery("t")); while (rs.next()) { // do nothing } throw new TestFailedException("Should have timed out."); } catch (SQLException sqle) { expectException("XCL52", sqle, "Should have timed out."); } } stmt.close(); } private static void statementRemembersTimeout(PreparedStatement ps) throws SQLException, TestFailedException { String name = (ps instanceof CallableStatement) ? "CallableStatement" : "PreparedStatement"; System.out.println("Testing that " + name + " remembers timeout."); ps.setQueryTimeout(1); for (int i = 0; i < 3; i++) { try { ResultSet rs = ps.executeQuery(); while (rs.next()) { // do nothing } throw new TestFailedException("Should have timed out."); } catch (SQLException sqle) { expectException("XCL52", sqle, "Should have timed out."); } } ps.close(); } /** * A function * arg0 : int seconds * */ static class Delay implements SQLite.Function { public void function(FunctionContext fc, String[] args) { int seconds = new Integer(args[0]).intValue(); int value = new Integer(args[1]).intValue(); try { Thread.sleep(seconds * 1000); } catch (InterruptedException e) { // Ignore } fc.set_result(value); } public void last_step(FunctionContext fc) { // TODO Auto-generated method stub } public void step(FunctionContext fc, String[] args) { // TODO Auto-generated method stub } } /** * The actual main bulk of this test. Sets up the environment, prepares * tables, runs the tests, and shuts down. */ public static Test suite() { TestSetup setup = new TestSetup( new TestSuite (QueryTimeoutTest.class)) { public void setUp() { // Establish connections Support_SQL.loadDriver(); try { for (int i = 0; i < connections.length; ++i) { connections[i] = Support_SQL.getConnection(); } for (int i = 0; i < connections.length; ++i) { connections[i] .setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); } // setup Delay function prepare(); } catch (Throwable e) { fail("Unexpected SQLException " + e.toString()); } System.out.println("Connections set up"); } public void tearDown() { for (int i = connections.length - 1; i >= 0; --i) { if (connections[i] != null) { try { connections[i].close(); } catch (SQLException ex) { printSQLException(ex); } } } System.out.println("Closed connections"); } public void prepare() throws TestFailedException { System.out.println("Preparing for testing queries with timeout"); Database db = new Database(); Connection conn = connections[0]; try { db.open(Support_SQL.getFilename(), 1); conn.setAutoCommit(true); } catch (Exception e) { throw new TestFailedException("Unexpected Exception", e); } Function delayFc = new Delay(); db.create_function("DELAY", 2, delayFc); prepareTables(conn, "t"); } }; TestSuite ts = new TestSuite(); ts.addTestSuite(QueryTimeoutTest.class); return setup; } }