/*

   Derby - Class org.apache.derbyTesting.functionTests.tests.jdbcapi.SetQueryTimeoutTest

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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 org.apache.derbyTesting.functionTests.tests.jdbcapi;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import java.util.Collection;
import java.util.HashSet;
import java.util.Collections;

import org.apache.derby.tools.ij;

/**
 * Functional test for the Statement.setQueryTimeout() method.
 *
 * 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 (DERBY-1692).
 */
public class SetQueryTimeoutTest
{
    private static final int TIMEOUT = 1; // In seconds
    private static final int CONNECTIONS = 100;

    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) {
                    SetQueryTimeoutTest.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();
            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
    {
        HashSet<String> ignore = new HashSet<String>();
        ignore.add("42Y55");
        
        exec(conn, "drop table " + tablePrefix + "_orig", ignore);
        exec(conn, "drop table " + 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)");

        exec(conn,
             "insert into "
             + tablePrefix + "_orig"
             + " values(0),(1),(2),(3),(4),(5),(6)");
    }

    /**
     * This is the user-defined function which is called from our queries
     */
    public static int delay(int seconds, int value)
        throws
            SQLException
    {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            // Ignore
        }
        return value;
    }

    private static void prepareForTimedQueries(Connection conn)
        throws
            TestFailedException
    {
        System.out.println("Preparing for testing queries with timeout");

        try {
            conn.setAutoCommit(true);
        } catch (SQLException e) {
            throw new TestFailedException("Should not happen", e);
        }
        
        try {
            exec(conn, "DROP FUNCTION DELAY");
        } catch (Exception e) {
            // Ignore
        }

        exec(conn, "CREATE FUNCTION DELAY(SECONDS INTEGER, VALUE INTEGER) RETURNS INTEGER PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'org.apache.derbyTesting.functionTests.tests.jdbcapi.SetQueryTimeoutTest.delay'");

        prepareTables(conn, "t");
    }
    
    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";
    }
    
    public 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.
     */
    private static void testTimeoutWithFetch(Connection conn1,
                                             Connection conn2)
        throws
            TestFailedException
    {
        System.out.println("Testing timeout with fetch operations");

        try {
            conn1.setAutoCommit(false);
            conn2.setAutoCommit(false);
        } catch (SQLException e) {
            throw new TestFailedException("Should not happen", 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);
        }
    }

    /**
     * Part two of this test.
     */
    private static void testTimeoutWithExec(Connection[] connections)
        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("Should not happen", 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);
            }
        }
    }
    
    private static void testInvalidTimeoutValue(Connection conn)
        throws
            TestFailedException
    {
        System.out.println("Testing setting a negative timeout value");

        try {
            conn.setAutoCommit(true);
        } catch (SQLException e) {
            throw new TestFailedException("Should not happen", e);
        }

        // Create statement
        PreparedStatement stmt = null;
        try {
            stmt = conn.prepareStatement("select * from sys.systables");
        } catch (SQLException e) {
            throw new TestFailedException("Should not happen", 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("Should not happen", 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);
            }
        }
    }

    /** This tests timeout with executeUpdate call. */
    private static void testTimeoutWithExecuteUpdate(Connection conn)
        throws TestFailedException
    {
    	System.out.println("Testing timeout with executeUpdate call.");
        try{
            Statement stmt = conn.createStatement();
            stmt.setQueryTimeout(TIMEOUT);
            stmt.executeUpdate(getExecQuery("t"));    
        } catch (SQLException sqle) {
        	expectException("XCL52", sqle, "Should have timed out.");
        }
    }
    
    /** Test for DERBY-1692. */
    private static void testRememberTimeoutValue(Connection conn)
        throws TestFailedException
    {
        String sql = getFetchQuery("t");
        try {
            Statement stmt = conn.createStatement();
            testStatementRemembersTimeout(stmt);
            PreparedStatement ps = conn.prepareStatement(sql);
            testStatementRemembersTimeout(ps);
            CallableStatement cs = conn.prepareCall(sql);
            testStatementRemembersTimeout(cs);
        } catch (SQLException sqle) {
            throw new TestFailedException("Should not happen", sqle);
        }
    }

    /** Test that a statement remembers its timeout value when executed
     * multiple times. */
    private static void testStatementRemembersTimeout(Statement stmt)
        throws SQLException, TestFailedException
    {
        System.out.println("Testing that Statement remembers timeout.");
        stmt.setQueryTimeout(1);
        long runTime=0;
        for (int i = 0; i < 3; i++) {
            try {
                long startTime = System.currentTimeMillis();
                ResultSet rs = stmt.executeQuery(getFetchQuery("t"));
                while (rs.next());
                long endTime = System.currentTimeMillis();
                runTime = endTime - startTime;
                throw new TestFailedException("Should have timed out, for " +
                    "statement, iteration: " +i+ ", took (millis): "+runTime);
            } catch (SQLException sqle) {
                expectException("XCL52", sqle, "Should have timed out, got " +
                    "unexpected exception, for statement, iteration: " + i + 
                    ", time taken (millis): " + runTime);
            }
        }
        stmt.close();
    }

    /** Test that a prepared statement remembers its timeout value when
     * executed multiple times. */
    private static void testStatementRemembersTimeout(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++) {
            long runTime=0;
            try {
                long startTime = System.currentTimeMillis();
                ResultSet rs = ps.executeQuery();
                while (rs.next()); 
                long endTime = System.currentTimeMillis();
                runTime = endTime - startTime;
                throw new TestFailedException(
                    "Should have timed out, for " + name + ", on iteration " 
                    + i + ", runtime(millis): " + runTime);
           } catch (SQLException sqle) {
                expectException("XCL52", sqle, "Should have timed out, " +
                    "got unexpected exception, for " + name + ", on iteration "
                    + i + ", runtime(millis): " + runTime);
            }
        }
        ps.close();
    }

    /**
     * Main program, makes this class invocable from the command line
     */
    public static void main(String[] args)
    {
        new SetQueryTimeoutTest().go(args);
    }

    /**
     * The actual main bulk of this test.
     * Sets up the environment, prepares tables,
     * runs the tests, and shuts down.
     */
    public void go(String[] args)
    {
        System.out.println("Test SetQueryTimeoutTest starting");

        Connection[] connections = new Connection[CONNECTIONS];

        try {
            // Load the JDBC Driver class
            // use the ij utility to read the property file and
            // create connections
            ij.getPropertyArg(args);
            for (int i = 0; i < connections.length; ++i) {
                connections[i] = ij.startJBMS();
            }

            System.out.println("Got connections");

            for (int i = 0; i < connections.length; ++i) {
                connections[i].setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
            }

            prepareForTimedQueries(connections[0]);
            testTimeoutWithFetch(connections[0], connections[1]);
            testTimeoutWithExec(connections);
            testInvalidTimeoutValue(connections[0]);
            testRememberTimeoutValue(connections[0]);
            testTimeoutWithExecuteUpdate(connections[0]);
  
            System.out.println("Test SetQueryTimeoutTest PASSED");
        } catch (Throwable e) {
            System.out.println("Test SetQueryTimeoutTest FAILED");
            e.printStackTrace();
        } finally {
            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");
        }
    }
}
